Go工程化(一) 架构整洁之道阅读笔记

时间:2022-06-17 02:58:38

 Go工程化(一) 架构整洁之道阅读笔记

本系列为极客时间 Go 进阶训练营笔记,同步直播更新,预计一周更新 1 ~ 2 篇文章,到 202103 月更新完成

其实这一篇文章不应该算在这里面,(PS: 毛老师课程上没讲这本书)但是恰好最近把这本书读完了,并且部门内推荐大家读这本书,毛老师在课上也推荐这本书,也和我们这次的主题有一些关系,一切都是最好的安排,那就放这系列吧。

阅读建议: 全文接近 2W 字,篇幅较长,采用书中重点摘录+不成熟的个人小结组成,桌面端可以点击右侧目录快速定位到你感兴趣的章节

读书笔记

 

前言

  • 今天的软件与过去的软件本质上仍然是一样的。都是由 if 语句、赋值语句以及 while 循环组成的
  • 软件架构的规则其实就是排列组合代码块的规则

这说明什么呢,说明了可能我们以为过时的,古老的技术或者解决方案也是有用的

第一部分 概述

 

第 1 章 设计与架构究竟是什么

  • 架构图里实际上包含了所有的「底层设计细节」,这些细节信息共同支撑了顶层的架构设计,「底层设计信息和顶层架构设计」共同组成了整个房屋的架构文档。
  • 「软件架构的终极目标是,用最小的人力成本来满足构建和维护该系统的需求。」
  • 一个软件架构的优劣,「可以用它满足用户需求所需要的成本来衡量。」
  • 乱麻系统:这种系统一般都是没有经过设计,匆匆忙忙被构建起来的
  1. 我们经常使用一句话来欺骗自己**“我们可以未来再重构代码,产品上线最重要!”**
  2. 另外一个错误的观点:「“在工程中容忍糟糕的代码存在可以在短期内加快该工程上线的速度,未来这些代码会造成一些额外的工作量,但是并没有什么大不了”」
  • 研发团队最好的选择是清晰地认识并避开工程师们过度自信的特点,开始认真地对待自己的代码架构,对其质量负责

软件的架构的终极目标,以及如何衡量一个架构的优劣,尤其是两个错误的观点非常感同身受,我也说过类似的话语,还有一句话是“当前的需求非常紧急,这只是一个临时的系统很快就会被替换掉,我们先完成它”。作为一个专业的技术人员我们需要有一些底线来保证我们的代码架构和质量,不能轻易妥协,这在 Bob 大叔整洁系列的另外一本书中也有提到。

第 2 章 两个价值纬度

1.行为价值

只有可以产生收入的代码才是有用的代码,技术是需要为业务服务的,但是我们的工作并不是说就按照需求文档写代码,修bug就行了

  • 软件系统的行为是其最直观的价值维度。程序员的工作就是让机器按照某种指定方式运转,给系统的使用者创造或者提高利润。
  • 按照需求文档编写代码,并且修复任何 Bug。这真是大错特错。
  • 「系统行为,是紧急的,但是并不总是特别重要。」

2.架构价值

架构价值主要就是为了能够应对变化,其实举个反面例子,我们之前有一个系统 A 是直接在 A 中调用接口获取数据,随着业务的发展我们拆分了一个应用 B 需要从 B 中获取对应的数据,这个时候我们发现代码变更非常严重,从里到外都需要进行重构修改,这就是典型了依赖了“具体的形状”导致的额外成本

  • 为了达到软件的本来目的,软件系统必须够“软”——也就是说,软件应该容易被修改。
  • 当需求方改变需求的时候,随之所需的软件变更必须可以简单而方便地实现。
  • 变更实施的难度应该和变更的范畴(scope)成等比关系,而与变更的具体形状(shape)无关。
  • 「系统架构,是重要的,但是并不总是特别紧急。」

3.重要紧急的排序

  • 重要且紧急
  • 重要不紧急
  • 不重要但紧急
  • 不重要且不紧急

4.业务/市场的同事往往是无法评估架构的重要性的,所以,「平衡系统架构的重要性与功能的紧急程度这件事,是软件研发人员自己的职责。」

我们当前处在公共技术的部门,这也是一个经常困扰的一个例子,所有的业务方在提需求的时候都会表示需求非常紧急,但是这个功能的实现对我们来说重要吗?这个需要打上一个大大的问号,其他部门的同学其实是无法对评估需求对于我们的重要性的,这个需要我们自己来权衡。

