本系列为极客时间 Go 进阶训练营笔记,同步直播更新,预计一周更新 1 ~ 2 篇文章,到 202103 月更新完成
创新互联公司主要从事成都网站建设、网站设计、网页设计、企业做网站、公司建网站等业务。立足成都服务漳州,十余年网站建设经验,价格优惠、服务专业,欢迎来电咨询建站服务:18980820575
其实这一篇文章不应该算在这里面,(PS: 毛老师课程上没讲这本书)但是恰好最近把这本书读完了,并且部门内推荐大家读这本书,毛老师在课上也推荐这本书,也和我们这次的主题有一些关系,一切都是最好的安排,那就放这系列吧。
阅读建议: 全文接近 2W 字,篇幅较长,采用书中重点摘录+不成熟的个人小结组成,桌面端可以点击右侧目录快速定位到你感兴趣的章节
这说明什么呢,说明了可能我们以为过时的,古老的技术或者解决方案也是有用的
软件的架构的终极目标,以及如何衡量一个架构的优劣,尤其是两个错误的观点非常感同身受,我也说过类似的话语,还有一句话是“当前的需求非常紧急,这只是一个临时的系统很快就会被替换掉,我们先完成它”。作为一个专业的技术人员我们需要有一些底线来保证我们的代码架构和质量,不能轻易妥协,这在 Bob 大叔整洁系列的另外一本书中也有提到。
1.行为价值
只有可以产生收入的代码才是有用的代码,技术是需要为业务服务的,但是我们的工作并不是说就按照需求文档写代码,修bug就行了
2.架构价值
架构价值主要就是为了能够应对变化,其实举个反面例子,我们之前有一个系统 A 是直接在 A 中调用接口获取数据,随着业务的发展我们拆分了一个应用 B 需要从 B 中获取对应的数据,这个时候我们发现代码变更非常严重,从里到外都需要进行重构修改,这就是典型了依赖了“具体的形状”导致的额外成本
3.重要紧急的排序
4.业务/市场的同事往往是无法评估架构的重要性的,所以,「平衡系统架构的重要性与功能的紧急程度这件事,是软件研发人员自己的职责。」
我们当前处在公共技术的部门,这也是一个经常困扰的一个例子,所有的业务方在提需求的时候都会表示需求非常紧急,但是这个功能的实现对我们来说重要吗?这个需要打上一个大大的问号,其他部门的同学其实是无法对评估需求对于我们的重要性的,这个需要我们自己来权衡。
5.为好的软件架构而持续斗争
这不仅仅是架构师的职责,这是每一位开发同学的职责,忽略架构的价值会导致我们带来无休止的加班,领导的质疑,产品的argue
编程范式指的是程序的编写模式,与具体的编程语言关系相对较小。这些范式会告诉你应该在什么时候采用什么样的代码结构 当前的三种编程范式,结构化编程,面向对象,函数式编程
1.结构化编程(面向过程)
2.面向对象
3.函数式编程
这个角度之前还没有看到过,对我而言还是比较新奇,从限制的角度来看不同的编程范式有着不同限制,可以减少在编程当中出错的可能
结构化编程可以让我们将一个大的模块按照功能进行拆分,变成小的功能模块,同时通过测试我们可以证明其错误性,无论是架构上还是实际的开发过程中,大模块拆小模块的思路的数不胜数,其实单体应用拆分为微服务应用也是这个范畴内的。
1.什么是面向对象?
面向对象理论是在 1966 年提出的,当时 Dahl 和 Nygaard 主要是将函数调用栈迁移到了堆区域中
2.封装
3.继承
4.多态
5.依赖反转
6**面向对象编程就是以多态为手段来对源代码中的依赖关系进行控制的能力,**这种能力让软件架构师可以构建出某种插件式架构,让高层策略性组件与底层实现性组件相分离,底层组件可以被编译成插件,实现独立于高层组件的开发和部署。
在刚学习编程的时候,学到面向对象一定会说到,封装、继承、和多态,但是通过这一章我们可以发现,面向对象语言的封装不一定比面向过程的 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 原则的历史已经非常悠久了,但是直到现在它仍然非常具有指导意义。
1.「任何一个软件模块都应该有且仅有一个被修改的原因。」
2.任何一个软件模块都应该只对一个用户(User)或系统利益相关者(Stakeholder)负责。
3.「任何一个软件模块都应该只对某一类行为者负责。」
4.「反例: 代码合并冲突」
单一职责原则非常容易被误认为“每个模块应该只做一件事”,没错之前我也是这么理解的,虽然这个描述没错,但是这并不是 SRP 的全部。
1.设计良好的计算机软件应该易于扩展,同时抗拒修改。
2.一个好的软件架构设计师会努力将旧代码的修改需求量降至最小,甚至为 0。
3.如果 A 组件不想被 B 组件上发生的修改所影响,那么就应该让 B 组件依赖于 A 组件。
4.软件架构师可以根据相关函数被修改的原因、修改的方式及修改的时间来对其进行分组隔离,并将这些互相隔离的函数分组整理成组件结构,使得高阶组件不会因低阶组件被修改而受到影响。
5.OCP 是我们进行系统架构设计的主导原则,其主要目标是让系统易于扩展,同时限制其每次被修改所影响的范围。
开闭原则在架构设计上非常常见,其中最常见的做法就是使用接口实现依赖反转,如果开闭原则实现的不好就有可能导致我们在进行后续功能扩展的时候牵一发而动全身,成本非常的高。
1.如果对于每个类型是 S 的对象 o1 都存在一个类型为 T 的对象 o2,能使操作 T 类型的程序 P 在用 o2 替换 o1 时行为保持不变,我们就可以将 S 称为 T 的子类型。
2.比较常见的一个违反 LSP 原则的例子,长方形与正方形
这个反面例子对我的震撼比较大,依稀记得最开始在学习编程语言继承的例子的时候就常常用长方形正方形来举例,但是这个其实是违反了里式替换原则的。在架构设计上这个原则也十分的重要,因为我们只有做到了 LSP 我们才可以在例如数据库类型切换,微服务拆分这种场景下做的游刃有余。
由于 Go 接口的隐式依赖的特性,让 ISP 在 Go 中处处可见,我们常常采用的方式就是在调用者处依赖接口,而不管实现,这样就可以做到,模块分离以及最小化依赖。
1.如果想要设计一个灵活的系统,在源代码层次的依赖关系中就应该多引用抽象类型,而非具体实现。
2.在应用 DIP 时,我们也不必考虑稳定的操作系统或者平台设施,因为这些系统接口很少会有变动。
3.主要应该关注的是软件系统内部那些会经常变动的(volatile)具体实现模块,这些模块是不停开发的,也就会经常出现变更。
4.编码规范
通常来说,接口会比实现更加稳定,举个反例,如果接口变动实现是必须要跟着修改的,因为实现是依赖接口的,但是反过来确未必。DIP 原则指导我们无论是在架构设计还是在编码实现当中都应该尽量的依赖抽象而不是实现细节。
1.组件是软件的部署单元,是整个软件系统在部署过程中可以独立完成部署的最小实体
2.链接加载器让程序员们可以将程序切分成多个可被分别编译、加载的程序段
3.组件化的插件式架构已经成为我们习以为常的软件构建形式了。
1.构建组件相关的基本原则
2.REP:复用/发布等同原则
3.CCP:共同闭包原则
4.CRP:共同复用原则
5.组件张力图
image.png
看到这三个原则会感到有点熟悉,像共同闭包原则就和 SOLID 中的单一职责原则类似,共同复用原则和接口隔离原则看上去也有那么几分相似,这些知识从不同的角度看待总结问题的不同术语。最后这个组件张力图很有意思,这说明我们在进行架构设计的时候是不可能做到每一项都很完美的,这当中会有一个取舍的过程,书中讲到,一般而言会项目初期会从三角右侧开始,进行一段时间后会滑动到左边,是因为在初期为了效率我们可以牺牲一定的复用性,但是随着依赖关系越来越复杂,那么我们就要考虑复用和扩展了。
在 Go 中在编译器上就限制了我们不能出现循环依赖,所以我们大量的使用了 DIP 的方式,但是讲层次拔高一点,从微服务的角度来讲仍然不应该出现循环依赖,如果出现那么在版本发布的时候可能会导致灾难性的后果,架构的原则都是想通的,我们要时刻警惕循环依赖的出现,对于微服务来说可以在 api 网关进行判定是否成环
稳定依赖原则
稳定性指标
这一部分提出了一个对我现阶段非常有用的一个原则,被大量依赖的组件应该是稳定的,依赖关系必须要指向更稳定的方向,我当前处在公共技术团队,我们的服务被外部大量的依赖,所以在变更的时候会非常的麻烦,我们 I 值非常的小,几乎可以说接近于 0,所以我们的服务在设计时一定要满足开闭原则,保证足够的扩展性。
稳定抽象原则
稳定抽象原则说明了越稳定的组件应该越抽象,从代码的角度来讲,接口是最抽象的组件之一,因为接口一般不会有其他外部的依赖,而被大量依赖,同时还给出一个统计抽象程度的方法,这个可以用来统计一下我们现在的现状。
一个组件的抽象化程度应该与其稳定性保持一致。
如何才能让一个无限稳定的组件(I=0)接受变更呢?开闭原则(OCP)为我们提供了答案。这个原则告诉我们:创造一个足够灵活、能够被扩展,而且不需要修改的类是可能的,而这正是我们所需
假设 A 指标是对组件抽象化程度的一个衡量,它的值是组件中抽象类与接口所占的比例。那么:
image.png
1.软件架构师自身需要是程序员,并且必须一直坚持做一线程序员,绝对不要听从那些说应该让软件架构师从代码中解放出来以专心解决高阶问题的伪建议
2.如果不亲身承受因系统设计而带来的麻烦,就体会不到设计不佳所带来的痛苦,接着就会逐渐迷失正确的设计方向。
这个也是常常会遇到的问题,就现在我能观察到的为例,架构师级别的基本上没有看到过再做一线的程序开发工作,仅仅是平时的各种管理,规划上的事务就已经忙的不可开交,这其实不仅仅导致了架构师本身会脱节,同时也会导致下面的同学很少有机会学习到架构师们过往的经验。
3.软件架构这项工作的实质就是规划如何将系统切分成组件,并安排好组件之间的排列关系,以及组件之间互相通信的方式。
4.设计软件架构的目的,就是为了在工作中更好地对这些组件进行研发、部署、运行以及维护。
5.如果想设计一个便于推进各项工作的系统,其策略就是要在设计中尽可能长时间地保留尽可能多的可选项。
6.设计良好的架构可以让系统便于理解、易于修改、方便维护,并且能轻松部署。「软件架构的终极目标就是最大化程序员的生产力,同时最小化系统的总运营成本。」
7.开发
8.运行
人力成本往往会比机器的成本更高,所以这也就是我们在代码编写的过程当中对可读性和性能需要有一个权衡,如果不是差异过大往往代码的可读性需要更为重要
9.维护
10.保持可选项
软件的高层策略不应该关心其底层到底使用哪一种数据库
开发的早期阶段也不应该选定使用的 Web 服务
软件的高层策略压根不应该跟这些有关。
在开发的早期阶段不应过早地采用依赖注入框架
11.**优秀的架构师会小心地将软件的高层策略与其底层实现隔离开,让高层策略与实现细节脱钩,使其策略部分完全不需要关心底层细节,当然也不会对这些细节有任何形式的依赖。**另外,「优秀的架构师所设计的策略应该允许系统尽可能地推迟与实现细节相关的决策,越晚做决策越好」
这一点其实很容易被忽略掉,因为我们经常做的工作就是细节性的工作,在进行设计的时候很容易就不自觉的假定 Web UI,MySQL 数据库这些技术选型,在这本书的最后一个章节还会讲到,这些细节。
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)》总结这种埋坑的技巧
image.png
不同的边界的跨边界调用的成本是不同的,对于服务而言跨服务调用的成本非常高,这样我们在进行服务划分的时候一定要尽量的内聚减少频繁调用的情况。
1.策略
2.层次
距离 I/O 越远的策略层次越高,也就是说我们常见的 Web UI 应该属于最低层次,我们不应该依赖 Web UI 这种输入输出设备。同时给出了组件的划分原则,变更的时间原因和层次相同的属于同一个组件。
1.业务逻辑就是程序中那些真正用于赚钱或省钱的业务逻辑与过程
2.“关键业务逻辑”是一项业务的关键部分,不管有没有自动化系统来执行这项业务,这一点是不会改变的。
3.业务实体
4.用例(usecase)
用例和业务实体应该是应用当中最重要的,所以我们的单元测试最低的要求就是要覆盖所有的 usecase 逻辑,这一部分应该保持纯净不依赖数据库,Web 等 I/O 方式
5.选择直接在数据结构中使用对业务实体对象的引用。毕竟,业务实体与请求/响应模型之间有很多相同的数据。但请一定不要这样做!这两个对象存在的意义是非常、非常不一样的。随着时间的推移,这两个对象会以不同的原因、不同的速率发生变更。
6.这些业务逻辑应该保持纯净,不要掺杂用户界面或者所使用的数据库相关的东西。在理想情况下,这部分代表业务逻辑的代码应该是整个系统的核心,其他低层概念的实现应该以插件形式接入系统中。业务逻辑应该是系统中最独立、复用性最高的代码。
再次强调了不要偷懒,今天刚好看到之前写的一个反面例子的代码,代码里面有一个 GetA 函数,从数据库当中获取A对象数据和一些统计数据,这个函数中的统计数据部分其实只有在一个 Web 页面的接口中使用到,但是为了偷懒,在其他地方查询的时候也调用了这个函数,导致最后很多地方的接口性能都由于这个没用的统计数据多耗费了将近 1s 的时间。
1.架构设计的主题
2.架构设计的核心目标
3.可测试的架构设计
4.一个系统的架构应该着重于展示系统本身的设计,而并非该系统所使用的框架
用例是架构设计当中最应该关注的部分,框架数据库Web服务的选择都是细节,这些细节应该延后选择,我们的用例不应该依赖这些细节,这样才能很好的测试
1.按照不同关注点对软件进行切割。也就是说,这些架构都会将软件切割成不同的层,至少有一层是只包含该软件的业务逻辑的,而用户接口、系统接口则属于其他层。
2.特点
image.png
1.依赖关系规则
2.业务实体
3.用例
4.接口适配器
5.层次越往内,其抽象和策略的层次越高,同时软件的抽象程度就越高,其包含的高层策略就越多。最内层的圆中包含的是最通用、最高层的策略,最外层的圆包含的是最具体的实现细节。
6.这里最重要的是这个跨边界传输的对象应该有一个独立、简单的数据结构。
7.「不要投机取巧地直接传递业务实体或数据库记录对象。」
看过前面的部分再来看整洁架构这一章节会发现非常的自然
1.谦卑对象模式
2.展示器与视图
3.数据库网关
4.数据映射器(ORM)
这样的 ORM 系统应该属于系统架构中的哪一层呢?当然是数据库层。ORM 其实就是在数据库和数据库网关接口之间构建了另一种谦卑对象的边界。
5.因为跨边界的通信肯定需要用到某种简单的数据结构,而边界会自然而然地将系统分割成难以测试的部分与容易测试的部分,所以通过在系统的边界处运用谦卑对象模式,我们可以大幅地提高整个系统的可测试性。
这里主要是将很难进行单元测试的行为和容易测试的行为进行分离,很难被测试的行为常常会被分离成为一个谦卑对象,这个对象非常的简单,不会包含很多逻辑
1.构建不完全边界的方式
架构是需要取舍的,我们不可能每一项都做的很完美,边界的划分也是这样,所以就有了不完全的边界
1.「过度的工程设计往往比工程设计不足还要糟糕」
2.现实就是这样。作为软件架构师,我们必须有一点未卜先知的能力。有时候要依靠猜测——当然还要用点脑子。软件架构师必须仔细权衡成本,决定哪里需要设计架构边界,以及这些地方需要的是完整的边界,还是不完全的边界,还是可以忽略的边界
3.架构师必须持续观察系统的演进,时刻注意哪里可能需要设计边界,然后仔细观察这些地方会由于不存在边界而出现哪些问题。
不要过度优化,但是也不要什么都不管的一把梭,架构师需要演进和取舍的,没有完美的架构只有不断持续演进优化的架构。
main 是一个程序的入口,这是最细节的部分,因为之前为了很多东西不被依赖,我们一般会采用接口来实现依赖反转,这时候就会导致我们所有的依赖关系的构建都需要在 main 中进行完成,所以一般而言我们会在 main 中引入依赖注入框架。
1.所谓的服务本身只是一种比函数调用方式成本稍高的,分割应用程序行为的一种形式,与系统架构无关。
2.服务所带来的好处?
3.横跨型变更(cross-cutting concern)问题,它是所有的软件系统都要面对的问题,无论服务化还是非服务化的。
4.服务也可以按照 SOLID 原则来设计,按照组件结构来部署,这样就可以做到在添加/删除组件时不影响服务中的其他组件。
5.系统的架构边界事实上并不落在服务之间,而是穿透所有服务,在服务内部以组件的形式存在
6.「服务边界并不能代表系统的架构边界,服务内部的组件边界才是。」
7.系统的架构是由系统内部的架构边界,以及边界之间的依赖关系所定义的,与系统中各组件之间的调用和通信方式无关。
虽然现在微服务架构非常火热,基本上所有的服务都是拆分了服务,但是拆分了服务并不一定表示就解耦合了,也并不一定就真的能独立部署,想一想这是现在很常见的,一个应用必须要和另外一个应用一同上线,根本做不了独立部署。
不变的组件不要依赖多变的东西,这样会导致非常难以测试
1.虽然软件本身并不会随时间推移而磨损,但硬件及其固件却会随时间推移而过时,随即也需要对软件做相应改动
2.虽然软件质量本身并不会随时间推移而损耗,但是未妥善管理的硬件依赖和固件依赖却是软件的头号杀手。
3.但如果你在代码中嵌入了 SQL 或者是代码中引入了对某个平台的依赖的话,其实就是在写固件代码。
4.软件构建过程中的三个阶段
5.整洁的嵌入式架构就是可测试的嵌入式架构
6.软件与固件集成在一起也属于设计上的反模式(anti-pattern)
软件并不会随着时间磨损但是硬件是会过时的,而且换的还非常频繁,这时候我们就必须要把硬件以及固件代码给隔离起来,对了不要认为我们不做嵌入式开发平时就很少接触到这个,SQL 语句其实也是一种固件代码
数据很重要,但是数据库系统是一个细节,书上这一章用了一个例子说明有时候可能真的用不到数据库。换个常见的例子,我们可能系统刚开始的时候使用 SQlite 就可以,随着业务发展用上了 MySQL,然后随着并发的提高又会引入缓存组件,这些变化其实和业务逻辑都没有关系,数据库的变化是不应该影响到业务逻辑的
框架的选择要慎重,我们业务逻辑本身不能依赖框架
这一步看起来简单,但是非常考验一个人的功力
image.png
这一章对比了四种架构风格,同时提出了,架构设计是需要考虑实现细节的,设计需要映射到代码结构和代码树上,这个其实和最开始的“软件架构师自身需要是程序员,并且必须一直坚持做一线程序员”交相呼应。如果可以在编译时解决的问题,就不要放到运行时,编译的问题往往要比运行时的问题好解决,这也是为什么 Go 的依赖注入框架我更加推荐 wire 的原因,同理作者提出了 如果要防止直接中 web控制器调用数据层,那么我们就不应该将数据层(repo)暴露出来,只需要暴露 usecase 就好了。
之前其实也大概了解过整洁架构,从最开始觉得它又臭又长,到现在工作两三年后觉得“不听老人言,吃亏在眼前”,当我们在对一个架构或者是事务进行批判的时候一定要了解它面对的场景以及它的理念,这是最重要的。当然软件领域是没有银弹的,我们需要做的是吸收每一种思想,在不同的场景下做不同的取舍,接下来会有几篇文章结合毛老师课上讲的 Go 工程化相关的内容,以及我在工作当中进行的一些总结最后提出一种当下我觉得的 Go 项目的组织方式,这种方式不是最好的,但是我觉得是现阶段最适合的。推荐大家在仔细的阅读一下本书,期望你能有更多的收获。
参考文献架构整洁之道-罗伯特·C·马丁-微信读书
文章来源博客地址: https://lailin.xyz
文章标题:Go工程化(一)架构整洁之道阅读笔记
标题URL:http://www.stwzsj.com/qtweb/news11/11361.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联