注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用
本系列为 Go 进阶训练营 笔记,访问 博客: Go进阶训练营, 即可查看当前更新进度,部分文章篇幅较长,使用 PC 大屏浏览体验更佳。
其实这一篇文章不应该算在这里面,(PS: 毛老师课程上没讲这本书)但是恰好最近把这本书读完了,并且部门内推荐大家读这本书,毛老师在课上也推荐这本书,也和我们这次的主题有一些关系,一切都是最好的安排,那就放这系列吧。
阅读建议: 全文接近 2W 字,篇幅较长,采用书中重点摘录+不成熟的个人小结组成,桌面端可以点击右侧目录快速定位到你感兴趣的章节
这说明什么呢,说明了可能我们以为过时的,古老的技术或者解决方案也是有用的
软件的架构的终极目标,以及如何衡量一个架构的优劣,尤其是两个错误的观点非常感同身受,我也说过类似的话语,还有一句话是“当前的需求非常紧急,这只是一个临时的系统很快就会被替换掉,我们先完成它”。作为一个专业的技术人员我们需要有一些底线来保证我们的代码架构和质量,不能轻易妥协,这在 Bob 大叔整洁系列的另外一本书中也有提到。
行为价值
只有可以产生收入的代码才是有用的代码,技术是需要为业务服务的,但是我们的工作并不是说就按照需求文档写代码,修bug就行了
架构价值
架构价值主要就是为了能够应对变化,其实举个反面例子,我们之前有一个系统 A 是直接在 A 中调用接口获取数据,随着业务的发展我们拆分了一个应用 B 需要从 B 中获取对应的数据,这个时候我们发现代码变更非常严重,从里到外都需要进行重构修改,这就是典型了依赖了“具体的形状”导致的额外成本
重要紧急的排序
业务/市场的同事往往是无法评估架构的重要性的,所以,平衡系统架构的重要性与功能的紧急程度这件事,是软件研发人员自己的职责。
我们当前处在公共技术的部门,这也是一个经常困扰的一个例子,所有的业务方在提需求的时候都会表示需求非常紧急,但是这个功能的实现对我们来说重要吗?这个需要打上一个大大的问号,其他部门的同学其实是无法对评估需求对于我们的重要性的,这个需要我们自己来权衡。
为好的软件架构而持续斗争
这不仅仅是架构师的职责,这是每一位开发同学的职责,忽略架构的价值会导致我们带来无休止的加班,领导的质疑,产品的argue
编程范式指的是程序的编写模式,与具体的编程语言关系相对较小。这些范式会告诉你应该在什么时候采用什么样的代码结构
当前的三种编程范式,结构化编程,面向对象,函数式编程
这个角度之前还没有看到过,对我而言还是比较新奇,从限制的角度来看不同的编程范式有着不同限制,可以减少在编程当中出错的可能
结构化编程可以让我们将一个大的模块按照功能进行拆分,变成小的功能模块,同时通过测试我们可以证明其错误性,无论是架构上还是实际的开发过程中,大模块拆小模块的思路的数不胜数,其实单体应用拆分为微服务应用也是这个范畴内的。
.h 的模块是无法知道结构体中的成员变量的,但是 C++ 的头文件中包含了成员信息。在刚学习编程的时候,学到面向对象一定会说到,封装、继承、和多态,但是通过这一章我们可以发现,面向对象语言的封装不一定比面向过程的 C 语言做的更好,这里强调的更重要的是使用多态的手段对源码的依赖关系进行控制,主要是指通过接口来实现依赖反转,这样就可以将组件进行分离,可以进行独立开发和部署。 我现在主要使用的语言是 Go,有一个常见的问题就是 Go 是不是一个面向对象语言,回答也是 Yes or no,是也不是,Go 不支持继承,也不支持函数重载,运算符重载等在面向对象语言非常常见的特性,但是 Go 的接口非常强大,不需要显示依赖接口的设计让我们在依赖反转的使用上更加游刃有余。
在我们刚刚结束的上一个系列,[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 原则的历史已经非常悠久了,但是直到现在它仍然非常具有指导意义。
单一职责原则非常容易被误认为“每个模块应该只做一件事”,没错之前我也是这么理解的,虽然这个描述没错,但是这并不是 SRP 的全部。
开闭原则在架构设计上非常常见,其中最常见的做法就是使用接口实现依赖反转,如果开闭原则实现的不好就有可能导致我们在进行后续功能扩展的时候牵一发而动全身,成本非常的高。
这个反面例子对我的震撼比较大,依稀记得最开始在学习编程语言继承的例子的时候就常常用长方形正方形来举例,但是这个其实是违反了里式替换原则的。 在架构设计上这个原则也十分的重要,因为我们只有做到了 LSP 我们才可以在例如数据库类型切换,微服务拆分这种场景下做的游刃有余。
由于 Go 接口的隐式依赖的特性,让 ISP 在 Go 中处处可见,我们常常采用的方式就是在调用者处依赖接口,而不管实现,这样就可以做到,模块分离以及最小化依赖。
通常来说,接口会比实现更加稳定,举个反例,如果接口变动实现是必须要跟着修改的,因为实现是依赖接口的,但是反过来确未必。DIP 原则指导我们无论是在架构设计还是在编码实现当中都应该尽量的依赖抽象而不是实现细节。

看到这三个原则会感到有点熟悉,像共同闭包原则就和 SOLID 中的单一职责原则类似,共同复用原则和接口隔离原则看上去也有那么几分相似,这些知识从不同的角度看待总结问题的不同术语。 最后这个组件张力图很有意思,这说明我们在进行架构设计的时候是不可能做到每一项都很完美的,这当中会有一个取舍的过程,书中讲到,一般而言会项目初期会从三角右侧开始,进行一段时间后会滑动到左边,是因为在初期为了效率我们可以牺牲一定的复用性,但是随着依赖关系越来越复杂,那么我们就要考虑复用和扩展了。
组件依赖关系图中不应该出现环。
当组件结构依赖图中存在循环依赖时,想要按正确的顺序构建组件几乎是不可能的。
打破循环依赖
组件结构图是不可能自上而下被设计出来的。它必须随着软件系统的变化而变化和扩张,而不可能在系统构建的最初就被完美设计出来。
组件依赖结构图并不是用来描述应用程序功能的,它更像是应用程序在构建性与维护性方面的一张地图
组件结构图中的一个重要目标是指导如何隔离频繁的变更
如果我们在设计具体类之前就来设计组件依赖关系,那么几乎是必然要失败的。因为在当下,我们对项目中的共同闭包一无所知,也不可能知道哪些组件可以复用,这样几乎一定会创造出循环依赖的组件。
在 Go 中在编译器上就限制了我们不能出现循环依赖,所以我们大量的使用了 DIP 的方式,但是讲层次拔高一点,从微服务的角度来讲仍然不应该出现循环依赖,如果出现那么在版本发布的时候可能会导致灾难性的后果,架构的原则都是想通的,我们要时刻警惕循环依赖的出现,对于微服务来说可以在 api 网关进行判定是否成环
稳定依赖原则
稳定性指标
这一部分提出了一个对我现阶段非常有用的一个原则,被大量依赖的组件应该是稳定的,依赖关系必须要指向更稳定的方向,我当前处在公共技术团队,我们的服务被外部大量的依赖,所以在变更的时候会非常的麻烦,我们 I 值非常的小,几乎可以说接近于 0,所以我们的服务在设计时一定要满足开闭原则,保证足够的扩展性。
稳定抽象原则

稳定抽象原则说明了越稳定的组件应该越抽象,从代码的角度来讲,接口是最抽象的组件之一,因为接口一般不会有其他外部的依赖,而被大量依赖,同时还给出一个统计抽象程度的方法,这个可以用来统计一下我们现在的现状。
软件架构师自身需要是程序员,并且必须一直坚持做一线程序员,绝对不要听从那些说应该让软件架构师从代码中解放出来以专心解决高阶问题的伪建议
如果不亲身承受因系统设计而带来的麻烦,就体会不到设计不佳所带来的痛苦,接着就会逐渐迷失正确的设计方向。
这个也是常常会遇到的问题,就现在我能观察到的为例,架构师级别的基本上没有看到过再做一线的程序开发工作,仅仅是平时的各种管理,规划上的事务就已经忙的不可开交,这其实不仅仅导致了架构师本身会脱节,同时也会导致下面的同学很少有机会学习到架构师们过往的经验。
软件架构这项工作的实质就是规划如何将系统切分成组件,并安排好组件之间的排列关系,以及组件之间互相通信的方式。
设计软件架构的目的,就是为了在工作中更好地对这些组件进行研发、部署、运行以及维护。
如果想设计一个便于推进各项工作的系统,其策略就是要在设计中尽可能长时间地保留尽可能多的可选项。
设计良好的架构可以让系统便于理解、易于修改、方便维护,并且能轻松部署。软件架构的终极目标就是最大化程序员的生产力,同时最小化系统的总运营成本。
开发
运行
人力成本往往会比机器的成本更高,所以这也就是我们在代码编写的过程当中对可读性和性能需要有一个权衡,如果不是差异过大往往代码的可读性需要更为重要
维护
保持可选项
优秀的架构师会小心地将软件的高层策略与其底层实现隔离开,让高层策略与实现细节脱钩,使其策略部分完全不需要关心底层细节,当然也不会对这些细节有任何形式的依赖。另外,优秀的架构师所设计的策略应该允许系统尽可能地推迟与实现细节相关的决策,越晚做决策越好
这一点其实很容易被忽略掉,因为我们经常做的工作就是细节性的工作,在进行设计的时候很容易就不自觉的假定 Web UI,MySQL 数据库这些技术选型,在这本书的最后一个章节还会讲到,这些细节。
“如果两段看似重复的代码,如果有不同的变更速率和原因,那么这两段代码就不算是真正的重复”这有个非常典型的例子就是 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)》总结这种埋坑的技巧 😂