5.为好的软件架构而持续斗争

这不仅仅是架构师的职责,这是每一位开发同学的职责,忽略架构的价值会导致我们带来无休止的加班,领导的质疑,产品的argue

  • 软件架构师这一职责本身就应更关注系统的整体结构,而不是具体的功能和系统行为的实现。
  • 「软件架构师必须创建出一个可以让功能实现起来更容易、修改起来更简单、扩展起来更轻松的软件架构。」
  • 如果忽视软件架构的价值,系统将会变得越来越难以维护,终会有一天,系统将会变得再也无法修改。

第二部分 从基础构件开始:编程范式

 

编程范式指的是程序的编写模式,与具体的编程语言关系相对较小。这些范式会告诉你应该在什么时候采用什么样的代码结构 当前的三种编程范式,结构化编程,面向对象,函数式编程

第 3 章 编程范式总览

1.结构化编程(面向过程)

  • 结构化编程对程序控制权的直接转移进行了限制和规范。
  • 限制了 goto 语句的使用

2.面向对象

  • 面向对象编程对程序控制权的间接转移进行了限制和规范。
  • 限制了函数指针的使用

3.函数式编程

这个角度之前还没有看到过,对我而言还是比较新奇,从限制的角度来看不同的编程范式有着不同限制,可以减少在编程当中出错的可能

  • 函数式编程对程序中的赋值进行了限制和规范。
  • 限制了赋值语句的使用

第 4 章 结构化编程

  • Bohm 和 Jocopini 刚刚证明了人们可以用顺序结构、分支结构、循环结构这三种结构构造出任何程序。
  • 证明了我们构建可推导模块所需要的控制结构集与构建所有程序所需的控制结构集的最小集是等同的。
  • 结构化编程范式可将模块递归降解拆分为可推导的单元,这就意味着模块也可以按功能进行降解拆分。
  • 测试只能展示 Bug 的存在,并不能证明不存在 Bug。

结构化编程可以让我们将一个大的模块按照功能进行拆分,变成小的功能模块,同时通过测试我们可以证明其错误性,无论是架构上还是实际的开发过程中,大模块拆小模块的思路的数不胜数,其实单体应用拆分为微服务应用也是这个范畴内的。

  • 换句话说,一段程序可以由一个测试来证明其错误性,但是却不能被证明是正确的。测试的作用是让我们得出某段程序已经足够实现当前目标这一结论。

第 5 章 面向对象编程

1.什么是面向对象?

面向对象理论是在 1966 年提出的,当时 Dahl 和 Nygaard 主要是将函数调用栈迁移到了堆区域中

  • 一种常见的回答是“数据与函数的组合”,这种不太贴切
  • 另一种常见的回答是“面向对象编程是一种对真实世界进行建模的方式”,这有点避重就轻
  • 面向对象编程是封装(encapsulation)、继承(inheritance)、多态(polymorphism)这三项的有机组合

2.封装

  • 通过采用封装特性,我们可以把一组相关联的数据和函数圈起来,使圈外面的代码只能看见部分函数,数据则完全不可见
  • C 语言也支持完整的封装特性,使用 C 语言的时候应用头文件 .h 的模块是无法知道结构体中的成员变量的,但是 C++ 的头文件中包含了成员信息。
  • 不是面向对象语言的 C 语言相对于面向对象语言 C++ 反而拥有更好的封装特性,所以「我们很难说强封装是面向对象编程的必要条件」

3.继承

  • 继承的主要作用是让我们可以在某个作用域内对外部定义的某一组变量与函数进行覆盖
  • C 其实也可以实现继承,只是相对面向对象语言而言会更加困难。

4.多态

  • 归根结底,多态其实不过就是函数指针的一种应用。但是函数指针非常危险,需要人为的遵守很多约定,容易出 bug。
  • 面向对象编程语言虽然在多态上并没有理论创新,但它们也确实让多态变得更安全、更便于使用了。

5.依赖反转

  • 依赖关系(或者叫继承关系)的方向和控制流正好是相反的,我们称之为依赖反转
  • 依赖关系都可以通过引入接口的方式来进行反转。
  • 通过这种方法,软件架构师可以完全控制采用了面向对象这种编程方式的系统中所有的源代码依赖关系,
  • 而不再受到系统控制流的限制。不管哪个模块调用或者被调用,软件架构师都可以随意更改源代码依赖关系。
  • 当某个组件的源代码需要修改时,仅仅需要重新部署该组件,不需要更改其他组件,这就是独立部署能力。

