0%

《整洁架构之道》笔记

《架构整洁之道》笔记

  • 有多少次,我看到的系统设计图里,根本没有“层次”的概念,各个模块没有一致的层次划分,与子系统交互的不是子系统,而是一盘散沙式的接口,甚至接口之间随意互调、关系乱成一团麻的情况也时常出现,带来的就是维护和调试的噩梦。
  • 了解历史已经够难了,我们对现实的认识也不够可靠,语言未来就更难了。

所以嘛,能把当前的代码写清楚就很棒了。

软件开发理论的两个观点:

  • 只有权威和刚性才能带来强壮与稳定。如果某项变更成本高昂,那么就应该忽视它-变更背后的需求要么应该被抑制,要么就应该被丢到官僚主义的大机器中去较碎。

  • 另外一条路线则是到处充斥着大量的投机性的通用设计。

  • 软件架构的规则是相同的!

  • 最终产生的代码仍然只是顺序结构、分支结构、循环结构的组合。

  • 对系统的开发者来说,这会带来很大的挫败感,因为团队中并没有人偷懒,每个人还都是和之前一样的在拼命工作。

  • 然而,不管他们投入了多少个人实践,救了多少次火,加了多少次班,他们的产出始终上不去。工程师的大部分时间都消耗在对现有系统的修修补补上,而不是真正完成实际的新功能。这些工程师真正的任务是:拆了东墙补西墙,周而往复,偶尔有精力能顺便实现一点小功能。

  • 做事的优先级

    1. 重要且紧急
    2. 重要不紧急
    3. 不重要但紧急
    4. 不重要且紧急

有抗争才是健康的

研发团队必须从公司长远利益出发与其他部门抗争

编程范式(paradigm)

三种编程范式

  • 结构化编程(structured programming)
  • 面向对象编程 (object-oriented programming)
  • 函数式编程 (functional programming)

结构化编程对程序控制权的直接转移进行了限制和规范。
面对对象编程对程序控制权的间接转移进行了限制和规范。
函数式编程对程序中的赋值进行了限制和规范。

结构化编程

  • 如果没有工具的帮助,这些细节的信息是远远超过一个程序员的认识能力范围的。

  • 顺序结构、分支结构、循环结构这三种结构可以构造出任何程序。

  • 科学和数学在证明方法上有着根本性的不同,科学理论和科学定律通常是无法被证明的。

  • 科学理论和科学定律的特点:他们可以被证伪,但是没有办法被证明

  • 测试只能展示Bug的存在,并不能证明不存在Bug

  • 结构化编程范式,它赋予了我们创造可证伪程序单元的能力,所以”功能性降解拆分仍然是最佳时间之一“

面对对象编程

这意味着我们让用户界面和数据库都成为了业务逻辑的插件。

业务逻辑组件具有了

  • 独立部署能力
  • 独立开发能力

面对对象编程是什么?

通过多态对源代码中的依赖关系进行控制,实现某种插件式的架构。插件可以独立的开发和部署。

函数式编程

一个架构设计良好的应用程序应该将状态修改的部分和不需要修改的状态部门隔离成单独的组件,然后用合适的机制来保护壳变量。

软件架构师应该着力于将大部分处理逻辑都归于不可变组件中,可变状态组件的逻辑应该越少越好。

单一职责

多人为了不同的目的修改了同一份源代码,这很容易造成问题的产生。

开闭原则

一个良好的计算机系统应该在不需要修改的前提下就可以轻易被扩展。

实现方式是通过将系统划分为一系列组件,并且将这些组件间的依赖关系按层次结构进行组织,使得高阶组件不会因低阶组件被修改而受到影响。

里氏替换原则

审核代码的时候可以把这个原则用来当作判断代码好坏的标准(实现是否可替换)

接口隔离原则


不必要的依赖关系,会导致不必要的重新编译和重新部署。

如何层次的软件设计如果依赖了它不需要的东西,就会带来意料意外的麻烦。

