应用架构
贫血模型和充血模型 经典贫血三层架构以及DDD常见的应用架构。不管哪种架构,都是对系统的描述,可能在模块、代码组织上有区别。本质上描述同一个事物
常见问题
- 领域模型: 在DDD中,领域模型是对业务领域中概念和规则的抽象,它直接反映了业务的本质。这个模型是基于领域专家的知识和经验,作为团队理解业务并进行软件设计的基础。它负责表达业务概念和规则,是软件系统的核心。
- 与领域专家的合作: 强调与领域专家合作是为了充分了解业务需求和规则。这种合作有助于确保软件模型真实地反映业务需求,从而创建更符合业务目标的系统。领域专家的经验和洞察力有助于更好地塑造领域模型。
- 聚合和聚合根: 聚合是一组关联实体和值对象,它们一起完成某种业务功能。聚合根是聚合的主要实体,它负责确保聚合内的所有元素保持一致性。聚合根对外部对象可见,外部对象只能通过聚合根来访问聚合内的内容,从而保证了数据的完整性和领域规则的实施。
- 与外部系统集成: 防腐层是在与外部系统集成时的一种策略,用于隔离和转换外部系统与领域模型之间的交互。这一层的存在确保了领域模型不会受到外部系统变化的影响,同时也确保了数据交互的一致性。
- 适合采用DDD的项目: DDD最适合处理业务逻辑复杂、经常变化,并需要建立可持续发展、易于维护和扩展的系统的项目。在这样的情况下,DDD提供了一种有组织、结构化的方法来管理复杂性。但对于简单的项目,DDD可能会带来不必要的复杂性,因此需要权衡利弊。
模式对比
贫血模式:
- 方便性:相对简单,方便使用。在Java等编程语言中,使用ORM框架从数据库获取数据、在Service层进行业务逻辑的操作很常见
- 业务逻辑的封装问题:贫血模型中业务知识散落到Service层各个方法
- 业务逻辑与技术实现的分离问题:贫血模型实现的Service层通常难以对业务逻辑和技术实现进行有效的分离,导致代码的可维护性和清晰性下降
- 难以维护的问题:贫血模型的系统难以持续演化和维护,特别是多次迭代之后,Service层方法可能变得臃肿
充血模式:
- 业务逻辑的集中:在充血模型中,业务逻辑不仅仅局限于Service业务行为,而是直接存在于领域对象中。业务规则和操作在领域对象内部进行封装,形成更加集中的自洽业务逻辑
- 更好的封装性:提高了代码的模块性和可维护性
- 领域对象的自我管理:能够自我管理自身的状态和行为。确保自身处于一致的状态,并执行特定业务规则,不需要依赖外部Service层的操作
- 减少重复代码:可以减少在Service层中重复执行相似业务逻辑的情况
- 更直观的代码:充血模型是的代码更加直观,业务逻辑和领域对象的行为直接反映了实际业务的需求,而不是简单数据容器
DDD 常见应用架构
经典四层架构
用户接口层(User Interface)
- 责任: 负责与用户进行交互,接收用户输入,展示系统输出。
- 组成: 包括各种图形界面、Web 界面、移动端应用等。
- 特点: 不包含业务逻辑,只负责将用户请求传递给下一层处理,并按照协议返回执行结果。
应用层(Application)
- 责任: 协调领域模型和基础设施层,完成业务操作。
- 组成: 包含应用服务,负责加载领域模型中的聚合根,执行业务操作,返回处理结果。
- 特点: 不包含具体业务逻辑,专注于调度领域模型的操作。 领域模型层(Domain Model)
- 责任: 描述业务领域中的概念和规则,包括实体、值对象、领域服务等。
- 组成: 包含业务逻辑和规则,以及实体、值对象、领域服务等。
- 特点: 是软件系统的核心,业务逻辑和规则在这一层得到实现,但不包含任何与具体技术相关的代码。
基础设施层(Infrastructure)
- 责任: 提供基础设施支持,如数据库访问、缓存、消息队列、日志等。
- 组成: 包括与具体技术相关的代码,但不包含业务逻辑。
- 特点: 为上层层次提供支持,是系统的最底层,专注于处理技术细节。
端口和适配器架构(六边形架构)
- 外部世界(External World): 表示系统外部的各种组件、服务、用户界面等。
- 进站端口(Inbound Ports): 与外部世界进行交互的接口,定义系统对外提供的服务。
- 出站端口(Outbound Ports): 用于执行业务逻辑过程中需要依赖外部服务的接口。
- 驱动适配器(Driving Adapters): 将系统内部的业务逻辑通过端口暴露给外部,可以是 RESTful 接口、RPC 服务等。
- 业务逻辑(Business Logic): 系统的核心业务逻辑,独立于具体的技术实现。
- 被驱动适配器(Driven Adapters): 实现业务逻辑执行过程中需要使用的端口,与外部服务或数据进行适配。
/**
* 主动适配器,将创建文章的Port暴露为Http服务
*/
@RestController
public class ArticleController {
@Resource
private ArticleService service;
@RequestMapping("/create")
public void create(DTO dto) {
service.create(dto);
}
}
public interface ArticleService {
/**
* 端口和适配器架构中的Port,提供创建文章的能力
* 这是一个进站端口
* @param dto
*/
void create(DTO dto);
}
public interface AuthorServiceGateway {
/**
* 端口和适配器架构中的Port,查询作者信息
* 这是一个出站端口
* @param authorId
* @return
*/
AuthorDto queryAuthor(String authorId);
}
/**
* 被动适配器
*/
public interface AuthorServiceGatewayImpl implements AuthorServiceGateway {
/**
* 作家RPC服务
*/
@Resource
private AuthorServiceRpc rpc;
AuthorDto queryAuthor(String authorId) {
//拼装报文
AuthorRequest req = this.createRequest(authorId);
//执行RPC查询
AuthorResponse res = rpc.queryAuthor();
//解析查询结果并返回
return this.handleAuthorResponse(res);
}
}
应用架构演化
第一步 数据模型与 DAO 层合并
- 一一对应的映射: 数据模型通常与数据库表的列一一对应,而DAO层负责数据库的读写操作。将它们合并可以简化数据模型与数据库之间的映射关系,减少重复的代码,提高开发效率。
- 领域模型的封装: 数据模型作为贫血模型,主要用于持久层,其属性与数据库列一致,而业务逻辑通常在领域模型中。将数据模型与DAO层合并可以更方便地封装领域模型的业务逻辑,使其更贴近业务需求。
- 简化持久化操作: 数据模型通常带有ORM框架的注解,用于指导数据库持久化操作。将数据模型与DAO层合并可以更好地组织这些注解,简化持久化操作的配置和管理。
- 业务逻辑与持久层的耦合度: 数据模型通常在业务逻辑中起到承载数据的作用,与数据库交互。将数据模型与DAO层合并有助于降低业务逻辑与持久层之间的耦合度,使业务逻辑更专注于领域模型的操作。
- 一体化的设计: 数据模型和DAO层往往紧密相关,将它们合并可以使整体系统的设计更一体化,更容易理解和维护
第一步 Service 层抽取业务逻辑
-
原始 Service 层:
- 业务逻辑和基础设施调用混合在一个 Service 方法中,导致方法臃肿,不易维护。
- 存在大量的条件判断和基础设施调用。
-
抽取业务逻辑形成领域模型:
- 通过将业务逻辑从 Service 方法中抽取出来,形成领域模型,使得业务逻辑更为清晰。
- Service 方法变得更加简洁,专注于领域模型的调用和协调。
-
领域模型执行业务逻辑:
- 领域模型内部封装了具体的业务规则和逻辑,Service 方法不再直接操作业务逻辑,而是调用领域模型的方法。
- 模型自身去执行业务逻辑,Service 方法不再关心具体业务规则。
-
简化 Service 方法:
- Service 方法变得更为简洁,只需要调用领域模型的方法,不再包含复杂的条件判断和详细的业务逻辑。
- Service 方法主要负责协调领域模型的调用、协同基础设施完成其余操作。
-
支持单元测试:
- 由于业务逻辑被抽取到领域模型中,单元测试变得更为容易,可以专注于测试领域模型的方法,而无需关心 Service 方法中的复杂逻辑和基础设施调用。
第三步、维护领域对象生命周期
-
领域对象的生命周期:
- 在领域驱动设计(DDD)中,领域对象的生命周期管理是重要的概念。领域对象的生命周期通常包括创建、修改、删除等阶段,需要通过一些手段来保证领域对象在不同阶段的一致性和有效性。
- 为了维护领域对象的生命周期,通常引入了 Repository(领域仓储)的概念。Repository 负责加载、保存领域对象,并将领域对象的生命周期管理起来。
-
DomainRepository 接口的定义:
DomainRepository是一个领域仓储的接口,定义了保存和加载聚合根(Aggregate Root)的方法。聚合根是领域模型中负责维护一致性边界的根实体,通常是整个聚合的入口。save方法用于保存聚合根,load方法用于加载聚合根。
-
Repository 的实现:
- 为了实现
DomainRepository接口,需要在 DAO 层引入 Domain 包,并在 DAO 层提供DomainRepository的实现。 - 在 Repository 实现中,可以通过调用底层的 Mapper 查询出数据模型,并将其封装为领域模型,然后返回给上层的 Service。
- 为了实现
-
DAO 层的调整:
- 由于领域模型的引入,DAO 层不再向 Service 返回数据模型,而是返回领域模型。
- DAO 层也被更名为 Repository,强调其职责是与底层数据存储打交道,并提供领域模型的访问接口。
-
隐藏数据库交互细节:
- 将 Repository 的实现移到 DAO 层,有助于隐藏底层数据库交互的细节,使得 Service 层不需要直接操作数据模型。
- Service 层专注于领域模型的调用和业务逻辑,而不必关心数据模型和数据库的具体实现。
public interface DomainRepository {
void save(AggregateRoot root);
AggregateRoot load(EntityId id);
}
第四步、泛化抽象
-
Infrastructure 层的细化:
- Repository 层被更名为
infrastructure-persistence,即基础设施层持久化包。这一层负责领域模型的加载和持久化,与数据库等底层设施打交道。 - 考虑到可能引入其他基础设施,比如缓存,所以将其命名为
infrastructure-XXX的格式,便于区分不同的基础设施支持。
- Repository 层被更名为
-
User Interface 层的细化:
- Controller 层被更名为
user-interface-web,即用户接口层的 Web 包。这一层负责与用户进行交互,包括 RESTful 接口和一系列 Web 相关的拦截器。 - 考虑到可能有不同协议的用户接口,引入
user-interface-provider包,用于存放对外提供的 RPC 服务的实现类。 - 引入
user-interface-subscriber包,用于存放消费 Kafka 消息并调用 Service 层执行业务逻辑的部分。
- Controller 层被更名为
-
Application 层的引入:
- Service 层被更名为
Application层或者Application Service层。这一层主要负责提供应用服务,协调领域模型和基础设施层完成业务逻辑。 - 通过这种命名,强调了 Service 层不再包含业务逻辑,而是提供应用服务的调度和协同。
- Service 层被更名为
-
防腐层的引入:
- 引入了防腐层的概念,通过
infrastructure-gateway包来封装对外部系统或资源的访问,以防止外部模型污染本地上下文的领域模型。
- 引入了防腐层的概念,通过
第五步、完整的项目结构
-
包的整理:
- 项目结构经过整理,包括
infrastructure-persistence、user-interface-web、user-interface-provider、user-interface-subscriber、infrastructure-gateway、Application等包,使得不同层次的代码能够清晰地组织在一起。 - 引入了
infrastructure-XXX的格式,便于区分不同的基础设施支持。
- 项目结构经过整理,包括
-
启动包的考虑:
- 考虑到有多个 User Interface,不适合将启动类放在其中。由于 Application 层也不合适,因此决定将启动类放在单独的模块中。
- 为了明确启动模块的用途,命名为
launcher模块,这个模块负责提供启动类和项目运行所需的配置文件。
-
多个 Launcher 的支持:
- 引入了
launcher模块,允许项目存在多个启动类,可以按需引用不同的 User Interface。这样的设计使得项目更加灵活,可以根据需求选择不同的启动方式。
- 引入了
-
项目的模块化:
- 通过引入不同的模块,将不同层次的代码进行模块化,使得每个模块专注于特定的职责。这样的模块化设计有助于提高代码的可维护性和可扩展性。