若你是初涉或者从未涉及过领域驱动设计,你的思维会比较固定,如为什么我们需要领域驱动设计,长期以来你习惯以数据表为核心进行分析设计,想着某个功能我们应该如何建表。我敢打包票,在上面讲解的功能过程中,你一开始就在思考这个表怎么设计的,我从表中取哪个字段再怎样怎样,这是绝对的被数据库,被技术绑架了的思维但是,问题也在于,我们最终总是需要解决数据库这个问题,这个问题也就是在领域服务中解决。
领域服务解决的另一个问题是组装逻辑,举个例子,LoginUserE.DoVerify虽然不依赖任何第三方包或者同级的其他类,但是他的入參被我们依赖隔离,这个入參可能就依赖其他domain,因此,我们需要领域服务去组装这一层。
我们来讲一下登陆是如何实现的,首先我们定义LoginCmd为Login的入參,
这里就到了设计数据库的地方,我们需要去查找判断这个用户是否存在,那么问题来了,我们不能直接依赖数据库技术,但是我们又需要,这可咋整?类似的当然是定义接口
type LoginUserRepo interface { GetOne(username, tenantId string) *domain.LoginUserDO }但是这里又回到了上文讨论依赖注入的地方了,这里我为了简单起见,仍然没有用到依赖注入,但是我个人是建议使用的
var loginService *LoginService type LoginService struct { LoginUserRepo } func NewLoginService(repo LoginUserRepo,) *LoginService { //do not argue to use double check lock,it's a example and does not hurt anyway if loginService == nil { return &LoginService{ LoginUserRepo: repo, } } else { return loginService } } 这样我们就在初始化LoginService的时候将repoImpl传送进行,达到了依赖隔离的目的。回到GetOne(username, tenantId string) *domain.LoginUserDO这个方法,这里还暴露了一个点在于,DataObject类是定义在domain层中,而不是在service,更不是在base中,我以前纠结的一点是,既然domain层不依赖数据库技术,是不是也应该不关心DataObject,DataObject是不是放在base层下更加合适,但是现在之所以把DataObject放在domain层,原因在于1.domain核心层不直接依赖其他层,如果DataObject放在base层势必违背这点;2.domain层作为接口定义者,有权根据他自身的需求定义他想要的存储内容,其他层只需要服从并且实现。
同时,我们不希望代码中充斥着大量的convert,从cmd转到DO,从DO转到E,所以我们提炼出了dto.go这个文件,用于存放concert代码。最终的代码形式如下。
新的风暴又出现了,service.token(userE.UniqueCode, loginCmd.EffectiveSeconds)这段逻辑是什么意思,上文中也没有出现,待我慢慢需讲解。正常登陆下我们校验成功之后需要授予token,但是token的生成技术细节,用JWT还是什么其他的,domain不应该关心,所以我们给loginService 加一个类型为函数的field
type LoginService struct { LoginUserRepo token func(uniqueCode string, effectiveSeconds int) string } func NewLoginService(repo LoginUserRepo, token func(uniqueCode string, effectiveSeconds int) string) *LoginService { if loginService == nil { return &LoginService{ LoginUserRepo: repo, token: token, } } else { return loginService } } 测试驱动使领域驱动更加完美全篇下来的奥义在于隔离依赖,这些都是经验积累,有没有行之有效的规范得以遵守,答案是我也不知道,但是如果你遵守测试驱动的行为的话,这会迫使你去思考,什么该依赖,什么不该依赖,因为所有的第三方依赖,都需要用Mock去代替,这就是为什么目录中存在mocks这个文件。测试写得好,烦恼多不了。
总结