依赖反转原则

如果想要设计一个灵活的系统,在源代码层次的依赖关系中就应该多引用抽象类型,而非具体实现。

关注的是软件系统内部那些经常变动的具体实现模块,这些模块是不停开发的,也就是会经常出现变更。

继承关系是一切源代码依赖关系中最强的、最难被修改的,所以我们对继承的使用应该格外小心。

不要覆盖包含具体实现的函数。调用包含具体实现的函数通常就意味着引入了源代码级别的依赖。

如果想不依赖具体实现类,那么应该如何获取具体实现类呢?
1. 依赖注入
2. Service Locator/ Service Factory(抽象工厂模式)
依赖反转是什么意思?

控制流跨越架构边界的方向和源代码依赖关系跨越边界的方向正好相反,源代码依赖方向永远是控制流方向的反转

绝大部分系统中都至少存在一个具体实现组件

* 具体实现组件需要与系统中的其他部分隔离区分开
* 在具体实现组件中创建ServiceFactoryImpl实例

下图比较清晰的解释了什么是依赖反转

组件构建原则

组件是软件的部署单元,是软件系统在部署过程中可以独立完成部署的最小实体。(例如java中的jar)

组件构建原则就是用来判断哪些类应该被组合成一个组件

REP:复用/发布等同原则

  • 能复用,能发布
  • 组件中的类和模块应该是可以同时发布的,共享相同的版本号与版本追踪

CCP: 共同闭包原则

我们应该将那些会同时修改,并且为相同目的而修改的类放在同一组件中。

对于大部分应用程序说,可维护性的重要性要远远高于可复用性。

CRP: 共同复用原则

不要依赖不需要用到的东西。

三大原则之间的关系

组件的划分看来还是没有明确的对错准则,看情况确定

组件耦合

影响组件结构的不仅有技术水平和公司内部政治斗争这两个因素,其结构本身更是不断变化的。

无依赖环原则

组件依赖关系图中不应该出现环。

组件中的两种依赖关系
* 有向无环(健康的)
* 循环依赖(不健康的)

单向依赖,当组件发生变@化时,只需要关注一个方向的组件是否兼容即可。
循环依赖,当组件发生变化时,两个方向的依赖组件都需要小心处理维护。

通过依赖反转,创建一个Permissions组件来打破循环依赖

组件结构图是不可能自上而下被设计出来的。

组件结构图和功能单元不是相对应的。

组件结构图的作用?
组件结构图中的一个重要目标是指导如何隔离频繁的变更。我们不希望那些频繁变更的组件影响到其他本来应该很稳定的组件。

依赖关系必须要指向更稳定的方向。

我们可以创造出对某些变更敏感,对其他变更不敏感的组件。

组件的稳定性是什么?

如果一个组件想要成为稳定组件,那么它就应该由接口和抽象组成。

什么是软件架构

如果不亲身承受因系统设计而带来的麻烦,就必会不到设计不佳所带来的痛苦,接着就会逐渐迷失正确的设计方向。

基本上,所有的软件系统都可以降解为策略与细节这两种主要元素。策略体现的是软件中所有的业务规则与操作过程,因此它是系统真正的价值所在。

细节包括:
* I/O设备
* 数据库
* Web系统
* 服务器
* 框架
* 交互协议

软件架构师的目标是创建一种系统形态,该形态会以策略为最基础的元素,并让细节与策略脱离关系,以允许再具体决策过程中推迟延迟与细节相关的内容。

越到项目的后期,我们就拥有越多的信息来做出合理的决策。

优秀的架构师:

  1. 优秀的架构师会小心地将软件的高层策略与底层实现隔离开,让高层策略与实现细节脱钩,使其策略部分完全不需要关心底层细节,当然也不会对这些细节有任何形式的依赖。
  2. 优秀的架构师所设计的策略应该允许系统尽可能地推迟与实现细节相关的决策,越晚做决策越好。