6**面向对象编程就是以多态为手段来对源代码中的依赖关系进行控制的能力,**这种能力让软件架构师可以构建出某种插件式架构,让高层策略性组件与底层实现性组件相分离,底层组件可以被编译成插件,实现独立于高层组件的开发和部署。

在刚学习编程的时候,学到面向对象一定会说到,封装、继承、和多态,但是通过这一章我们可以发现,面向对象语言的封装不一定比面向过程的 C 语言做的更好,这里强调的更重要的是使用多态的手段对源码的依赖关系进行控制,主要是指通过接口来实现依赖反转,这样就可以将组件进行分离,可以进行独立开发和部署。我现在主要使用的语言是 Go,有一个常见的问题就是 Go 是不是一个面向对象语言,回答也是 Yes or no,是也不是,Go 不支持继承,也不支持函数重载,运算符重载等在面向对象语言非常常见的特性,但是 Go 的接口非常强大,不需要显示依赖接口的设计让我们在依赖反转的使用上更加游刃有余。

第 6 章 函数式编程

  • 函数式编程语言中的变量(Variable)是不可变(Vary)的。
  • 为什么软件架构师要操心变量的可变性呢?答案显而易见:所有的竞争问题、死锁问题、并发更新问题都是由可变变量导致的。
  • 一个架构设计良好的应用程序应该将状态修改的部分和不需要修改状态的部分隔离成单独的组件,然后用合适的机制来保护可变量。
  • 事件溯源体系下,我们只存储事务记录,不存储具体状态。当需要具体状态时,我们只要从头开始计算所有的事务即可。
  • 这种数据存储模式中不存在删除和更新的情况,我们的应用程序不是 CRUD,而是 CR。因为更新和删除这两种操作都不存在了,自然也就不存在并发问题。