真正核心的是我们业务逻辑,而输入输出是细节
不同的边界的跨边界调用的成本是不同的,对于服务而言跨服务调用的成本非常高,这样我们在进行服务划分的时候一定要尽量的内聚减少频繁调用的情况。
距离 I/O 越远的策略层次越高,也就是说我们常见的 Web UI 应该属于最低层次,我们不应该依赖 Web UI 这种输入输出设备。同时给出了组件的划分原则,变更的时间原因和层次相同的属于同一个组件。
业务逻辑就是程序中那些真正用于赚钱或省钱的业务逻辑与过程
“关键业务逻辑”是一项业务的关键部分,不管有没有自动化系统来执行这项业务,这一点是不会改变的。
业务实体
用例(usecase)
用例和业务实体应该是应用当中最重要的,所以我们的单元测试最低的要求就是要覆盖所有的 usecase 逻辑,这一部分应该保持纯净不依赖数据库,Web 等 I/O 方式
选择直接在数据结构中使用对业务实体对象的引用。毕竟,业务实体与请求/响应模型之间有很多相同的数据。但请一定不要这样做!这两个对象存在的意义是非常、非常不一样的。随着时间的推移,这两个对象会以不同的原因、不同的速率发生变更。
这些业务逻辑应该保持纯净,不要掺杂用户界面或者所使用的数据库相关的东西。在理想情况下,这部分代表业务逻辑的代码应该是整个系统的核心,其他低层概念的实现应该以插件形式接入系统中。业务逻辑应该是系统中最独立、复用性最高的代码。
再次强调了不要偷懒,今天刚好看到之前写的一个反面例子的代码,代码里面有一个 GetA 函数,从数据库当中获取A对象数据和一些统计数据,这个函数中的统计数据部分其实只有在一个 Web 页面的接口中使用到,但是为了偷懒,在其他地方查询的时候也调用了这个函数,导致最后很多地方的接口性能都由于这个没用的统计数据多耗费了将近 1s 的时间。
用例是架构设计当中最应该关注的部分,框架数据库Web服务的选择都是细节,这些细节应该延后选择,我们的用例不应该依赖这些细节,这样才能很好的测试

