头图

前言

大家好,我是春风,最近需求进入一段空窗期,闲暇之余,我便从公司图书角翻出一本Bob大神的《架构整洁之道》,虽然才看到一半,但其中的收获已经不得不让我想停下来先总结记录一下了,这应该是架构设计领域最好的一本启蒙书了吧!

第一部分:概述

第一部分就是回答一个问题:我们架构设计的终极目标是什么?

作者首先给出了衡量一个软件系统价值的两个维度:

  • 行为价值(让机器按照我们给出的指令运转,从而给使用者提供价值)
  • 架构价值(软件必须保持灵活,面对复杂的情形,可以最低成本的完成变更)
    而这两种价值究竟哪种更重要呢?作者引用艾森豪威尔矩阵:

行为价值-我们当下的第一要务是满足客户需求,所以它是紧急而不重要

架构价值-当下站在业务方的角度来说,不紧急,但是为了能应对后续的变更,无疑是非常重要

所以,我们架构的目标应该是创造架构价值,即用最小的人力成本来满足构建和维护系统的需求(尤其是最小人力成本维护,这里的维护我理解就是适应变更)

第二部分:编程范式-代码层面

本书的第二部分到第五部分其实是自下而上的说明该怎样设计一个好的架构。第二部分是代码层面;
第三部分是组件内部如何构建;第四部分是组件外部之间如何关联;第五部分是软件系统层面。

第二部分从代码层面给出了三大编程范式,其实就是限制我们不能做哪些内容

1. 结构化编程

我们的软件系统需要根据功能来做降解拆分,大型系统可以拆分成模块和组件,在这些模块和组件中,又可以进一步拆分成更小的函数,

结构化编程在代码层面就更像是我们函数的编写规范,任何一个复杂的函数结构都是顺序结构和分支结构、循环结构的集合,同时这三种基础结构是可被证明的,所以任何复杂的函数也就是可推导的

这里有个特殊的反例就是goto,goto能跳脱这三种结,所以为了我们整个函数是可推导的,一般都会放弃使用goto这种结构。

结构化编程的价值就是通过功能性降解拆分,使得我们拥有了创造可推导程序单元的能力,进而编写可推导程序的能力

2. 面向对象编程

面向对象是将程序的控制权间接转移给了对象
面向对象有三大特性:封装继承多态,这也是我们Java最老生常谈的面试题了

  • 封装:采用封装特性,我们可以把一组相关联的数据和函数圈起来,使圈外面的代码只能看见部分函数(为什么是部分封装?c语言其实就是完全封装,因此也恰恰削弱了面向对象的完美性),数据则完全不可见,函数封装可以让调用者不用关心函数的实现过程。
  • 继承:让我们可以在某个作用域内对外部定义的某一组变量和函数进行覆盖。继承是相比于非面向对象语言,提供了向上转型的便捷。
  • 多态:父类的方法被子类重写时,可以各自产生自己的功能行为。多态其实是函数指针的一种应用,但是消除了函数指针的危险性。多态的好处是当程序要增加一种范畴内的实现时,只需要重写方法增加其自己的实现即可。

    在我们使用多态前,函数的调用方都必须引用被调用方所在的模块,使用多态后,调用方可以通过抽象接口调用,而不再直接依赖被调用方,这种控制权的反转就是依赖反转

    这也是面向对象最大的好处,依赖反转不仅在函数调用中作用突出,后面要讲的组件依赖上也是功能强大,正是组件之间不用直接依赖给组件提供了可独立部署的能力

面向对象就是以多态的手段来对源代码中的依赖关系进行控制,这种控制能力可以让我们构建出各种插件或架构,让高层策略性组价与底层实现组件相分离,底层组件也因此具有了独立部署和独立开发的能力

3. 函数式编程

区别于按功能性拆解的结构化,我们需要从可变性的角度来做程序的隔离。即将程序划分为可变组件和不可变组件

为什么要做可变性的隔离?

因为我们程序的所有竞争,像Java的死锁、并发更新的安全问题都是修改可变变量导致的,可变性隔离可以分离出这些竞争状态,从而进一步避免并发的安全问题。

