领域驱动最佳实践--用代码来告诉你来如何进行领域驱动设计 (2)

     若你是初涉或者从未涉及过领域驱动设计,你的思维会比较固定,如为什么我们需要领域驱动设计,长期以来你习惯以数据表为核心进行分析设计,想着某个功能我们应该如何建表。我敢打包票,在上面讲解的功能过程中,你一开始就在思考这个表怎么设计的,我从表中取哪个字段再怎样怎样,这是绝对的被数据库,被技术绑架了的思维但是,问题也在于,我们最终总是需要解决数据库这个问题,这个问题也就是在领域服务中解决。
领域服务解决的另一个问题是组装逻辑,举个例子,LoginUserE.DoVerify虽然不依赖任何第三方包或者同级的其他类,但是他的入參被我们依赖隔离,这个入參可能就依赖其他domain,因此,我们需要领域服务去组装这一层。
我们来讲一下登陆是如何实现的,首先我们定义LoginCmd为Login的入參,

//implemention will show right behind this func (service *LoginService) Login(loginCmd common.LoginCmd) (string, error) type LoginCmd struct { Username string TenantId string EffectiveSeconds int Mobile string SourceCode string LoginWay string EncryptWay string }

     这里就到了设计数据库的地方,我们需要去查找判断这个用户是否存在,那么问题来了,我们不能直接依赖数据库技术,但是我们又需要,这可咋整?类似的当然是定义接口

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代码。最终的代码形式如下。

func (service *LoginService) Login(loginCmd common.LoginCmd) (string, error) { userDO := service.GetOne(loginCmd.Username, loginCmd.TenantId) userE := common.ToLoginUserE(*userDO) userE.EncryptWay = domain.EncryptWay(loginCmd.EncryptWay) //login way contains PASSWORD and SMS ,encryptCode()is to get which one to be verify ,so userE will not to care about which way is exactly by logining encryptCode := service.encryptCode(loginCmd.LoginWay, userDO) if _, err := userE.DoVerify(loginCmd.SourceCode, encryptCode); err != nil { return "", err } //todo add login event and callback return service.token(userE.UniqueCode, loginCmd.EffectiveSeconds), nil } func (service *LoginService) encryptCode(way string, userDO *domain.LoginUserDO) string { switch way { case "PASSWORD": return userDO.Password case "SMS": return service.FindSmsCode(userDO.Mobile) default: panic("unknown login way") } }

      新的风暴又出现了,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这个文件。测试写得好,烦恼多不了。

总结

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zyggdz.html