在我们刚刚结束的上一个系列,[Go并发编程](https://lailin.xyz/categories/Go%E8%BF%9B%E9%98%B6%E8%AE%AD%E7%BB%83%E8%90%A5/Go%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/)中,我们讲到的大量手段来避免数据竞争,这些都是由于在并发时写入导致的,而函数式编程最重要的一个特性就是变量不可变,由于变量无法被修改所以自然而然就不存在数据竞争,也就不需要加锁,这样可以获得很高的性能。

第三部分 设计原则

软件构建中层结构的主要目标:

  • 使软件可容忍被改动。
  • 使软件更容易被理解。
  • 构建可在多个软件系统中复用的组件。

在之前的[《Go设计模式》](https://lailin.xyz/post/go-design-pattern.html)系列文章当中也有提到 SOLID 原则,换个角度可以发现这些其实都是殊途同归的一些东西,SOLID 原则的历史已经非常悠久了,但是直到现在它仍然非常具有指导意义。

第 7 章 SRP:单一职责原则

1.「任何一个软件模块都应该有且仅有一个被修改的原因。」

2.任何一个软件模块都应该只对一个用户(User)或系统利益相关者(Stakeholder)负责。

3.「任何一个软件模块都应该只对某一类行为者负责。」

4.「反例: 代码合并冲突」

单一职责原则非常容易被误认为“每个模块应该只做一件事”,没错之前我也是这么理解的,虽然这个描述没错,但是这并不是 SRP 的全部。

  • 多人为了不同的目的修改了同一份源代码,这很容易造成问题的产生。
  • 避免这种问题产生的方法就是将服务不同行为者的代码进行切分。

第 8 章 OCP:开闭原则

1.设计良好的计算机软件应该易于扩展,同时抗拒修改。

  • 换句话说,「一个设计良好的计算机系统应该在不需要修改的前提下就可以轻易被扩展。」

2.一个好的软件架构设计师会努力将旧代码的修改需求量降至最小,甚至为 0。

  • 可以先将满足不同需求的代码分组(即 SRP),然后再来调整这些分组之间的依赖关系(即 DIP)

3.如果 A 组件不想被 B 组件上发生的修改所影响,那么就应该让 B 组件依赖于 A 组件。

4.软件架构师可以根据相关函数被修改的原因、修改的方式及修改的时间来对其进行分组隔离,并将这些互相隔离的函数分组整理成组件结构,使得高阶组件不会因低阶组件被修改而受到影响。

5.OCP 是我们进行系统架构设计的主导原则,其主要目标是让系统易于扩展,同时限制其每次被修改所影响的范围。

开闭原则在架构设计上非常常见,其中最常见的做法就是使用接口实现依赖反转,如果开闭原则实现的不好就有可能导致我们在进行后续功能扩展的时候牵一发而动全身,成本非常的高。

第 9 章 LSP:里氏替换原则

1.如果对于每个类型是 S 的对象 o1 都存在一个类型为 T 的对象 o2,能使操作 T 类型的程序 P 在用 o2 替换 o1 时行为保持不变,我们就可以将 S 称为 T 的子类型。

2.比较常见的一个违反 LSP 原则的例子,长方形与正方形

这个反面例子对我的震撼比较大,依稀记得最开始在学习编程语言继承的例子的时候就常常用长方形正方形来举例,但是这个其实是违反了里式替换原则的。在架构设计上这个原则也十分的重要,因为我们只有做到了 LSP 我们才可以在例如数据库类型切换,微服务拆分这种场景下做的游刃有余。

  • Square 类并不是 Rectangle 类的子类型,因为 Rectangle 类的高和宽可以分别修改,而 Square 类的高和宽则必须一同修改。

第 10 章 ISP:接口隔离原则

  • ISP 最初的成因:在一般情况下,任何层次的软件设计如果依赖于不需要的东西,都会是有害的。
  • 任何层次的软件设计如果依赖了它并不需要的东西,就会带来意料之外的麻烦。

由于 Go 接口的隐式依赖的特性,让 ISP 在 Go 中处处可见,我们常常采用的方式就是在调用者处依赖接口,而不管实现,这样就可以做到,模块分离以及最小化依赖。

第 11 章 DIP:依赖反转原则

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

2.在应用 DIP 时,我们也不必考虑稳定的操作系统或者平台设施,因为这些系统接口很少会有变动。

3.主要应该关注的是软件系统内部那些会经常变动的(volatile)具体实现模块,这些模块是不停开发的,也就会经常出现变更。

4.编码规范

通常来说,接口会比实现更加稳定,举个反例,如果接口变动实现是必须要跟着修改的,因为实现是依赖接口的,但是反过来确未必。DIP 原则指导我们无论是在架构设计还是在编码实现当中都应该尽量的依赖抽象而不是实现细节。

  • 应在代码中多使用抽象接口,尽量避免使用那些多变的具体实现类。
  • 不要在具体实现类上创建衍生类。我们对继承的使用应该格外小心。即使是在稍微便于修改的动态类型语言中,这条守则也应该被认真考虑
  • 不要覆盖(override)包含具体实现的函数
  • 应避免在代码中写入与任何具体实现相关的名字,或者是其他容易变动的事物的名字。

第四部分 组件构建原则

 

第 12 章 组件

1.组件是软件的部署单元,是整个软件系统在部署过程中可以独立完成部署的最小实体

  • 例如:.jar, .gem, .dll 文件

2.链接加载器让程序员们可以将程序切分成多个可被分别编译、加载的程序段

3.组件化的插件式架构已经成为我们习以为常的软件构建形式了。

第 13 章 组件聚合

1.构建组件相关的基本原则

  • REP:复用/发布等同原则
  • CCP:共同闭包原则
  • CRP:共同复用原则

2.REP:复用/发布等同原则

  • 软件复用的最小粒度应等同于其发布的最小粒度。
  • REP 原则就是指组件中的类与模块必须是彼此紧密相关的
  • 一个组件不能由一组毫无关联的类和模块组成,它们之间应该有一个共同的主题或者大方向。

3.CCP:共同闭包原则

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

4.CRP:共同复用原则

  • 不要强迫一个组件的用户依赖他们不需要的东西。
  • 不要依赖不需要用到的东西。

5.组件张力图

Go工程化(一) 架构整洁之道阅读笔记

image.png

看到这三个原则会感到有点熟悉,像共同闭包原则就和 SOLID 中的单一职责原则类似,共同复用原则和接口隔离原则看上去也有那么几分相似,这些知识从不同的角度看待总结问题的不同术语。最后这个组件张力图很有意思,这说明我们在进行架构设计的时候是不可能做到每一项都很完美的,这当中会有一个取舍的过程,书中讲到,一般而言会项目初期会从三角右侧开始,进行一段时间后会滑动到左边,是因为在初期为了效率我们可以牺牲一定的复用性,但是随着依赖关系越来越复杂,那么我们就要考虑复用和扩展了。

第 14 章 组件耦合

  • 组件依赖关系图中不应该出现环。
  • 当组件结构依赖图中存在循环依赖时,想要按正确的顺序构建组件几乎是不可能的。
  • 打破循环依赖
  1. 应用依赖反转原则(DIP)
  2. 创建一个新的组件,并让 Entities 与 Authorize 这两个组件都依赖于它。将现有的这两个组件中互相依赖的类全部放入新组件
  • 组件结构图是不可能自上而下被设计出来的。它必须随着软件系统的变化而变化和扩张,而不可能在系统构建的最初就被完美设计出来。
  • 组件依赖结构图并不是用来描述应用程序功能的,它更像是应用程序在构建性与维护性方面的一张地图
  • 组件结构图中的一个重要目标是指导如何隔离频繁的变更
  • 如果我们在设计具体类之前就来设计组件依赖关系,那么几乎是必然要失败的。因为在当下,我们对项目中的共同闭包一无所知,也不可能知道哪些组件可以复用,这样几乎一定会创造出循环依赖的组件。

在 Go 中在编译器上就限制了我们不能出现循环依赖,所以我们大量的使用了 DIP 的方式,但是讲层次拔高一点,从微服务的角度来讲仍然不应该出现循环依赖,如果出现那么在版本发布的时候可能会导致灾难性的后果,架构的原则都是想通的,我们要时刻警惕循环依赖的出现,对于微服务来说可以在 api 网关进行判定是否成环

稳定依赖原则

  • 依赖关系必须要指向更稳定的方向
  • 任何一个我们预期会经常变更的组件都不应该被一个难于修改的组件所依赖,否则这个多变的组件也将会变得非常难以被修改
  • 让软件组件难于修改的一个最直接的办法就是让很多其他组件依赖于它。

稳定性指标

这一部分提出了一个对我现阶段非常有用的一个原则,被大量依赖的组件应该是稳定的,依赖关系必须要指向更稳定的方向,我当前处在公共技术团队,我们的服务被外部大量的依赖,所以在变更的时候会非常的麻烦,我们 I 值非常的小,几乎可以说接近于 0,所以我们的服务在设计时一定要满足开闭原则,保证足够的扩展性。

  • Fan-in:入向依赖,这个指标指代了组件外部类依赖于组件内部类的数量。
  • Fan-out:出向依赖,这个指标指代了组件内部类依赖于组件外部类的数量。
  • I:不稳定性,I=Fan-out/(Fan-in+Fan-out)。该指标的范围是[0,1],I=0 意味着组件是最稳定的,I=1 意味着组件是最不稳定的。
  1. 其中一种方法是计算所有入和出的依赖关系。通过这种方法,我们就可以计算出一个组件的位置稳定性(positionalstability)。
  2. 稳定依赖原则(SDP)的要求是让每个组件的 I 指标都必须大于其所依赖组件的 I 指标。也就是说,组件结构依赖图中各组件的 I 指标必须要按其依赖关系方向递减。

稳定抽象原则

稳定抽象原则说明了越稳定的组件应该越抽象,从代码的角度来讲,接口是最抽象的组件之一,因为接口一般不会有其他外部的依赖,而被大量依赖,同时还给出一个统计抽象程度的方法,这个可以用来统计一下我们现在的现状。

  • 只有多变的软件组件落在痛苦区中才会造成麻烦
  • 现在我们来看看靠近(1,1)这一位置点的组件。该位置上的组件不会是我们想要的,因为这些组件通常是无限抽象的,但是没有被其他组件依赖,这样的组件往往无法使用。
  • 追求让这些组件位于主序列线上,或者贴近这条线即可。
  • Nc:组件中类的数量。
  • Na:组件中抽象类和接口的数量。
  • A:抽象程度,A=Na÷Nc
  • A 指标的取值范围是从 0 到 1,值为 0 代表组件中没有任何抽象类,值为 1 就意味着组件中只有抽象类。

一个组件的抽象化程度应该与其稳定性保持一致。

如何才能让一个无限稳定的组件(I=0)接受变更呢?开闭原则(OCP)为我们提供了答案。这个原则告诉我们:创造一个足够灵活、能够被扩展,而且不需要修改的类是可能的,而这正是我们所需

假设 A 指标是对组件抽象化程度的一个衡量,它的值是组件中抽象类与接口所占的比例。那么:

Go工程化(一) 架构整洁之道阅读笔记

image.png
  • D 指标[8]:距离 D=|A+I-1|,该指标的取值范围是[0,1]。值为 0 意味着组件是直接位于主序列线上的,值为 1 则意味着组件在距离主序列最远的位置。
  • 对于一个良好的系统设计来说,D 指标的平均值和方差都应该接近于 0

第五部分 软件架构

 

第 15 章 什么是软件架构

1.软件架构师自身需要是程序员,并且必须一直坚持做一线程序员,绝对不要听从那些说应该让软件架构师从代码中解放出来以专心解决高阶问题的伪建议

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

这个也是常常会遇到的问题,就现在我能观察到的为例,架构师级别的基本上没有看到过再做一线的程序开发工作,仅仅是平时的各种管理,规划上的事务就已经忙的不可开交,这其实不仅仅导致了架构师本身会脱节,同时也会导致下面的同学很少有机会学习到架构师们过往的经验。

3.软件架构这项工作的实质就是规划如何将系统切分成组件,并安排好组件之间的排列关系,以及组件之间互相通信的方式。

4.设计软件架构的目的,就是为了在工作中更好地对这些组件进行研发、部署、运行以及维护。

5.如果想设计一个便于推进各项工作的系统,其策略就是要在设计中尽可能长时间地保留尽可能多的可选项。

6.设计良好的架构可以让系统便于理解、易于修改、方便维护,并且能轻松部署。「软件架构的终极目标就是最大化程序员的生产力,同时最小化系统的总运营成本。」

7.开发

  • 实现一键式的轻松部署应该是我们设计软件架构的一个目标

8.运行

人力成本往往会比机器的成本更高,所以这也就是我们在代码编写的过程当中对可读性和性能需要有一个权衡,如果不是差异过大往往代码的可读性需要更为重要

  • 几乎任何运行问题都可以通过增加硬件的方式来解决,这避免了软件架构的重新设计
  • 基于投入/产出比的考虑,我们的优化重心应该更倾向于系统的开发、部署以及维护
  • 一个设计良好的软件架构应该能明确地反映该系统在运行时的需求。

9.维护

  • 在软件系统的所有方面中,维护所需的成本是最高的

10.保持可选项

软件的高层策略不应该关心其底层到底使用哪一种数据库

开发的早期阶段也不应该选定使用的 Web 服务

软件的高层策略压根不应该跟这些有关。

在开发的早期阶段不应过早地采用依赖注入框架

  • 软件有行为价值与架构价值两种价值。这其中的第二种价值又比第一种更重要
  • 软件的灵活性则取决于系统的整体状况、组件的布置以及组件之间的连接方式。
  • 如果在开发高层策略时有意地让自己摆脱具体细节的纠缠,我们就可以将与具体实现相关的细节决策推迟或延后,因为越到项目的后期,我们就拥有越多的信息来做出合理的决策。
  • 一个优秀的软件架构师应该致力于最大化可选项数量

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

这一点其实很容易被忽略掉,因为我们经常做的工作就是细节性的工作,在进行设计的时候很容易就不自觉的假定 Web UI,MySQL 数据库这些技术选型,在这本书的最后一个章节还会讲到,这些细节。

第 16 章 独立性

1.用例

  • 软件的架构必须为其用例提供支持。

2.任何一个组织在设计系统时,往往都会复制出一个与该组织内沟通结构相同的系统。

3.一个设计良好的架构通常不会依赖于成堆的脚本与配置文件,也不需要用户手动创建一堆“有严格要求”的目录与文件

4.如果我们按照变更原因的不同对系统进行解耦,就可以持续地向系统内添加新的用例,而不会影响旧有的用例。如果我们同时对支持这些用例的 UI 和数据库也进行了分组,那么每个用例使用的就是不同面向的 UI 与数据库,因此增加新用例就更不太可能会影响旧有的用例了。

5.如果有两段看起来重复的代码,它们走的是不同的演进路径,也就是说它们有着不同的变更速率和变更缘由,那么这两段代码就不是真正的重复

6.解耦模式

“如果两段看似重复的代码,如果有不同的变更速率和原因,那么这两段代码就不算是真正的重复”这有个非常典型的例子就是 API 接口的参数和最后我们模型数据虽然很多时候大部分字段是相同的,但是它们的变更速率和原因其实都是不一样的,如果把他们耦合在一起虽然前期可能可以减少一些代码的编写,但是到最后需要扩展时会发现变更会很困难。之前我还写了一篇文章 《[Go Web 小技巧(三)Gin 参数绑定 ](https://lailin.xyz/post/11996.html#2-%E7%94%A8-model-%E5%B1%82%E7%9A%84-struct-%E7%BB%91%E5%AE%9A%E5%8F%82%E6%95%B0)》总结这种埋坑的技巧