做外汇看什么网站,网站建设培训 上海,newcard wordpress,营销型网站建设网站建设制作程序的世界飞速发展#xff0c;今天所掌握的技能可能明年就过时了#xff0c;但有些知识历久弥新#xff0c;掌握了它们#xff0c;你在程序的海洋中就不会迷路#xff0c;架构思想就是这样的知识。 本文是《架构整洁之道》的读书心得#xff0c;作者将书中内容拆解后再组… 程序的世界飞速发展今天所掌握的技能可能明年就过时了但有些知识历久弥新掌握了它们你在程序的海洋中就不会迷路架构思想就是这样的知识。 本文是《架构整洁之道》的读书心得作者将书中内容拆解后再组织不仅加入了个人的独到见解而且用一张详细的知识脉络图帮助大家了解整本书的精华。如果你读过这本书可以将本文当做一次思想交流如果你还没看过这本书更要阅读这篇文章相信你会得到不同于以往的启发。 本篇文章我们将从软件系统的价值出发首先认识架构工作的价值和目标 接下来依次了解架构设计的基础、指导思想设计原则、组件拆分的方法和粒度、组件之间依赖设计、组件边界多种解耦方式以及取舍、降低组件之间通信成本的方法从而最终指导我们做出正确的架构决策和架构设计。
一、软件系统的价值
架构是软件系统的一部分所以要明白架构的价值首先要明确软件系统的价值。软件系统的价值有两方面行为价值和架构价值。
行为价值是软件的核心价值包括需求的实现以及可用性保障功能性 bug 、性能、稳定性。这几乎占据了我们90%的工作内容支撑业务先赢是我们工程师的首要责任。如果业务是明确的、稳定的架构的价值就可以忽略不计但业务通常是不明确的、飞速发展的这时架构就无比重要因为架构的价值就是让我们的软件Software更软Soft。可以从两方面理解
当需求变更时所需的软件变更必须简单方便。变更实施的难度应该和变更的范畴scope成等比而与变更的具体形状shape无关。
当我们只关注行为价值不关注架构价值时会发生什么事情这是书中记录的一个真实案例随着版本迭代工程师团队的规模持续增长但总代码行数却趋于稳定相对应的每行代码的变更成本升高、工程师的生产效率降低。从老板的视角就是公司的成本增长迅猛如果营收跟不上就要开始赔钱啦。 可见架构价值重要性接下来从著名的紧急重要矩阵出发看我们如何处理好行为价值和架构价值的关系。
重要紧急矩阵中做事的顺序是这样的1.重要且紧急 2.重要不紧急 3.不重要但紧急 4.不重要且不紧急。实现行为价值的需求通常是 PD 提出的都比较紧急但并不总是特别重要架构价值的工作内容通常是开发同学提出的都很重要但基本不是很紧急短期内不做也死不了。所以行为价值的事情落在1和3重要且紧急、不重要但紧急而架构价值落在2重要不紧急。我们开发同学在低头敲代码之前一定要把杂糅在一起的1和3分开把我们架构工作插进去。
二、架构工作的目标
前面讲解了架构价值追求架构价值就是架构工作的目标说白了就是用最少的人力成本满足构建和维护该系统的需求再细致一些就是支撑软件系统的全生命周期让系统便于理解、易于修改、方便维护、轻松部署。对于生命周期里的每个环节优秀的架构都有不同的追求
开发阶段组件不要使用大量复杂的脚手架不同团队负责不同的组件避免不必要的协作。部署阶段部署工作不要依赖成堆的脚本和配置文件组件越多部署工作越繁重而部署工作本身是没有价值的做的越少越好所以要减少组件数量。运行阶段架构设计要考虑到不同的吞吐量、不同的响应时长要求架构应起到揭示系统运行的作用用例、功能、行为设置应该都是对开发者可见的一级实体以类、函数或模块的形式占据明显位置命名能清晰地描述对应的功能。维护阶段减少探秘成本和风险。探秘成本是对现有软件系统的挖掘工作确定新功能或修复问题的最佳位置和方式。风险是做改动时可能衍生出新的问题。
三、编程范式
其实所谓架构就是限制限制源码放在哪里、限制依赖、限制通信的方式但这些限制比较上层。编程范式是最基础的限制它限制我们的控制流和数据流结构化编程限制了控制权的直接转移面向对象编程限制了控制权的间接转移函数式编程限制了赋值相信你看到这里一定一脸懵逼啥叫控制权的直接转移啥叫控制权的间接转移不要着急后边详细讲解。
这三个编程范式最近的一个也有半个世纪的历史了半个世纪以来没有提出新的编程范式以后可能也不会了。因为编程范式的意义在于限制限制了控制权转移限制了数据赋值其他也没啥可限制的了。很有意思的是这三个编程范式提出的时间顺序可能与大家的直觉相反从前到后的顺序为函数式编程1936年、面向对象编程1966年、结构化编程1968年。
1.结构化编程
结构化编程证明了人们可以用顺序结构、分支结构、循环结构这三种结构构造出任何程序并限制了 goto 的使用。遵守结构化编程工程师就可以像数学家一样对自己的程序进行推理证明用代码将一些已证明可用的结构串联起来只要自行证明这些额外代码是确定的就可以推导出整个程序的正确性。
前面提到结构化编程对控制权的直接转移进行了限制其实就是限制了 goto 语句。什么叫做控制权的直接转移就是函数调用或者 goto 语句代码在原来的流程里不继续执行了转而去执行别的代码并且你指明了执行什么代码。为什么要限制 goto 语句因为 goto 语句的一些用法会导致某个模块无法被递归拆分成更小的、可证明的单元。而采用分解法将大型问题拆分正是结构化编程的核心价值。
其实遵守结构化编程工程师们也无法像数学家那样证明自己的程序是正确的只能像物理学家一样说自己的程序暂时没被证伪没被找到bug。数学公式和物理公式的最大区别就是数学公式可被证明而物理公式无法被证明只要目前的实验数据没把它证伪我们就认为它是正确的。程序也是一样所有的 test case 都通过了没发现问题我们就认为这段程序是正确的。
2.面向对象编程
面向对象编程包括封装、继承和多态从架构的角度这里只关注多态。多态让我们更方便、安全地通过函数调用的方式进行组件间通信它也是依赖反转让依赖与控制流方向相反的基础。
在非面向对象的编程语言中我们如何在互相解耦的组件间实现函数调用答案是函数指针。比如采用C语言编写的操作系统中定义了如下的结构体来解耦具体的IO设备 IO 设备的驱动程序只需要把函数指针指到自己的实现就可以了。
struct FILE {void (*open)(char* name, int mode);void (*close)();int (*read)();void (*write)(char);void (*seek)(long index, int mode);
}
这种通过函数指针进行组件间通信的方式非常脆弱工程师必须严格按照约定初始化函数指针并严格地按照约定来调用这些指针只要一个人没有遵守约定整个程序都会产生极其难以跟踪和消除的 Bug。所以面向对象编程限制了函数指针的使用通过接口-实现、抽象类-继承等多态的方式来替代。
前面提到面向对象编程对控制权的间接转移进行了限制其实就是限制了函数指针的使用。什么叫做控制权的间接转移就是代码在原来的流程里不继续执行了转而去执行别的代码但具体执行了啥代码你也不知道你只调了个函数指针或者接口。
3.函数式编程
函数式编程有很多种定义很多种特性这里从架构的角度只关注它的没有副作用和不修改状态。函数式编程中函数要保持独立所有功能就是返回一个新的值没有其他行为尤其是不得修改外部变量的值。前面提到函数式编程对赋值进行了限制指的就是这个特性。
在架构领域所有的竞争问题、死锁问题、并发问题都是由可变变量导致的。如果有足够大的存储量和计算量应用程序可以用事件溯源的方式用完全不可变的函数式编程只通过事务记录从头计算状态就避免了前面提到的几个问题。目前要让一个软件系统完全没有可变变量是不现实的但是我们可以通过将需要修改状态的部分和不需要修改的部分分隔成单独的组件在不需要修改状态的组件中使用函数式编程提高系统的稳定性和效率。
综上没有结构化编程程序就无法从一块块可证伪的逻辑搭建没有面向对象编程跨越组件边界会是一个非常麻烦而危险的过程而函数式编程让组件更加高效而稳定。没有编程范式架构设计将无从谈起。
四、设计原则
和编程范式相比设计原则和架构的关系更加紧密设计原则就是架构设计的指导思想它指导我们如何将数据和函数组织成类如何将类链接起来成为组件和程序。反向来说架构的主要工作就是将软件拆解为组件设计原则指导我们如何拆解、拆解的粒度、组件间依赖的方向、组件解耦的方式等。
设计原则有很多我们进行架构设计的主导原则是 OCP开闭原则在类和代码的层级上有SRP单一职责原则、LSP里氏替换原则、ISP接口隔离原则、DIP依赖反转原则在组件的层级上有REP复用、发布等同原则、 CCP共同闭包原则、CRP共同复用原则处理组件依赖问题的三原则无依赖环原则、稳定依赖原则、稳定抽象原则。
1.OCP开闭原则
设计良好的软件应该易于扩展同时抗拒修改。这是我们进行架构设计的主导原则其他的原则都为这条原则服务。
2.SRP单一职责原则
任何一个软件模块都应该有且只有一个被修改的原因“被修改的原因“指系统的用户或所有者翻译一下就是任何模块只对一个用户的价值负责。该原则指导我们如何拆分组件。
举个例子CTO 和 COO 都要统计员工的工时当前他们要求的统计方式可能是相同的我们复用一套代码这时 COO 说周末的工时统计要乘以二按照这个需求修改完代码CTO 可能就要过来骂街了。当然这是个非常浅显的例子实际项目中也有很多代码服务于多个价值主体这带来很大的探秘成本和修改风险。
另外当一份代码有多个所有者时就会产生代码合并冲突的问题。
3.LSP里氏替换原则
当用同一接口的不同实现互相替换时系统的行为应该保持不变。该原则指导的是接口与其实现方式。
你一定很疑惑实现了同一个接口他们的行为也肯定是一致的呀还真不一定。假设认为矩形的系统行为是面积宽*高让正方形实现矩形的接口在调用 setW 和 setH 时正方形做的其实是同一个事情设置它的边长。这时下边的单元测试用矩形能通过用正方形就不行实现同样的接口但是系统行为变了这是违反 LSP 的经典案例。
r.setW(5);
r.setH(2);
assert(r.area() 10);
4.ISP接口隔离原则
不依赖任何不需要的方法、类或组件。该原则指导我们的接口设计。
当我们依赖一个接口但只用到了其中的部分方法时其实我们已经依赖了不需要的方法或类当这些方法或类有变更时会引起我们类的重新编译或者引起我们组件的重新部署这些都是不必要的。所以我们最好定义个小接口把用到的方法拆出来。
5.DIP依赖反转原则
跨越组建边界的依赖方向永远与控制流的方向相反。该原则指导我们设计组件间依赖的方向。
依赖反转原则是个可操作性非常强的原则当你要修改组件间的依赖方向时将需要进行组件间通信的类抽象为接口接口放在边界的哪边依赖就指向哪边。
6.REP复用、发布等同原则
软件复用的最小粒度应等同于其发布的最小粒度。直白地说就是要复用一段代码就把它抽成组件。该原则指导我们组件拆分的粒度。
7.CCP共同闭包原则
为了相同目的而同时修改的类应该放在同一个组件中。CCP 原则是 SRP 原则在组件层面的描述。该原则指导我们组件拆分的粒度。
对大部分应用程序而言可维护性的重要性远远大于可复用性由同一个原因引起的代码修改最好在同一个组件中如果分散在多个组件中那么开发、提交、部署的成本都会上升。
8.CRP共同复用原则
不要强迫一个组件依赖它不需要的东西。CRP 原则是 ISP 原则在组件层面的描述。该原则指导我们组件拆分的粒度。
相信你一定有这种经历集成了组件A但组件A依赖了组件B、C。即使组件B、C 你完全用不到也不得不集成进来。这是因为你只用到了组件A的部分能力组件A中额外的能力带来了额外的依赖。如果遵循共同复用原则你需要把A拆分只保留你要用的部分。
REP、CCP、CRP 三个原则之间存在彼此竞争的关系REP 和 CCP 是黏合性原则它们会让组件变得更大而 CRP 原则是排除性原则它会让组件变小。遵守REP、CCP 而忽略 CRP 就会依赖了太多没有用到的组件和类而这些组件或类的变动会导致你自己的组件进行太多不必要的发布遵守 REP 、CRP 而忽略 CCP因为组件拆分的太细了一个需求变更可能要改n个组件带来的成本也是巨大的。 优秀的架构师应该能在上述三角形张力区域中定位一个最适合目前研发团队状态的位置例如在项目早期CCP比REP更重要随着项目的发展这个最合适的位置也要不停调整。
9.无依赖环原则
健康的依赖应该是个有向无环图DAG互相依赖的组件实际上组成了一个大组件这些组件要一起发布、一起做单元测试。我们可以通过依赖反转原则 DIP 来解除依赖环。
10.稳定依赖原则
依赖必须要指向更稳定的方向。
这里组件的稳定性指的是它的变更成本和它变更的频繁度没有直接的关联变更的频繁程度与需求的稳定性更加相关。影响组件的变更成本的因素有很多比如组件的代码量大小、复杂度、清晰度等等最最重要的因素是依赖它的组件数量让组件难以修改的一个最直接的办法就是让很多其他组件依赖于它
组件稳定性的定量化衡量指标是不稳定性I 出向依赖数量 / (入向依赖数量 出向依赖数量)。如果发现违反稳定依赖原则的地方解决的办法也是通过 DIP 来反转依赖。
11.稳定抽象原则
一个组件的抽象化程度应该与其稳定性保持一致。为了防止高阶架构设计和高阶策略难以修改通常抽象出稳定的接口、抽象类为单独的组件让具体实现的组件依赖于接口组件这样它的稳定性就不会影响它的扩展性。
组件抽象化程度的定量化描述是抽象程度A 组件中抽象类和接口的数量 / 组件中类的数量。
将不稳定性I作为横轴抽象程度A作为纵轴那么最稳定、只包含抽象类和接口的组件应该位于左上角01最不稳定、只包含具体实现类没有任何接口的组件应该位于右下角10他们连线就是主序列线位于线上的组件他们的稳定性和抽象程度相匹配是设计良好的组件。位于00周围区域的组件它们是非常稳定注意这里的稳定指的是变更成本并且非常具体的组件因为他们的抽象程度低决定了他们经常改动的命运但是又有许多其他组件依赖他们改起来非常痛苦所以这个区域叫做痛苦区。右上角区域的组件没有其他组件依赖他们他们自身的抽象程度又很高很有可能是陈年的老代码所以这个区域叫做无用区。 另外可以用点距离主序列线的距离 Z 来表示组件是否遵循稳定抽象原则Z 越大表示组件越违背稳定依赖原则。
五、架构工作的基本方针
了解了编程范式和设计原则接下来我们看看如何应用他们拆分组件、处理组件依赖和组件边界。架构工作有两个方针
尽可能长时间地保留尽可能多的可选项。这里的可选项指的是无关紧要的细节设计比如具体选用哪个存储方式、哪种数据库或者采用哪种 Web 框架。业务代码要和这些可选项解耦数据库或者框架应该做到像插件一样切换业务层对这个切换的过程应该做到完全无感。
低层次解耦方式能解决的不要用高层次解耦方式。组件之间的解耦方式后边细讲这里强调的是边界处理越完善开发和部署成本越高。所以不完全边界能解决的不要用完全边界低层次解耦能解决的不要用高层次解耦。
六、组件拆分
首先要给组件下个定义组件是一组描述如何将输入转化为输出的策略语句的集合在同一个组件中策略的变更原因、时间、层次相同。
从定义就可以看出组件拆分需要在两个维度进行按层次拆分、按变更原因拆分。
这里的变更原因就是业务用例按变更原因进行组件拆分的例子是订单组件、聊天组件。按层次拆分可以拆为业务实体、用例、接口适配器、框架与驱动程序。
业务实体关键业务数据和业务逻辑的集合与界面无关、与存储无关、与框架无关只有业务逻辑。用例特定场景下的业务逻辑可以理解为 输入 业务实体 输出 用例。接口适配器包含整个整个MVC以及对存储、设备、界面等的接口声明和使用。
一条策略距离系统的输入、输出越远它的层次越高所以业务实体是最高的层框架与驱动程序是最低的层。
七、组件依赖处理
前面拆好了组件分好了层依赖就很好处理了依赖关系与数据流控制流脱钩而与组件所在层次挂钩始终从低层次指向高层次如下图。越具体的策略处在的层级越低越插件化。切换数据库是框架驱动层的事情接口适配器完全无感知切换展示器是接口适配器层面的事情用例完全无感知而切换用例也不会影响到业务实体。 八、组件边界处理
一个完整的组件边界包括哪些内容首先跨越组件边界进行通信的两个类都要抽象为接口另外需要声明专用的输入数据模型、声明专用的返回数据模型想一想每次进行通信时都要进行的数据模型转换就能理解维护一个组件边界的成本有多高。
除非必要我们应该尽量使用不完全边界来降低维护组件边界的成本。不完全边界有三种方式
省掉最后一步声明好接口做好分割后仍然放在一个组件中等到时机成熟时再拆出来独立编译部署。单向边界正常的边际至少有两个接口分别抽象调用方和被调用方。这里只定义一个接口高层次组件用接口调用低层次组件而低层次组件直接引用高层次组件的类。门户模式控制权的间接转移不用接口和实现去做而是用门户类去做用这种方式连接口都不用声明了。
除了完全边界和不完全边界的区分边界的解耦方式也可以分为3个层次
源码层次做了接口、类依赖上的解耦但是放在同一个组件中通常放在不同的路径下。和不完全边界的省略最后一步一样。部署层次拆分为可以独立部署的不同组件比如 iOS 的静态库、动态库真正运行时处于同一台物理机器上组件之间通常通过函数调用通讯。服务层次运行在不同的机器上通过 url 、网络数据包等方式进行通讯。
从上到下开发、部署成本依次升高如果低层次的解耦已经满足需要不要进行高层次的解耦。
原文链接 本文为云栖社区原创内容未经允许不得转载。