独立性

用例(use case):用例一般是由软件开发者和最终用户共同创作的。

架构师的目标是让系统结构支持其所需要的所有用例。

系统可以拆分为
1. UI界面
2. 应用独有的业务逻辑
3. 领域普适的业务逻辑
4. 数据库

这种居于服务来构建的架构,架构师们通常称之为面向服务的架构(service-oriented architectrue)。

重复的情况中也有一些是假的,或者说这种重复只是表面性的。如果有两段看起来重复的代码,他们走的是不同的演进路径,也就是说它们有着不同的变更速率和变更缘由,那么这两段代码就不是真正的重复。

我们要消除的是真正意义上的重复
解耦系统的方式:
1. 源码层次
2. 部署层次
3. 服务层次

其中源码层次解耦是基础,根据实际场景选择采用哪一种

业务逻辑

应用程序划分为业务逻辑和插件

关键业务逻辑和关键业务数据是紧密相关的,所以它们很适合放在同一个对象中处理。我们将这种对象称为业务实体(Entity)

业务实体包含一系列用于操作关键数据的业务逻辑。

业务实体这个概念只是要求我们将关键业务数据和关键业务逻辑绑定在一个独立的软件模块内。

业务逻辑应该是系统中最独立,复用性最高的代码。

尖叫的软件架构

如果我们要构建的是一个医疗系统,新来的程序员第一次看到源码时就应该知道这是一个医疗系统。新来的程序员应该先了解该系统的用例

最先关注的是系统能提供什么样的服务?而不是如何实现这样的服务

整洁架构

架构的特点:

  • 独立于框架
  • 可被测试
  • 独立于UI
  • 独立于数据库
  • 独立于任何外部机构

用例

  1. 一方面用例的设计,不应该受外部因素的影响(数据库,UI等..),用例因该与它们保持隔离
  2. 另一方面应用行为等变化又会影响到用例本身

Main组件

在所有系统中,都至少有一个组件来负责创建、协调、监督其他组件的运转。我们将其称为Main组件。

服务:宏观与微观

架构设计的任务就是找到高层策略与低层细节之间的架构边界,同时保证这些边界遵守依赖关系规则。

所谓的服务本身只是一种比函数调用方式成本稍高的,分割应用程序行为的一种形式,与系统架构无关。

任何形式的共享数据行为都会强耦合

测试边界

修改一个通用的系统组件可能会导致成百上千个测试出现问题

整洁的嵌入式架构

有一个LED被连接到一个GPIO比特位上。
固件函数:Led_Turnon(5)
HAL函数:Indicate_LowBattery()

不要向HAL的用户暴露硬件细节

数据库只是实现细节

很多数据访问框架允许将数据行和数据表以对象的形式在系统内部传递。这么做在系统架构上来说是完全错误的,这会导致程序的用例、业务逻辑、甚至UI与数据的关系模型互相绑定在一起。

所以不喜欢在界面上直接使用Android Room返回的LiveData,让界面直接和数据库耦合了
数据的存储方式
* 链表
* 树
* 哈希表
* 堆栈
* 队列

Web是实现细节

应用程序框架是实现细节

如果框架要求我们根据它们的基类来创建派生类,就请不要这样做!

不要框架污染我们的核心代码。

以Sprint为例,千万别在业务对象里到处写@autowired注解。业务对象应该对Spring完全不知情才对。

我们可以利用Spring将依赖关系注入到Main组件中,毕竟Main组件作为系统架构中最底层、依赖最多的组件,它依赖于Spring并不是问题。

对koin等依赖注入框架不能滥用,只能在Main组件中使用

案例分析

有的抽象并不是必须的,因为如果没有这一层的抽象,整个产品并不会受到影响
  • 按层封装:三层架构
  • 按功能封装
  • 端口和适配器
  • 按组件封装:将业务逻辑与持久化代码合并在一起,称为组件,通过package权限防止外界对持久化代码的访问。