看过前面的部分再来看整洁架构这一章节会发现非常的自然
这里主要是将很难进行单元测试的行为和容易测试的行为进行分离,很难被测试的行为常常会被分离成为一个谦卑对象,这个对象非常的简单,不会包含很多逻辑
架构是需要取舍的,我们不可能每一项都做的很完美,边界的划分也是这样,所以就有了不完全的边界
不要过度优化,但是也不要什么都不管的一把梭,架构师需要演进和取舍的,没有完美的架构只有不断持续演进优化的架构。
main 是一个程序的入口,这是最细节的部分,因为之前为了很多东西不被依赖,我们一般会采用接口来实现依赖反转,这时候就会导致我们所有的依赖关系的构建都需要在 main 中进行完成,所以一般而言我们会在 main 中引入依赖注入框架。
虽然现在微服务架构非常火热,基本上所有的服务都是拆分了服务,但是拆分了服务并不一定表示就解耦合了,也并不一定就真的能独立部署,想一想这是现在很常见的,一个应用必须要和另外一个应用一同上线,根本做不了独立部署。
不变的组件不要依赖多变的东西,这样会导致非常难以测试
软件并不会随着时间磨损但是硬件是会过时的,而且换的还非常频繁,这时候我们就必须要把硬件以及固件代码给隔离起来,对了不要认为我们不做嵌入式开发平时就很少接触到这个,SQL 语句其实也是一种固件代码
数据很重要,但是数据库系统是一个细节,书上这一章用了一个例子说明有时候可能真的用不到数据库。换个常见的例子,我们可能系统刚开始的时候使用 SQlite 就可以,随着业务发展用上了 MySQL,然后随着并发的提高又会引入缓存组件,这些变化其实和业务逻辑都没有关系,数据库的变化是不应该影响到业务逻辑的
框架的选择要慎重,我们业务逻辑本身不能依赖框架
这一步看起来简单,但是非常考验一个人的功力

