不知道你是否与笔者一样长久以来一直有一些纠结的疑惑:
-
如果把提供 Web api 的 Controller 看做一个 Facade,那么 Application Service 作为另一个 Facade 是否还有存在的必要?
-
当某个业务操作需要向外部系统发消息时,是否可以直接依赖外部系统的 Web Service 接口?还是应该依赖更为抽象的接口?这个接口定义在哪一层?
-
Repository 是否和 DAO 是同一个东西的两个名字?为什么只有聚合根才能有 Repository?
我们可以借助六边形架构让系统结构更加清晰。六边形架构的思想是将输入和输出都放在设计的边缘部分,并且明确区分系统的内部
和外部
。系统内部通过端口和适配器与系统外部交互。可以把系统内部
想象为一个手机,端口
可以是一个 type-c 接口,适配器
可以是任意一个符合标准的电源适配器
,或者任意一个符合标准的充电宝
。手机的充电模块可以只针对 type-c 接口和相关标准进行设计。一个端口
可以通过任意多个适配器
与系统外部环境交互。
适配器分为主动适配器(Driving Adapters)
和被动适配器(Driven Dadpters)
两种。
-
主动适配器(Driving Adapters),也就是适配器会主动调用端口。典型的主动适配器是 Rest Web api 和 WebService api。Application Service 层非常适合作为被它们调用的端口。
-
被动适配器(Driven Dadpters),是指适配器被系统内部调用。由于不允许系统内部直接依赖适配器,所以要使用依赖倒置,只允许系统内部依赖一些窄接口(作为被动适配器的端口),然后由被动适配器实现这些接口。典型的被动适配器包括向外部系统发消息的 WebService 客户端、消息队列,以及访问数据库的 DAO。
对于企业信息系统,数据库访问的代码量很大。为简单起见,数据库访问的端口就直接使用Mybatis plus的mapper接口,例如:
package com.bzb.atjob.app.auth.adaptor.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bzb.atjob.app.auth.core.dept.model.Dept;
public interface DeptMapper extends BaseMapper<Dept> {}
端口的数据库访问适配器会由 Mybatis plus 自动生成,不需要我们自己写实现类。
尽量不使用 SQL 层面的关联查询,当一个聚合根需要查询另一个聚合根时,可依赖一个作为端口的窄接口:
package com.bzb.atjob.app.auth.adaptor.finder;
import com.bzb.atjob.app.auth.core.role.model.Role;
@FunctionalInterface
public interface FindRoleById {
Role findRoleById(String roleId);
}
此端口可以有多个适配器:
-
如果 Role 存在于外部系统,需要增加一个使用 WebService 客户端获取数据的实现类。
-
如果 Role 存在于系统内部,为简单起见,直接让 RoleRepository 实现该端口:
package com.bzb.atjob.app.auth.core.role.repository; @Service @RequiredArgsConstructor public class RoleRepository implements FindRoleById { @Override public Role findRoleById(String roleId) { return this.byId(roleId); } }
Repository 不是 DAO。Repository 是更为抽象一点的聚合根的“家”。可以这样想象一下,如果我们拥有一台无限内存,永不宕机的服务器——也就是不需要数据持久化,是否还需要Repository?
即使这样我们仍然需要Repository。
-
我们需要 DeptRepository 告诉我们目前还有多少存活的 Dept 实例。
-
User 只保留了 deptId,它需要能够使用此Id得到 Dept 实例。