这里把不可变组件理解为我们的业务代码层(service层)可变组件理解为我们的数据库层,所以我们一直都是在遵守这种编程范式,将大部分业务逻辑归于不可变组件中,而为了能避免可变变量的竞争状态的发生,我们通常采用事务型内存(数据库的实现)或者重试机制

可变变不可变

除了这两种方式,书中也有一个有趣的保护可变变量的方式,就是将可变性变成不可变。比如我们在银行存取款,每次存取款都需要将余额加一减一,这个余额就是一个可变变量,但是如果内存足够大,我们就可以不去每次修改余额,而只保留存取款记录,当要查询余额的时候,再将所有的存取款记录做一遍计算。

这样余额这个可变变量也就成了不可变,不存在更新,也就不存在并发问题。但这无疑对内存和性能是一项很大的挑战。这个的权衡关系让我想到了著名的CAP理论

第三部分:设计原则-如何构建一个组件,即我们模块级编程的五大原则:SOLID

首先说明我们模块级编程(中层结构)的三个主要目标是:

  1. 使软件可容忍被改动;
  2. 使软件更容易被理解;
  3. 构建可在多个软件系统中复用的组件

为了实现这些目标,构建一个组件的时候,便需要我们遵循SOLID原则:SOLID也是这五大原则的首字母缩写

SRP:单一职责原则

任何一个软件模块都应该只对某一类行为负责。这里的软件模块可以理解为就是一组紧密相关的函数和数据,即我们Java中的类。所以SRP也更像是规定类的划分。定义中对其负责的某一类行为也就是我们这个模块有且只有的一个被改变的理由

违反SRP的反例:

OCP:开闭原则

对扩展开放,对修改关闭

开闭原则通常采用架构分层(MVC) + 依赖反转的方式来实现。使得高阶组件不会因为低阶组件的修改而受到影响,从而使得只用扩展低阶组件就可以满足更多的业务功能。

LSP:里氏替换原则

通过继承父类与子类的替换)或者接口与实现类的替换来实现,这也是方便扩展一个方式。如果违反了LSP,系统将不得不为此增加大量的应对变化的机制。

ISP:接口隔离原则

前面单一职责原则和开闭原则都是讲如何将一组程序划分到一起,但是任何层次的软件设计如果依赖了它并不需要的东西,就会带来不必要的麻烦。

所以ISP就是分离出那些不需要的依赖,通常采用的方式是将原来需要直接依赖的某个类改为依赖某个分离的接口

DIP:依赖反转

在源代码层次的依赖关系中,我们应该多用抽象类型,而非具体实现

我们每次修改接口的时候,一定会去修改具体实现,但是修改具体实现的时候,却很少需要修改接口,所以我们也可以认为接口比实现更稳定,而依赖关系中,我们便也应该依赖这种更稳定的结构来减少应对变更带来的修改难度。

为追求这种架构上的稳定性,在编码层面可以归结为下面这几条具体的编码守则:

  1. 应在代码中多使用抽象接口,尽量避免使用那些多变的具体实现
    包括对象的创建过程,因为所有面向对象的语言中,创建对象都免不了要在源代码层次上依赖该对象的具体实现,所以为了避免依赖具体实现,我们会采用抽象工厂模式来创建,这也是spring创建bean的模式。
  2. 不要在具体实现类上创建衍生类,就是我们常说的少用继承,多用组合。
    继承关系是一切源代码依赖关系中最强的、最难被修改的。
  3. 不要企图通过覆盖包含具体实现的函数,来扩展功能.
    因为即使覆盖,我们也无法消除对具体实现的依赖,唯一的方法就是创建一个新的抽象类。
  4. 应避免在代码中写入与任何具体实现相关的名字,或者其他容易变动的事物的名字。
DIP不能完全消除对具体实现的依赖,当避免不了违反DIP的时候,我们可以将这部分进行隔离。
图中跨越边界、朝向抽象层的依赖关系则会成为一个设计守则:依赖守则

抽象工厂模式:

第四部分:组件原则-组件之间如何划分、关联

SOLID是指导我们如何将砖块气成墙与房间,那组件原则就是指导我们如何将这些房间组合成房子。

程序的墨菲定律+摩尔定律使得我们有开发插件化架构的必要和可能。