这一章对比了四种架构风格,同时提出了,架构设计是需要考虑实现细节的,设计需要映射到代码结构和代码树上,这个其实和最开始的“软件架构师自身需要是程序员,并且必须一直坚持做一线程序员”交相呼应。 如果可以在编译时解决的问题,就不要放到运行时,编译的问题往往要比运行时的问题好解决,这也是为什么 Go 的依赖注入框架我更加推荐 wire 的原因,同理作者提出了 如果要防止直接中 web控制器调用数据层,那么我们就不应该将数据层(repo)暴露出来,只需要暴露 usecase 就好了。
之前其实也大概了解过整洁架构,从最开始觉得它又臭又长,到现在工作两三年后觉得“不听老人言,吃亏在眼前”,当我们在对一个架构或者是事务进行批判的时候一定要了解它面对的场景以及它的理念,这是最重要的。当然软件领域是没有银弹的,我们需要做的是吸收每一种思想,在不同的场景下做不同的取舍,接下来会有几篇文章结合毛老师课上讲的 Go 工程化相关的内容,以及我在工作当中进行的一些总结最后提出一种当下我觉得的 Go 项目的组织方式,这种方式不是最好的,但是我觉得是现阶段最适合的。
推荐大家在仔细的阅读一下本书,期望你能有更多的收获。
注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用
本系列为 Go 进阶训练营 笔记,访问 博客: Go进阶训练营, 即可查看当前更新进度,部分文章篇幅较长,使用 PC 大屏浏览体验更佳。
其实这一篇文章不应该算在这里面,(PS: 毛老师课程上没讲这本书)但是恰好最近把这本书读完了,并且部门内推荐大家读这本书,毛老师在课上也推荐这本书,也和我们这次的主题有一些关系,一切都是最好的安排,那就放这系列吧。
阅读建议: 全文接近 2W 字,篇幅较长,采用书中重点摘录+不成熟的个人小结组成,桌面端可以点击右侧目录快速定位到你感兴趣的章节
这说明什么呢,说明了可能我们以为过时的,古老的技术或者解决方案也是有用的
软件的架构的终极目标,以及如何衡量一个架构的优劣,尤其是两个错误的观点非常感同身受,我也说过类似的话语,还有一句话是“当前的需求非常紧急,这只是一个临时的系统很快就会被替换掉,我们先完成它”。作为一个专业的技术人员我们需要有一些底线来保证我们的代码架构和质量,不能轻易妥协,这在 Bob 大叔整洁系列的另外一本书中也有提到。
行为价值
只有可以产生收入的代码才是有用的代码,技术是需要为业务服务的,但是我们的工作并不是说就按照需求文档写代码,修bug就行了
架构价值
架构价值主要就是为了能够应对变化,其实举个反面例子,我们之前有一个系统 A 是直接在 A 中调用接口获取数据,随着业务的发展我们拆分了一个应用 B 需要从 B 中获取对应的数据,这个时候我们发现代码变更非常严重,从里到外都需要进行重构修改,这就是典型了依赖了“具体的形状”导致的额外成本
重要紧急的排序
业务/市场的同事往往是无法评估架构的重要性的,所以,平衡系统架构的重要性与功能的紧急程度这件事,是软件研发人员自己的职责。
我们当前处在公共技术的部门,这也是一个经常困扰的一个例子,所有的业务方在提需求的时候都会表示需求非常紧急,但是这个功能的实现对我们来说重要吗?这个需要打上一个大大的问号,其他部门的同学其实是无法对评估需求对于我们的重要性的,这个需要我们自己来权衡。
为好的软件架构而持续斗争
这不仅仅是架构师的职责,这是每一位开发同学的职责,忽略架构的价值会导致我们带来无休止的加班,领导的质疑,产品的argue
编程范式指的是程序的编写模式,与具体的编程语言关系相对较小。这些范式会告诉你应该在什么时候采用什么样的代码结构
当前的三种编程范式,结构化编程,面向对象,函数式编程
这个角度之前还没有看到过,对我而言还是比较新奇,从限制的角度来看不同的编程范式有着不同限制,可以减少在编程当中出错的可能
结构化编程可以让我们将一个大的模块按照功能进行拆分,变成小的功能模块,同时通过测试我们可以证明其错误性,无论是架构上还是实际的开发过程中,大模块拆小模块的思路的数不胜数,其实单体应用拆分为微服务应用也是这个范畴内的。
.h 的模块是无法知道结构体中的成员变量的,但是 C++ 的头文件中包含了成员信息。在刚学习编程的时候,学到面向对象一定会说到,封装、继承、和多态,但是通过这一章我们可以发现,面向对象语言的封装不一定比面向过程的 C 语言做的更好,这里强调的更重要的是使用多态的手段对源码的依赖关系进行控制,主要是指通过接口来实现依赖反转,这样就可以将组件进行分离,可以进行独立开发和部署。 我现在主要使用的语言是 Go,有一个常见的问题就是 Go 是不是一个面向对象语言,回答也是 Yes or no,是也不是,Go 不支持继承,也不支持函数重载,运算符重载等在面向对象语言非常常见的特性,但是 Go 的接口非常强大,不需要显示依赖接口的设计让我们在依赖反转的使用上更加游刃有余。
在我们刚刚结束的上一个系列,[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 原则的历史已经非常悠久了,但是直到现在它仍然非常具有指导意义。
单一职责原则非常容易被误认为“每个模块应该只做一件事”,没错之前我也是这么理解的,虽然这个描述没错,但是这并不是 SRP 的全部。
开闭原则在架构设计上非常常见,其中最常见的做法就是使用接口实现依赖反转,如果开闭原则实现的不好就有可能导致我们在进行后续功能扩展的时候牵一发而动全身,成本非常的高。
这个反面例子对我的震撼比较大,依稀记得最开始在学习编程语言继承的例子的时候就常常用长方形正方形来举例,但是这个其实是违反了里式替换原则的。 在架构设计上这个原则也十分的重要,因为我们只有做到了 LSP 我们才可以在例如数据库类型切换,微服务拆分这种场景下做的游刃有余。
由于 Go 接口的隐式依赖的特性,让 ISP 在 Go 中处处可见,我们常常采用的方式就是在调用者处依赖接口,而不管实现,这样就可以做到,模块分离以及最小化依赖。
通常来说,接口会比实现更加稳定,举个反例,如果接口变动实现是必须要跟着修改的,因为实现是依赖接口的,但是反过来确未必。DIP 原则指导我们无论是在架构设计还是在编码实现当中都应该尽量的依赖抽象而不是实现细节。

看到这三个原则会感到有点熟悉,像共同闭包原则就和 SOLID 中的单一职责原则类似,共同复用原则和接口隔离原则看上去也有那么几分相似,这些知识从不同的角度看待总结问题的不同术语。 最后这个组件张力图很有意思,这说明我们在进行架构设计的时候是不可能做到每一项都很完美的,这当中会有一个取舍的过程,书中讲到,一般而言会项目初期会从三角右侧开始,进行一段时间后会滑动到左边,是因为在初期为了效率我们可以牺牲一定的复用性,但是随着依赖关系越来越复杂,那么我们就要考虑复用和扩展了。
组件依赖关系图中不应该出现环。
当组件结构依赖图中存在循环依赖时,想要按正确的顺序构建组件几乎是不可能的。
打破循环依赖
组件结构图是不可能自上而下被设计出来的。它必须随着软件系统的变化而变化和扩张,而不可能在系统构建的最初就被完美设计出来。
组件依赖结构图并不是用来描述应用程序功能的,它更像是应用程序在构建性与维护性方面的一张地图
组件结构图中的一个重要目标是指导如何隔离频繁的变更
如果我们在设计具体类之前就来设计组件依赖关系,那么几乎是必然要失败的。因为在当下,我们对项目中的共同闭包一无所知,也不可能知道哪些组件可以复用,这样几乎一定会创造出循环依赖的组件。
在 Go 中在编译器上就限制了我们不能出现循环依赖,所以我们大量的使用了 DIP 的方式,但是讲层次拔高一点,从微服务的角度来讲仍然不应该出现循环依赖,如果出现那么在版本发布的时候可能会导致灾难性的后果,架构的原则都是想通的,我们要时刻警惕循环依赖的出现,对于微服务来说可以在 api 网关进行判定是否成环
稳定依赖原则
稳定性指标
这一部分提出了一个对我现阶段非常有用的一个原则,被大量依赖的组件应该是稳定的,依赖关系必须要指向更稳定的方向,我当前处在公共技术团队,我们的服务被外部大量的依赖,所以在变更的时候会非常的麻烦,我们 I 值非常的小,几乎可以说接近于 0,所以我们的服务在设计时一定要满足开闭原则,保证足够的扩展性。
稳定抽象原则

稳定抽象原则说明了越稳定的组件应该越抽象,从代码的角度来讲,接口是最抽象的组件之一,因为接口一般不会有其他外部的依赖,而被大量依赖,同时还给出一个统计抽象程度的方法,这个可以用来统计一下我们现在的现状。
软件架构师自身需要是程序员,并且必须一直坚持做一线程序员,绝对不要听从那些说应该让软件架构师从代码中解放出来以专心解决高阶问题的伪建议
如果不亲身承受因系统设计而带来的麻烦,就体会不到设计不佳所带来的痛苦,接着就会逐渐迷失正确的设计方向。
这个也是常常会遇到的问题,就现在我能观察到的为例,架构师级别的基本上没有看到过再做一线的程序开发工作,仅仅是平时的各种管理,规划上的事务就已经忙的不可开交,这其实不仅仅导致了架构师本身会脱节,同时也会导致下面的同学很少有机会学习到架构师们过往的经验。
软件架构这项工作的实质就是规划如何将系统切分成组件,并安排好组件之间的排列关系,以及组件之间互相通信的方式。
设计软件架构的目的,就是为了在工作中更好地对这些组件进行研发、部署、运行以及维护。
如果想设计一个便于推进各项工作的系统,其策略就是要在设计中尽可能长时间地保留尽可能多的可选项。
设计良好的架构可以让系统便于理解、易于修改、方便维护,并且能轻松部署。软件架构的终极目标就是最大化程序员的生产力,同时最小化系统的总运营成本。
开发
运行
人力成本往往会比机器的成本更高,所以这也就是我们在代码编写的过程当中对可读性和性能需要有一个权衡,如果不是差异过大往往代码的可读性需要更为重要
维护
保持可选项
优秀的架构师会小心地将软件的高层策略与其底层实现隔离开,让高层策略与实现细节脱钩,使其策略部分完全不需要关心底层细节,当然也不会对这些细节有任何形式的依赖。另外,优秀的架构师所设计的策略应该允许系统尽可能地推迟与实现细节相关的决策,越晚做决策越好
这一点其实很容易被忽略掉,因为我们经常做的工作就是细节性的工作,在进行设计的时候很容易就不自觉的假定 Web UI,MySQL 数据库这些技术选型,在这本书的最后一个章节还会讲到,这些细节。
“如果两段看似重复的代码,如果有不同的变更速率和原因,那么这两段代码就不算是真正的重复”这有个非常典型的例子就是 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)》总结这种埋坑的技巧 😂

