chapter 1 什么是不好的架构?
好的架构是什么样的呢?保留选择的余地,并且能很快的适应变化的需求和额外的因素。
促进ddd
ddd
就是创建模型,让交互和管理业务逻辑变的简单化。我们创建 domain
的时候会对它的行为建模,虽然状态很重要,但是行为会改变状态,驱动业务逻辑的运转。结合ORM
框架和前端框架,很容易将业务规则跟持久层混合起来。
容易走捷径
在传统的分层架构中,唯一的全局规则是,从某一层,我们只能访问同一层或同一层中的组件。所以,如果我们需要访问在我们上面的层中的某个组件,我们只需要将该组件下推一层,我们就可以访问它了。由于我们可以访问持久化层中的所有内容,随着时间的推移,它会变得越来越臃肿。
作为开发者,我们会轻易走这种捷径,这是一种被称为“破窗理论”的心理效应。
当我们把组件往下推时,持久层(或者用更通用的术语来说,最底层)会变臃肿。最理想的选择是助手或实用程序组件,因为它们似乎不属于任何特定的层。
所以,如果我们想要禁用我们架构的“快捷模式”,分层并不是最好的选择,至少在没有强制执行某种额外的架构规则的情况下是这样。
很难测试
随着分层的进化,如果web
直接接触persistence
层,就会在web
层添加很多的领域逻辑,测试不仅仅要模拟领域层,还要模拟持久层,增加单元测试的复杂性。
隐藏用例
因为我们作为程序员,需要快速添加和更改代码的正确位置,我们的体系结构应该帮助我们快速浏览代码库。
分层结构中,领域逻辑很容易分散到各个层中。这样会让我们很难找到想要处理的用例,也不方便测试。
并发变的困难
因为需要开发不同的架构层级,团队规模增大的时候,需要并行操作。领域逻辑和持久层如果耦合严重,会影响并行开发,增加代码冲突的概率。
如何帮助我们创建可维护的软件
分层架构易于维护和更改,但是没有严格的自律,随着时间的推移会变得不可维护。了解分层架构的弊端,有助于我们反对走捷径,转而构建更易于维护的解决方案。
chapter 2 依赖反转
单一职责原则
一个组件应该只有一个被更改的理由。避免耦合越来越多的理由,让domain
的行为变的复杂,导致一个行为的更改会波及另外的行为。
一个关于副作用的故事
一个老板不想为更改核心逻辑,怕出问题,愿意多花钱的故事。
依赖倒置原则
程序要依赖接口,而不是实现。 在domain
层添加repository
接口,让领域逻辑从对持久层代码的依赖中解放出来。
干净的架构
在干净的架构中,所有的依赖指向了核心领域逻辑。
六角形架构
端口适配器架构
如何帮助我们创建可维护的软件
不管是干净架构,还是端口适配器架构,我们都通过接口将web
层和persistence
层跟领域逻辑解耦。更加方便的修改代码。
领域代码可以自由建模为最适合解决业务问题的模型,持久层和UI
代码可以建模为最适合解决持久性和UI
问题的代码。
chapter 3 组织代码
按层组织
组织代码的一种方法是分层。
按特性组织
按特性组织代码的层级结构一般不是很明显。需要更加明确的包结构来表达架构。
架构富有表现力的包
包含实体,用例,传入传出端口,传入传出适配器。
端口必须是公共的,因为适配器需要访问他们。领域模型必须是公共的,方便服务和适配器访问。service
服务不需要是公共的,因为他们可以隐藏在传入传出端口后面。
有表达性的包结构,可以减少代码和架构之间的差距。
依赖注入
如何帮助我们创建可维护的软件
六边形架构的包结构,尽可能让我们的代码接近 ddd
。有助于后期的沟通,开发和维护。
chapter 4 实现用例
实现领域模型
一个简单的用例
- 输入
- 验证业务规则
- 修改模型状态
- 返回输出
校验输入
构造函数的力量
每个用例的专用输入模型使用例更加清晰,还可以与其他用例解耦。例如创建和更新,我们采用不同的模型来区分不同的行为,而不是通过传null
字段来人为过滤,这样就会有很多的字段有不同的可能性,容易让人混淆。
不同用例对应的不同输入模型
校验业务逻辑规则
充血和贫血模型
在贫血模型中,实体本事非常单薄,通常只有 getter setter,不包含任何领域逻辑。领域逻辑在用例中实现的,负责验证业务规则,更改实体状态,并将其传递到负责将其存储在数据库中的出站端口。“丰富度“包含在用例中,而不是实体中。
不同用例对应的不同的输出模型
遵循单一职责原则,避免修改用例需要输出模型的一个新字段,其他用例也需要处理这个字段。
什么是只读用例
查询职责隔离(CQRS)
如何帮助我们创建可维护的软件
我们独立的对用例创建输入输出模型,就可以避免不必要的维护工作。同时促进来程序员的并发协同工作。
chapter 5 实现 web 适配器
依赖反转
web
适配器接收外部的请求,并将其转换为对我们应用程序核心的调用,告诉它该做什么。控制流从web
适配器中的控制权传递到应用层中的服务。应用层提供了特定的端口,web
适配器可以通过这些端口进行通信。
web adapter 的职责
- 将 http 请求映射到 Java 对象
- 执行授权检查
- 验证输入
- 将输入映射到用例的输入模型
- 调用用例
- 将用例的输出映射会 http
- 返回一个 http 响应
切片控制器
尽可能多的创建控制器,确保每个控制器实现了一个尽可能窄的 web
适配器切片,并且尽可能少的与其他控制器共享。
如何帮助我们创建可维护的软件
- web 适配器转换为应用程序的调用,并将结果转会 http,不做任何逻辑处理
- 应用层不应该做 http , 一方面不会泄漏 http 细节,另一方面,可以用其他的 web 适配器替换。
- 可以构建细粒度的控制器,更加容易掌握和测试,支持并行,减少维护工作。
chapter 6 实现持久层适配器
依赖反转
端口是应用程序和持久性代码的中间层。领域逻辑不需要对持久层代码依赖,重构持久层代码也并不一定会导致核心代码的变化。
持久层的职责
- 需要输入
- 将输入映射成数据库格式
- 向数据库输入
- 将数据库输出映射为应用程序格式
- 返回输出
分割端口接口
接口隔离原则(ISP):拆分非常庞大臃肿的接口成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。
切片持久层适配器
spring data jpa 例子
数据库事务
如何帮助我们创建可维护的软件
通过实现端口的方式,我们可以采用不同的持久层技术,而不影响到应用层,只需要遵循约定的接口即可。
chapter 7 测试架构元素
测试金字塔
构建成本低,易于维护,快速运行且稳定的测试是单元测试。单元测试是金字塔的基础,通常是实例化一个类,并通过它的接口测试它的功能,如果有其他测试类,采用mock的方式,模拟实际类的行为。比如我们常用的 对entity
的测试,首先构建一个对象,然后测试它的各种内部行为。充血模型才会拥有比较丰富的内部行为。
集成测试构成了金字塔的下一层。集成测试一般跨越了层级。比如我们对Repository
层的测试,通过注入的方式 ,注入具体的实现是 JPA
还是 mybatis
,像接口传送一些数据,并获取验证结果。
系统测试启动构成了我们应用程序的整个对象网络,通过应用程序所有层验证某个用例是否按预期工作。感觉这个执行起来比较复杂,涉及多个层就会涉及多个层级的依赖。
测试 domain 采用单元测试
测试 use case 采用单元测试
测试 web adapter 采用集成测试
@WebMvcTest
测试 持久层 adapter 采用集成测试
@DataJpaTest
测试 主流程 采用系统测试
@SpringBootTest
上面的这些测试在项目中都有体现,丰富的测试对于项目安全是护城河。
有多少测试是足够呢
每次产生一个生产 bug, 为什么我们的测试没有覆盖到,那么我们将添加一个测试去覆盖它。当测试在功能开发期间完成而不是之后完成时,它们就会成为一种开发工具,不再感觉像一件苦差事。
如何帮助我们创建可维护的软件
用单元测试覆盖 domain 和 user case,用集成测试覆盖 web 和 persistence,输入输出我们可以采用 mock 的方式,也可以决定是模拟的还是真实实现的。
如果我们不知道采用什么样的测试来覆盖代码,说明我们的架构不够明确,存在缺陷。
chapter 8 如何在层之间映射
不需要映射
模型我们不在它们中间映射,如果web
需要添加一些额外的字段,会违背单一职责原则。如果仅仅针对 CRUD ,可以采用不映射。
双向映射策略
每个适配器都有自己的模型,适配器负责将它们的模型映射到领域模型。因为每一层都有自己的模型,那么久可以在不影响其他层的情况下修改自己的模型。
优点
就是修改灵活,不影响其他的层次,缺点
就是会产生大量的样板代码,还有就是领域模型用来跨层边界通信,会容易因为外界的需求而更改。
全映射
每个操作, 通过 command 进行映射。
单向映射策略
实现共同的接口,统一很困难,尤其在多个层级之间。
什么时候使用那种映射策略
单一不能满足需求。我们团队改造了多次,之前我们团队采用的 rest
层映射 data
,service
和 repository
采用entity
映射,后来经过改造后,改成了 service
和 repository
采用entity
映射,graphql
层进行返回结果的组合,重复的字段采取mapStruct
映射,避免重复字段的方式。
如何帮助我们创建可维护的软件
根据不同的情况采用适合团队的策略🐶
chapter 9 组装应用程序
为啥要关心组装
保证部件成为可工作的应用程序。
- 创建 web 适配器,确保 http 请求被路由映射到 web 适配器
- 创建带有用例实例的 web 适配器
- 创建持久性适配器实例,确保持久性适配器可以访问数据库
通过普通代码组装
通过spring 的路径扫描 组装
@Component
通过spring 的 java config 组装
@Configuration
如何帮助我们创建可维护的软件
Spring 和 Spring Boot 将我们提供的组件组装成应用程序。我们获得了高度内聚的模块,我们可以在彼此隔离的情况下启动。
chapter 10 加强架构间的界限
边界和依赖
虚线指的不能依赖的方向,实现指向向内依赖。
可见性修饰符
public,protected,private,package-private,default
编译后检查
构建
如何帮助我们创建可维护的软件
关于管理架构元素之间的依赖关系。
- 生成新代码或重构现有代码,牢记包结构,避免依赖包外部访问的类。
- 可以采用编译工具校验,ArchUnit。
- 体系结构足够稳定,将体系结构元素提取到自己的构建模块中,显示控制依赖关系。
chapter 11 有意识的走捷径
走捷径可能会积累一堆我们无力偿还的技术债。
捷径就像破窗
在低质量的代码库上工作,添加更多低质量代码的门槛很低。
在有很多违规代码库上工作,添加另一个编码违规的门槛很低。
在走捷径的代码库上工作,其他人也走捷径的门槛很低。
在少测试的代码库上工作,其他人不写测试的门槛也很低。
保持用例之间的模型干净是一种责任
使用领域模型作为输入输出的模型
跳过传入的端口
跳过应用程序
如何帮助我们创建可维护的软件
在用例脱离 CRUD 的时候,团队能从长远来看更易于维护的体系结构来取代快捷方式。
chapter 12 决定架构的风格
domain 是 king
作为是否使用本书所呈现的架构风格的第一个指标,如果领域代码不是你的应用程序中最重要的东西,你可能不需要这种架构风格。
在不受外部影响的情况下发展领域代码,是六边形架构风格的唯一最重要的论据。
这就是为什么这种架构风格非常适合领域驱动设计(DDD)实践。显而易见的是,在DDD中,领域驱动开发,如果我们不需要同时考虑持久性问题和其他技术方面,我们就可以最好地对领域进行推进。
我甚至会说,以领域为中心的架构风格,比如六边形风格,是DDD的推动者。如果没有一个将领域置于事物中心的架构,如果没有将依赖关系倒置到领域代码上,我们就没有机会真正做DDD;设计总是会受到其他因素的驱动。
经验 是 queen
我们是习惯的生物。习惯会自动帮我们做决定,这样我们就不用花时间在这些决定上了。如果有一头狮子向我们奔来,我们就跑。如果我们构建一个新的web应用程序,我们使用分层架构风格。我们过去经常这样做,以至于它已经成为一种习惯。
我并不是说这一定是一个糟糕的决定。习惯既能帮助我们做出正确的决定,也能帮助我们做出错误的决定。我的意思是,我们正在做我们所经历过的事情。我们对过去所做的事情感到满意,所以我们为什么要改变什么呢?
所以,要对一种建筑风格做出有根据的决定,唯一的方法就是拥有不同建筑风格的经验。如果你对六边形架构风格不确定,那就在你正在构建的应用程序的一个小模块上尝试一下。先习惯这些概念,然后慢慢适应。应用这本书中的观点,修改它们,并加入你自己的想法,以发展一种你感到舒适的风格。
这些经验可以指导你的下一个架构决策。
这取决于
选择那种代码风格取决于要构建的软件类型,取决于领域代码的作用,取决于团队的经验,取决于是否可以坦然的做决定。
ps:
架构设计也遵循设计模式的部分原则,如单一职责等,下面是设计模式的原则:
名称 | 英文名称和缩写 | 含义 |
---|---|---|
开闭原则 | Open Closed Principle,OCP | 软件中的对象,对扩展开放,对修改关闭 |
单一职责原则 | Single Responsibility Principle, SRP | 一个类应该只有一个发生变化的原因 |
里氏代换原则 | Liskov Substitution Principle,LSP | 任何父类出现的地方,子类可以替代其功能 |
依赖倒转原则 | Dependency Inversion Principle,DIP | 程序依赖抽象接口,不依赖具体实现 |
接口隔离原则 | Interface Segregation Principle,ISP | 类之间的依赖关系应该建立在最小的接口上 |
合成/聚合复用原则 | Composite/Aggregate Reuse Principle,CARP | 尽量使用合成/聚合,而不是通过继承达到复用的目的 |
迪米特法则 | Law of Demeter,LOD | 一个类对其他的类知道的越少越好 |