首先再定义一下组件这个概念:可以独立部署的最小实体。也是一份可重定位的二进制文件。像Java中一个jar包。
这里理解可重定位就是可以知道并修改我们加载依赖的位置。

构建组件的基本原则:

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

复用/等同原则:

软件复用的最小粒度应等同于其发布的最小粒度

我们最常用的模块管理工具:maven,管理了我们要依赖的所有组件,而这些引入的每一个独立组件都是可以单独发布的,也是可以在任何系统单独引用的。

其中每一个依赖的组件都有一个共同的主题,而且都有自己的版本管理,正因为有版本管理,我们才可以在依赖时,有针对的选择,避免依赖的版本更新对原先调用方造成影响。

共同闭包原则:

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

这也是单一职责原则在组件层面上的再度阐述。变更体现在同一个组件,其它组件就不用再重新部署,所以我们要将某一类变更所涉及的所有类尽量聚合到一起。

共同复用原则:

不要强迫一个组件的用户依赖他们不需要的东西,所以一个组件中,不是紧密相连的类不应该放在一起,这也是接口隔离原则在组件层面的体现

这里总结一下,三原则并不是都要完全的遵循,也不可能都完全遵循,而是在软件开发的过程中,侧重点不断的调整。比如在早期,组件的划分更加倾向于业务功能,而预想不到太多的复用场景,所以CCP比REP更重要。

前面三原则规范了组件的划分,那组件之间的相互关联耦合同样也需要遵循下面这四个原则:

无依赖环原则:

即不应该出现循环依赖

前面介绍过,为了依赖者不受到被依赖者修改的被动影响,我们通常的做法是版本管理,让依赖者自行决定依赖内容,但这个前提是不能出现循环依赖。
而当出现了循环依赖的情况,我们就要使用

  • 依赖反转,让依赖者依赖抽象接口
  • 创建新的组件,切分开循环依赖的部分
    这两种方式来打破循环依赖。

自下而上的设计原则:

你会发现我们本书讲解的架构顺序也就是这样自下而上的。

从源代码层面一直到组件到系统,所以组件结构图必须随着软件系统的变化而变化和扩张,而不可能在系统构建的最初就被完美设计出来。

稳定依赖原则:

依赖关系必须趋向于更稳定的方向。因为稳定性与变更难度有关。每个组件都有一个可计算的稳定指标,即 出向依赖占所有依赖的占比,稳定依赖原则告诉我们出向依赖应该尽可能的少,但也并不是所有组件都应该是稳定的,一般的高层架构反而是不稳定的,越底层越应该稳定。即底层应该是最多被依赖的,而最小依赖他人的。

稳定抽象原则:

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

具体说就是稳定的组件同时也应该是抽象的。这样它的稳定性就不会影响到它的扩展;而相反,不稳定的组件反而应该包含具体的实现代码,这样它的不稳定性就可以通过具体的代码被轻易的修改。

组件的稳定性与抽象化程度应该是在下图中一个主序列附近。

以上就是我阅读《架构整洁之道1-14章内容的对架构设计知识方面的收获,另外也有一些其他的感悟,与君共勉:

  1. 读书有时会比通过百度学习能理解的更深,很多优秀的博客会帮我们总结出每一个知识点,但往往没有书中作者那样会给你描述出整个知识的背景以及来龙去脉。

    就像我这篇读书笔记一样,也都是我自己消化后再整理出的内容,其中不乏我自己的主观描述,我自己能理解,但同样的话在你心里就不一定能表达出同一个意思。

    而且我们平常接触的知识点太过零碎,就像我们学习maven,学习它的每一个命令,但却从来没想过,为什么要用maven这种方式来管理依赖,为什么要设计出版本?

  1. 及时输出才能加深自己对知识的理解,或者说一个知识只有你能用自己的话输出出来,才能证明这个知识你是真正的掌握了。
  2. 写博客什么时候都不晚。一定要写起来
    所以这也是为什么我做了这么多年程序员,却是发布第一篇文章的原因。但从现在开始,我会坚持输出。我们共同成长

我是春风,春风和煦,不负归期。公众号:程序员春风
最后的最后,码字不易,能看到这里,求个赞


李牧云
1 声望1 粉丝

理想主义程序员。春风和煦,不负归期!