真正核心的是我们业务逻辑,而输入输出是细节
不同的边界的跨边界调用的成本是不同的,对于服务而言跨服务调用的成本非常高,这样我们在进行服务划分的时候一定要尽量的内聚减少频繁调用的情况。
距离 I/O 越远的策略层次越高,也就是说我们常见的 Web UI 应该属于最低层次,我们不应该依赖 Web UI 这种输入输出设备。同时给出了组件的划分原则,变更的时间原因和层次相同的属于同一个组件。
业务逻辑就是程序中那些真正用于赚钱或省钱的业务逻辑与过程
“关键业务逻辑”是一项业务的关键部分,不管有没有自动化系统来执行这项业务,这一点是不会改变的。
业务实体
用例(usecase)
用例和业务实体应该是应用当中最重要的,所以我们的单元测试最低的要求就是要覆盖所有的 usecase 逻辑,这一部分应该保持纯净不依赖数据库,Web 等 I/O 方式
选择直接在数据结构中使用对业务实体对象的引用。毕竟,业务实体与请求/响应模型之间有很多相同的数据。但请一定不要这样做!这两个对象存在的意义是非常、非常不一样的。随着时间的推移,这两个对象会以不同的原因、不同的速率发生变更。
这些业务逻辑应该保持纯净,不要掺杂用户界面或者所使用的数据库相关的东西。在理想情况下,这部分代表业务逻辑的代码应该是整个系统的核心,其他低层概念的实现应该以插件形式接入系统中。业务逻辑应该是系统中最独立、复用性最高的代码。
再次强调了不要偷懒,今天刚好看到之前写的一个反面例子的代码,代码里面有一个 GetA 函数,从数据库当中获取A对象数据和一些统计数据,这个函数中的统计数据部分其实只有在一个 Web 页面的接口中使用到,但是为了偷懒,在其他地方查询的时候也调用了这个函数,导致最后很多地方的接口性能都由于这个没用的统计数据多耗费了将近 1s 的时间。
用例是架构设计当中最应该关注的部分,框架数据库Web服务的选择都是细节,这些细节应该延后选择,我们的用例不应该依赖这些细节,这样才能很好的测试

