https://segmentfault.com/a/1190000023476636
领域建模是通过识别领域对象与行为来连接与现实世界业务主体与操作的映射关系。对象与行为的组织设计原则更体现面向对象设计的思想,通过聚合、解耦、抽象、组合等多种设计方式达到系统可复用,可维护,易扩展的能力。
在实际程序代码设计中,由于语言、结构、技术的不一样对领域建模代码落地也有所不同,且各有优缺点。
一、贫血模型
此种模型下领域对象的作用很简单,只有所有属性的get/set方式,以及少量简单的属性值转换,不包含任何业务逻辑,不关系对象持久化,只是用来做为数据对象的承载和传递的介质。
@Entity @Data @ToString @AllArgsConstructor @NoArgsConstructor public class User { @Id private String userId; private String userName; private String password; private boolean isLock; }
而真正的业务逻辑则由领域服务负责实现,此服务引入持久化仓库,在业务逻辑完成之后持久化到仓库中,并在此可以发布领域事件(Domain Event)
public interface UserService { void create(User user); void edit(User user); void changePassword(String userId, String newPassword); void lock(String userId); void unlock(String userId); } @Service public class UserServiceImpl implements UserService { @Autowired private UserRepository repo; @Override public void edit(User user) { User dbUser = repo.findById(user.getUserId()).get(); dbUser.setUserName(user.getUserName()); repo.save(dbUser); // 发布领域事件 ... } @Override public void lock(String userId) { User dbUser = repo.findById(userId).get(); dbUser.setLock(true); repo.save(dbUser); // 发布领域事件 ... } // ... 省略完整代码 }
优点: 结构简单,职责单一,相互隔离性好,使用单例模型提高运行性能
缺点: 对象状态与行为分离,不能直观地描述领域对象。行为的设计主要考虑参数的输入和输出而非行为本身,不太具有面向对象设计的思考方式。行为间关联性较小,更像是面向过程式的方法,可复用性也较小。
SpringBoot 采用单例模式,尽量不手动创建对象,对象无状态化,故较推荐使用贫血模型
二、 充血模型
此种模型下领域对象作用此领域相关行为,包含此领域相关的业务逻辑,同时也包含对领域对象的持久化操作。
@Entity @Data @Builder @AllArgsConstructor public class User implements UserService { @Id private String userId; private String userName; private String password; private boolean isLock; // 持久化仓库 @Transient private UserRepository repo; // 是否是持久化对象 @Transient private boolean isRepository; @PostLoad public void per() { isRepository = true; } public User() { } public User(UserRepository repo) { this.repo = repo; } @Override public void create(User user) { repo.save(user); } @Override public void edit(User user) { if (!isRepository) { throw new RuntimeException("用户不存在"); } userName = user.userName; repo.save(this); // 发布领域事件 ... } @Override public void lock() { if (!isRepository) { throw new RuntimeException("用户不存在"); } isLock = true; repo.save(this); // 发布领域事件 ... } }
优点: 对象自洽程度很高,表达能力很强,因此非常适合于复杂的企业业务逻辑的实现,以及可复用程度比较高,更符合面向对象设计思想
缺点: 对象属性中掺杂持久化仓库,不够纯粹,持久化操作是否属于业务逻辑有待求证。但由于持久化仅需暴露接口,对业务逻辑与持久化操作的耦合度有一定降低。
说明: 有人认为对象中的Create()
,是新建对象方法不应该属于对象本身,应由其它对象产生或static方法产生。我的理解是不能把业务对象中的新建和程序对象上的新建混淆。业务对象的新建是指的是业务行为操作得出的结果,理应属于对象本身行为。而程序里的新建则是对象初始化过程New()
,这是程序构建逻辑不是业务概念,不能相等对待。
在领域对象行为逻辑较复杂的情况下,需要多个行为共享对象状态的时候,充血模型表现力更强,个人比较推荐此种模型
三、充血模型2
为了解决业务逻辑不纯粹问题,也有将持久化操作移出业务逻辑的作法。
@Entity @Data @Builder @AllArgsConstructor public class User implements UserService { @Id private String userId; private String userName; private String password; private boolean isLock; // 是否是持久化对象 @Transient private boolean isRepository; @Override public void create(User user) { user.userId = UUID.randomUUID().toString(); } @Override public void edit(User user) { userName = user.userName; } @Override public void lock() { isLock = true; } } @Service public class UserManager { @Autowired private UserRepository repo; public User findOne(String userId){ return repo.findById(userId).get(); } public void edit(User u) { User user = findOne(u.getUserId()); user.edit(u); repo.save(user); // 发布领域事件 ... } public void lock(String userId) { User user = findOne(userId); user.lock(); repo.save(user); // 发布领域事件 ... } }
优点: 保持了业务逻辑的纯粹性,去掉了持久化的入侵
缺点: 降低了领域服务的自治性,破坏了行为逻辑的完整性,部分逻辑混入了application
层,尤其是领域事件的发布
此种方式是前两种方式的折中,充分地做到了解耦,但也牺牲了部分内聚
四、总结
架构设计是一项持续性演进性的工作,不是一成不变的。架构的选择并没有好坏只有适合,每一种都有自己的使用场景。如何选择需要自身理论支持,保持相对方向性统一,并持续审视是否符合预期目标。
五、 源码
https://gitee.com/hypier/barr...
发布于: 2021 年 04 月 09 日
面试官问:随着微服务架构的普及,领域驱动设计也焕发了新春,得到了大范围的推广,在代码实现层面,领域驱动设计相比数据表驱动设计的主要区别就是充血模型和贫血模型,你能聊聊对这两个模型的理解吗?
候选人甲:额,这个没有听说过...
面试官:好的,没关系,那我们继续其他问题...
旁白:确实有很多候选人没有了解过充血模型和贫血模型,虽然平时开发中可能一直在使用贫血模型而不自知。此外,领域驱动设计已经在很多公司或多或少实践着,例如:
所以,作为一名现代的后端开发,掌握领域驱动设计的基本概念和思想能够让你无论是在平时项目开发中还是跳槽面试中受益匪浅。
候选人乙:说到充血模型和贫血模型区别,不得不先聊一聊领域驱动设计和数据表驱动设计:
-
领域驱动设计:系统的设计以面向对象的方式进行,当拿到需求后,首先进行用例模型设计,分析整个系统要实现哪些功能;然后进行领域模型的设计,识别出系统中的实体、属性和关系,可以采用类图的形式,每个类通过它的属性来描述数据结构,通过类方法来描述对数据结构的操作。领域模型设计好之后,最后才是进行数据库的设计,此时,数据库设计就弱化成领域对象的一种持久化方式而已,实际的业务逻辑都封装在领域对象中,如果需要的话,可以将持久化实现为插件式方案,既可以使用 MySQL 来存储,也可以使用 MongoDB 来存储,但无论采用何种存储,领域对象都是不变的。这种既包含数据,也包含数据对应的业务逻辑的类设计方式就被称为充血模型(Rich Domain Model),它满足面向对象的封装特性,是典型的面向对象编程风格。
-
数据表驱动设计:系统的设计以数据库表设计为核心,当拿到需求并进行分析后,首先把数据库表设计好之后,再通过工具自动生成数据库表对应的 Mapper、DO、BO、Service、VO、Controller 等对象。在这种模式下,Service 层的数据和业务逻辑被分割到 BO 和 Service 两个类中。其中,像 BO 这种只包含属性以及属性对应的 setter、getter 方法的类,本质上就是一个纯粹的数据结构类,不包含业务逻辑,因此被称为贫血模型(Anemic Domain Model)。同理,DO 和 VO 也都是基于贫血模型的设计。贫血模型将数据和操作分离,破坏了面向对象的封装特性,是一种典型的面向过程的编程风格。
面试官:好的,那充血模型相比贫血模型的优点是什么?
候选人乙:首先从代码可读性方面看,充血模型的实体类由于包含业务逻辑,符合人类的思维模式,因此易于阅读与理解;而贫血模型的实体类不包含业务逻辑,相关操作都封装在 Service 甚至 SQL 语句中,因此,代码可读性可理解性较差。此外,从可测试性方面看,充血模型的设计将业务操作与数据库操作解耦了,因此,在测试业务功能时,可以仅通过对实体类进行单元测试来验证业务逻辑的正确性,效率高;而贫血模型中,实体类是纯粹的数据结构,不需要测试,但需要对 Mapper 类写数据库集成测试,而核心业务逻辑封装在 Service 中,因此单元测试重点是对 Service 的测试,但 Service 中的方法逻辑是面向过程的封装,可测试性并不好,说白了就是单元测试可能没那么好写。
面试官:好的,那既然充血模型有这么多优点,那为何基于贫血模型的传统开发模式还是如此受欢迎呢?
候选人乙:主要有几个原因,首先数据库表驱动设计这种传统方式已经深入人心,大家用起来很顺手,在没有外因或者内因驱动的情况下,大家更倾向待在舒适区;其次充血模型的设计相比贫血模型而已,有一定的学习门槛,设计起来也会更花费时间和精力;最后,对大多数系统来说,可能没有太复杂的业务逻辑,因此基于贫血模型的表驱动设计就能够满足需求,也不会给后续系统的维护升级带来太多影响,因此也体现不出使用充血模型的价值。
面试官:好的,那什么项目适合使用基于充血模型的 DDD 开发模式呢?
候选人乙:从领域驱动设计的经典之作《领域驱动设计:软件核心复杂性应对之道》书名中我们可以看出,DDD 适用于复杂度较高的软件设计,例如企业级软件设计。其次,从系统的分类来看,可以分为 OLTP 系统和 OLAP 系统,前者是在线事务系统,也就是平时大家接触较多的业务系统,而后者是在线分析系统,也就是统计分析类系统,大部分是从大数据平台中获取数据进行报表、大屏等的展示。基于充血模型的 DDD 开发模式显然更适合 OLTP 系统,而 OLAP 系统还是老老实实采用传统的表驱动设计就行了,别瞎折腾。