《Get your hands dirty on clean architecture》读书笔记

本文探讨了不好的架构特点,推崇六角形架构和依赖反转原则,强调分层架构的问题和测试的重要性。文章介绍了如何通过组织代码、实现用例和适配器,以及遵循设计模式来创建可维护的软件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

chapter 1 什么是不好的架构?

好的架构是什么样的呢?保留选择的余地,并且能很快的适应变化的需求和额外的因素。

促进ddd
ddd 就是创建模型,让交互和管理业务逻辑变的简单化。我们创建 domain 的时候会对它的行为建模,虽然状态很重要,但是行为会改变状态,驱动业务逻辑的运转。结合ORM框架和前端框架,很容易将业务规则跟持久层混合起来。

容易走捷径
在传统的分层架构中,唯一的全局规则是,从某一层,我们只能访问同一层或同一层中的组件。所以,如果我们需要访问在我们上面的层中的某个组件,我们只需要将该组件下推一层,我们就可以访问它了。由于我们可以访问持久化层中的所有内容,随着时间的推移,它会变得越来越臃肿。

作为开发者,我们会轻易走这种捷径,这是一种被称为“破窗理论”的心理效应。

当我们把组件往下推时,持久层(或者用更通用的术语来说,最底层)会变臃肿。最理想的选择是助手或实用程序组件,因为它们似乎不属于任何特定的层。

所以,如果我们想要禁用我们架构的“快捷模式”,分层并不是最好的选择,至少在没有强制执行某种额外的架构规则的情况下是这样。

很难测试
随着分层的进化,如果web直接接触persistence层,就会在web层添加很多的领域逻辑,测试不仅仅要模拟领域层,还要模拟持久层,增加单元测试的复杂性。

隐藏用例
因为我们作为程序员,需要快速添加和更改代码的正确位置,我们的体系结构应该帮助我们快速浏览代码库。
分层结构中,领域逻辑很容易分散到各个层中。这样会让我们很难找到想要处理的用例,也不方便测试。

并发变的困难
因为需要开发不同的架构层级,团队规模增大的时候,需要并行操作。领域逻辑和持久层如果耦合严重,会影响并行开发,增加代码冲突的概率。

如何帮助我们创建可维护的软件
分层架构易于维护和更改,但是没有严格的自律,随着时间的推移会变得不可维护。了解分层架构的弊端,有助于我们反对走捷径,转而构建更易于维护的解决方案。

chapter 2 依赖反转

单一职责原则
一个组件应该只有一个被更改的理由。避免耦合越来越多的理由,让domain的行为变的复杂,导致一个行为的更改会波及另外的行为。

一个关于副作用的故事
一个老板不想为更改核心逻辑,怕出问题,愿意多花钱的故事。

依赖倒置原则
程序要依赖接口,而不是实现。 在domain层添加repository 接口,让领域逻辑从对持久层代码的依赖中解放出来。

干净的架构
在这里插入图片描述
在干净的架构中,所有的依赖指向了核心领域逻辑。

六角形架构
在这里插入图片描述
端口适配器架构

如何帮助我们创建可维护的软件
不管是干净架构,还是端口适配器架构,我们都通过接口将web层和persistence层跟领域逻辑解耦。更加方便的修改代码。
领域代码可以自由建模为最适合解决业务问题的模型,持久层和UI代码可以建模为最适合解决持久性和UI问题的代码。

chapter 3 组织代码

按层组织
组织代码的一种方法是分层。
在这里插入图片描述

按特性组织
按特性组织代码的层级结构一般不是很明显。需要更加明确的包结构来表达架构。

架构富有表现力的包
在这里插入图片描述
包含实体,用例,传入传出端口,传入传出适配器。
端口必须是公共的,因为适配器需要访问他们。领域模型必须是公共的,方便服务和适配器访问。service服务不需要是公共的,因为他们可以隐藏在传入传出端口后面。
有表达性的包结构,可以减少代码和架构之间的差距。

依赖注入
在这里插入图片描述

如何帮助我们创建可维护的软件
六边形架构的包结构,尽可能让我们的代码接近 ddd。有助于后期的沟通,开发和维护。

chapter 4 实现用例

实现领域模型

