什么是领域驱动
领域模型是通过识别领域对象与行为来连接现实主体与操作之间的映射关系。对象行为的组织原则更体现面向对象对象设计思想,通过聚合,解耦抽象等方式达到系统的可复用,可维护,可扩展能力。
MVC
MVC 三层架构中 M 表示 model ,V 表示的是 View, C 表示的是 Controller, 也就是分成了三层:数据层, 表示层,逻辑层。
- 模型:负责存储系统的中心数据
- 视图:将数据显示给用户
- 控制器:处理用户输入的信息,负责读取视图数据,控制用户输入,并向模型写入数据。
后端项目一般是分成三层: Repository 层,Service 层,Controller 层,其中,Repository 负责数据访问,Service 负责处理业务逻辑, Controller 负责暴露接口。
什么是贫血模型
贫血模型领域对象很简单,只有所有属性的 get/set 方式,与少量的属性值转换,不包含任何业务逻辑, 不关心数据对象的持久化,只用来做数据对象的承载和传递介质。
领域对象
@Entity @Data @ToString @AllArgsConstructor @NoArgsConstructor public class User { @Id private String userId; private String userName; private String password; private boolean isLock; }
真正的业务逻辑在领域服务中负责实现,此服务中引入持久化仓库, 在业务逻辑之后持久化到仓库中,并可发布领域事件。
Service 层, 领域服务, 业务处理,使用领域对象进行数据承载。
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); // 发布领域事件 ... } // ... 省略完整代码 }
再看个贫血模型的具体例子:
////////// Controller+VO(View Object) ////////// public class UserController { private UserService userService; //通过构造函数或者IOC框架注入 public UserVo getUserById(Long userId) { UserBo userBo = userService.getUserById(userId); UserVo userVo = [...convert userBo to userVo...]; return userVo; } } public class UserVo {//省略其他属性、get/set/construct方法 private Long id; private String name; private String cellphone; } ////////// Service+BO(Business Object) ////////// public class UserService { private UserRepository userRepository; //通过构造函数或者IOC框架注入 public UserBo getUserById(Long userId) { UserEntity userEntity = userRepository.getUserById(userId); UserBo userBo = [...convert userEntity to userBo...]; return userBo; } } public class UserBo {//省略其他属性、get/set/construct方法 private Long id; private String name; private String cellphone; } ////////// Repository+Entity ////////// public class UserRepository { public UserEntity getUserById(Long userId) { //... } } public class UserEntity {//省略其他属性、get/set/construct方法 private Long id; private String name; private String cellphone; }
UserEntity 和 UserRepository 负责数据访问, UserBo 和 UserService 负责处理业务逻辑, UserVo 和 UserController 属于接口层。
这样 UserBo 中只包含属性数据,不包含业务逻辑的累,叫做贫血模式
传统的贫血模型其实很受欢迎, 贫血模型是面向过程的编程风格。
- 开发系统业务比较简单,只要实现 CRUD 操作即可, 不需要费力设计充血模型, 其次贫血模型足够应对这种简单的业务操作。
- 充血模型的设计比贫血模型更加有难度,充血模型是面向对象的编程风格,一开始就要想好,对数据要暴露哪些操作,定义哪些业务逻辑, 而不是和贫血模型一样,只要设计好模型,后续有什么功能需求,Service 就定义什么操作。
- 思维固化,转型有成本,基于贫血模型开发多年,一下要转换成充血模型,领域驱动,有一定的学习成本。
充血模型
充血模型包含领域对象作用此对象的相关行为, 领域对象不仅包含属性,还包含业务处理逻辑,同时也包含对象的持久化操作。
@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); // 发布领域事件 ... } }
领域模型的优点
- 对象自治度很高,表达能力很强,非常适合复杂的业务逻辑的实现,可靠复用程度比较高。更符合面向对象的思想。
- 当前充血模型不够纯粹,将业务逻辑和持久化操作放在一块,有一定的耦合度。
在领域对象行为比较复杂的情况下, 需要多个行为共享对象状态时,充血模型表现更强。
充血模型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); // 发布领域事件 ... } }
- 去掉了持久化的入侵,业务更加纯粹
什么场景选择充血模型
基于充血模型的 DDD 开发模式,更适合复杂的系统开发,比如包含各种利息计算模型,还款模型的复杂业务的金融系统。
领域驱动设计主要是用来指导如何解耦业务系统,划分业务模块, 定义业务领域模型及其交互。他的发展主要是由于微服务,微服务不仅仅要关注服务治理,监控,调用链追踪, API 网关等问题,还有个重要的问题,就是对公司业务进行拆分,形成微服务。对领域驱动设计关键在于对业务的熟悉理解程度,领域驱动设计的思想是:轻 Service ,重 Domain。
我们平时开发时,大部分是 SQL 驱动,接到一个后端开发需求时,首先是去看接口如何对应到数据库中,要访问哪些表,哪些库,然后思考 SQL 如何写。这样会看到,类似的SQL 到处都是,通常为一个 需求写一个 SQL。
对于领域驱动,我们首先需要理清所有业务,包括定义模型包含的属性和方法,领域模型相当于可复用业务的中间层,新的需求需要基于之前定义好的业务来完成。