看过前面的部分再来看整洁架构这一章节会发现非常的自然
这里主要是将很难进行单元测试的行为和容易测试的行为进行分离,很难被测试的行为常常会被分离成为一个谦卑对象,这个对象非常的简单,不会包含很多逻辑
架构是需要取舍的,我们不可能每一项都做的很完美,边界的划分也是这样,所以就有了不完全的边界
不要过度优化,但是也不要什么都不管的一把梭,架构师需要演进和取舍的,没有完美的架构只有不断持续演进优化的架构。
main 是一个程序的入口,这是最细节的部分,因为之前为了很多东西不被依赖,我们一般会采用接口来实现依赖反转,这时候就会导致我们所有的依赖关系的构建都需要在 main 中进行完成,所以一般而言我们会在 main 中引入依赖注入框架。
虽然现在微服务架构非常火热,基本上所有的服务都是拆分了服务,但是拆分了服务并不一定表示就解耦合了,也并不一定就真的能独立部署,想一想这是现在很常见的,一个应用必须要和另外一个应用一同上线,根本做不了独立部署。
不变的组件不要依赖多变的东西,这样会导致非常难以测试
软件并不会随着时间磨损但是硬件是会过时的,而且换的还非常频繁,这时候我们就必须要把硬件以及固件代码给隔离起来,对了不要认为我们不做嵌入式开发平时就很少接触到这个,SQL 语句其实也是一种固件代码
数据很重要,但是数据库系统是一个细节,书上这一章用了一个例子说明有时候可能真的用不到数据库。换个常见的例子,我们可能系统刚开始的时候使用 SQlite 就可以,随着业务发展用上了 MySQL,然后随着并发的提高又会引入缓存组件,这些变化其实和业务逻辑都没有关系,数据库的变化是不应该影响到业务逻辑的
框架的选择要慎重,我们业务逻辑本身不能依赖框架
这一步看起来简单,但是非常考验一个人的功力