一个简单的用例

  1. 输入
  2. 验证业务规则
  3. 修改模型状态
  4. 返回输出
    在这里插入图片描述

校验输入

构造函数的力量
每个用例的专用输入模型使用例更加清晰,还可以与其他用例解耦。例如创建和更新,我们采用不同的模型来区分不同的行为,而不是通过传null字段来人为过滤,这样就会有很多的字段有不同的可能性,容易让人混淆。

不同用例对应的不同输入模型

校验业务逻辑规则

充血和贫血模型
在贫血模型中,实体本事非常单薄,通常只有 getter setter,不包含任何领域逻辑。领域逻辑在用例中实现的,负责验证业务规则,更改实体状态,并将其传递到负责将其存储在数据库中的出站端口。“丰富度“包含在用例中,而不是实体中。

不同用例对应的不同的输出模型
遵循单一职责原则,避免修改用例需要输出模型的一个新字段,其他用例也需要处理这个字段。

什么是只读用例
查询职责隔离(CQRS)

如何帮助我们创建可维护的软件
我们独立的对用例创建输入输出模型,就可以避免不必要的维护工作。同时促进来程序员的并发协同工作。

chapter 5 实现 web 适配器

依赖反转
web适配器接收外部的请求,并将其转换为对我们应用程序核心的调用,告诉它该做什么。控制流从web适配器中的控制权传递到应用层中的服务。应用层提供了特定的端口,web适配器可以通过这些端口进行通信。

web adapter 的职责

  1. 将 http 请求映射到 Java 对象
  2. 执行授权检查
  3. 验证输入
  4. 将输入映射到用例的输入模型
  5. 调用用例
  6. 将用例的输出映射会 http
  7. 返回一个 http 响应

切片控制器
尽可能多的创建控制器,确保每个控制器实现了一个尽可能窄的 web适配器切片,并且尽可能少的与其他控制器共享。

如何帮助我们创建可维护的软件

  1. web 适配器转换为应用程序的调用,并将结果转会 http,不做任何逻辑处理
  2. 应用层不应该做 http , 一方面不会泄漏 http 细节,另一方面,可以用其他的 web 适配器替换。
  3. 可以构建细粒度的控制器,更加容易掌握和测试,支持并行,减少维护工作。

chapter 6 实现持久层适配器

依赖反转
端口是应用程序和持久性代码的中间层。领域逻辑不需要对持久层代码依赖,重构持久层代码也并不一定会导致核心代码的变化。

持久层的职责

  1. 需要输入
  2. 将输入映射成数据库格式
  3. 向数据库输入
  4. 将数据库输出映射为应用程序格式
  5. 返回输出

分割端口接口
接口隔离原则(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 层映射 dataservice repository 采用entity 映射,后来经过改造后,改成了 servicerepository采用entity映射,graphql层进行返回结果的组合,重复的字段采取mapStruct映射,避免重复字段的方式。

如何帮助我们创建可维护的软件
根据不同的情况采用适合团队的策略🐶

chapter 9 组装应用程序

为啥要关心组装
保证部件成为可工作的应用程序。

  1. 创建 web 适配器,确保 http 请求被路由映射到 web 适配器
  2. 创建带有用例实例的 web 适配器
  3. 创建持久性适配器实例,确保持久性适配器可以访问数据库

通过普通代码组装
通过spring 的路径扫描 组装
@Component
通过spring 的 java config 组装
@Configuration
如何帮助我们创建可维护的软件
Spring 和 Spring Boot 将我们提供的组件组装成应用程序。我们获得了高度内聚的模块,我们可以在彼此隔离的情况下启动。

chapter 10 加强架构间的界限

边界和依赖
在这里插入图片描述
虚线指的不能依赖的方向,实现指向向内依赖。

可见性修饰符
public,protected,private,package-private,default
编译后检查
构建
如何帮助我们创建可维护的软件
关于管理架构元素之间的依赖关系。

  1. 生成新代码或重构现有代码,牢记包结构,避免依赖包外部访问的类。
  2. 可以采用编译工具校验,ArchUnit。
  3. 体系结构足够稳定,将体系结构元素提取到自己的构建模块中,显示控制依赖关系。

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一个类对其他的类知道的越少越好
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值