这一章对比了四种架构风格,同时提出了,架构设计是需要考虑实现细节的,设计需要映射到代码结构和代码树上,这个其实和最开始的“软件架构师自身需要是程序员,并且必须一直坚持做一线程序员”交相呼应。 如果可以在编译时解决的问题,就不要放到运行时,编译的问题往往要比运行时的问题好解决,这也是为什么 Go 的依赖注入框架我更加推荐 wire 的原因,同理作者提出了 如果要防止直接中 web控制器调用数据层,那么我们就不应该将数据层(repo)暴露出来,只需要暴露 usecase 就好了。
之前其实也大概了解过整洁架构,从最开始觉得它又臭又长,到现在工作两三年后觉得“不听老人言,吃亏在眼前”,当我们在对一个架构或者是事务进行批判的时候一定要了解它面对的场景以及它的理念,这是最重要的。当然软件领域是没有银弹的,我们需要做的是吸收每一种思想,在不同的场景下做不同的取舍,接下来会有几篇文章结合毛老师课上讲的 Go 工程化相关的内容,以及我在工作当中进行的一些总结最后提出一种当下我觉得的 Go 项目的组织方式,这种方式不是最好的,但是我觉得是现阶段最适合的。
推荐大家在仔细的阅读一下本书,期望你能有更多的收获。