前言大家好,我是春风,最近需要进入空缺期。大神《架构整洁之道》,虽然只看了一半,但是不得不停下来记录下自己的所见所闻。这应该是建筑设计领域最好的启蒙书!第一部分:概述第一部分是回答一个问题:我们架构设计的最终目标是什么?作者首先给出了两个衡量软件系统价值的维度:行为价值(让机器按照我们给的指令运行,为用户提供价值)架构价值(软件必须保持灵活性,可以在复杂的情况下使用。至少完成变更的成本)以及这两个值中哪个更重要?作者引用艾森豪威尔矩阵:行为价值——我们的当务之急是满足客户需求,所以紧急但不重要;架构价值——目前从业务方的角度来看,并不紧迫,但为了能够应对后续的变化,无疑是非常重要的。所以我们架构的目标应该是创造架构价值,即用最小的人工成本来满足构建和维护系统的需要(特别是最小人工成本维护,我理解这里的维护是为了适应变化)第二部分:编程范式——代码层面本书的第二部分到第五部分实际上是对如何设计一个好的架构的自下而上的解释。第二部分是代码层面;第三部分是组件内部是如何构建的;第四部分是外部组件是如何关联的;第五部分是软件系统层面。第二部分从代码层面给出了三大编程范式,其实就是限制我们不能做什么。1、结构化编程我们的软件系统需要按照功能进行降级拆分。大型系统可以拆分成模块和组件,而这些模块和组件又可以进一步拆分成更小的功能。结构化编程在代码层面更像是我们的函数编写规范。任何复杂的函数结构都是顺序结构、分支结构和循环结构的集合。同时这三个基本结构都是可证明的,所以任何复杂的函数都是Derivable。这里有一个特殊的反例是goto,goto可以逃脱这三种knot,所以为了我们整个函数的可导性,我们一般放弃使用goto结构。结构化编程的价值在于,通过功能降级和拆分,我们有能力创建可推导的程序单元,进而编写可推导的程序。2、面向对象编程面向对象就是将程序的控制权间接地转移给对象。面向对象具有三大特点:封装、继承、多态。圈出一组相关的数据和函数,这样圈外的代码只能看到部分功能(为什么是部分封装?C语言其实是完全封装,所以只是弱化了面向对象的完善),数据完全不可见。可见,函数封装可以让调用者不用关心函数的实现过程。继承:允许我们在一定范围内覆盖外部定义的一组特定变量和函数。与非面向对象语言相比,继承提供了向上转型的便利。多态性:当父类的方法被子类重写时,各自可以产生自己的功能行为。多态其实是函数指针的一种应用,只是消除了函数指针的危险。多态的好处是当一个程序想要添加一个类的实现时,只需要重写方法来添加自己的实现即可。在我们使用多态之前,函数的调用者必须引用被调用者所在的模块。使用多态后,调用者可以通过抽象接口进行调用,而不是直接依赖被调用者。这种控制反转就是依赖反转。这也是面向对象的最大优势。依赖倒置不仅在函数调用中作用突出,在组件依赖方面也有强大的作用,后面会讲到。正是因为组件之间不需要直接依赖,才为组件提供了独立部署的能力。面向对象就是通过多态的方式来控制源代码中的依赖关系。这种控制能力使我们能够构建各种插件或架构,使高层的战略组件与底层的实现组件分离,底层组件也具有独立部署和独立开发的能力。3、函数式编程不同于按功能进行结构反汇编。我们需要从可变性的角度来隔离程序。也就是说,程序分为可变组件和不可变组件。为什么要隔离可变性?因为我们程序中所有的竞争,比如Java死锁和并发更新安全问题,都是由修改可变变量引起的,可变性隔离可以将这些竞争状态分开,从而进一步避免并发安全问题。这里不可变组件理解为我们的业务代码层(服务层),可变组件理解为我们的数据库层,所以我们一直遵循这种编程范式,将大部分业务逻辑归于不可变组件。为了避免可变变量竞争条件的发生,我们通常使用事务内存(数据库实现)或重试机制。可变和不可变除了这两种方法,书中还有一个保护可变变量的有趣方法,就是把可变性变成不可变性。比如我们在银行存取款,每次存取款都需要在余额上加一减一。这个balance是一个可变变量,但是如果内存足够大,我们可以不用每次都修改balance,而是只保留存提款记录,当要查询余额的时候,再计算所有的存提款记录。这样balance的variable变量就变成不可变的了,没有update,也就没有并发问题了。但这对于内存和性能来说无疑是一个很大的挑战。这种权衡关系让我想起了著名的CAP理论。Part3:Designprinciples-howtobuildacomponent即我们模块级编程的五个原则:SOLID首先说明了我们模块级编程(中层结构)的三个主要目标:让软件具有包容性被改变;使软件更容易被理解;构建可在多个软件系统中复用的组件为了实现这些目标,在构建组件时,我们需要遵循SOLID原则:SOLID也是这五个原则的首字母缩写SRP:SingleResponsibilityPrincipleAnysoftwareModulesshouldonlybe对某种行为负责。这里的软件模块可以理解为一组密切相关的函数和数据,也就是我们Java中的类。所以SRP更像是规定班级的划分。在定义中负责它的某种类型的行为是我们的模块被更改的唯一原因。违反SRP的反例:OCP:开闭原则对扩展开放,对修改关闭。开闭原则通常采用架构分层(MVC)+依赖倒置的方式实现。使得高层组件不会因为低层组件的修改而受到影响,仅通过扩展低层组件就可以满足更多的业务功能。LSP:通过继承(父类和子类的替换)或者接口和实现类的替换实现里氏替换原则,也是一种方便的扩展方式。如果违反了LSP,系统将不得不增加很多机制来应对变化。ISP:InterfaceSegregationPrinciple单一职责原则和开闭原则都讲的是如何将一组程序划分在一起,但是如果软件设计的任何一个层次依赖了自己不需要的东西,就会造成不必要的麻烦。所以ISP就是把那些不必要的依赖分离出来。通常的做法是将一个需要直接依赖的类改成依赖一个分离的接口。DIP:DependencyInversion在源代码层面的依赖关系中,我们应该更多地使用抽象类型而不是具体实现。我们每次修改接口,肯定会修改具体的实现,但是在修改具体实现的时候,我们很少需要修改接口,所以我们也可以认为接口比实现更稳定,而且在依赖关系中,我们也应该依靠这种更稳定的结构来降低变更带来的修改难度。为了追求该架构的稳定性,在编码层面可以概括为以下具体的编码规则:代码中应该多使用抽象接口,尽量避免那些易变的具体实现。包括对象的创建过程,因为在所有面向对象的语言中,创建对象都不可避免地依赖于对象在源码层面的具体实现,所以为了避免依赖具体的实现,我们会使用抽象工厂模式创建,也是spring创建bean的模式。不要在具体的实现类上创建派生类,也就是我们常说的少继承多组合。继承关系是所有源码依赖中最强也是最难修改的。不要试图通过覆盖包含具体实现的函数来扩展函数。因为即使覆盖了,也无法消除对具体实现的依赖。唯一的方法是创建一个新的抽象类。避免在您的代码中放置任何依赖于实现的名称,或其他可能会更改的名称。DIP不能完全消除对特定实现的依赖。当无法避免违反DIP时,我们可以将这部分隔离。图中跨边界到抽象层的依赖关系成为设计规则:依赖规则。抽象工厂模式:第四部分:组件原理——如何划分和关联组件SOLID是指导我们如何把砖块做成墙和房间,而组件原理是指导我们如何将这些房间组合成房子。程序的墨菲定律+摩尔定律使得我们开发插件架构成为必要和可能。首先定义组件的概念:可以独立部署的最小实体。也是一个可重定位的二进制文件。就像Java中的jar包。在这里理解可重定位性意味着能够知道和修改我们加载依赖项的位置。构建组件的基本原则:REP:Reuse/PublishEquivalencePrincipleCCP:CommonClosurePrincipleCRP:CommonReusePrincipleReuse/EquivalencePrinciple:软件重用的最小粒度应该等于其发布的最小粒度。我们最常用的模块管理工具:maven,管理着我们需要依赖的所有组件,而这些引入的每个独立组件都可以单独发布,也可以在任何系统中单独引用。每个依赖组件都有一个共同的主题,并有自己的版本管理。因为有版本管理,我们在依赖的时候可以有针对性的选择,避免依赖版本更新对原调用者的影响。.通用闭包原则:为了同一目的而同时修改的类应该放在同一个组件中。这也是在组件层面对单一职责原则的重新阐述。变化体现在同一个组件中,不需要重新部署其他组件,所以我们需要尽可能聚合某类变化涉及的所有类。常见的复用原则:不要强行让一个组件的使用者去依赖他们不需要的东西,所以在一个组件中,不要把联系不紧密的类放在一起,这也是接口隔离原则在组件处的体现等级。这里总结一下,三个原则不一定要完全遵循,也不可能全部遵循,但是在软件开发的过程中,重点是不断调整的。比如早期的组件划分更倾向于业务功能,没有预料到太多的复用场景,所以CCP比REP更重要。前三项原则规定了组件的划分,组件之间的相互关联耦合还需要遵循以下四项原则:无依赖环原则:即不应存在循环依赖。上面说了,为了让依赖不被依赖的修改被动影响,我们通常的做法是版本管理,让依赖决定自己的内容,但这个前提是不能出现循环依赖。而当存在循环依赖的时候,我们就需要使用依赖倒置,让被依赖的依赖抽象接口来创建一个新的组件,拆分掉循环依赖的部分。这两种方法打破了循环依赖。自下而上的设计原则:你会发现我们在本书中讲解的架构顺序也是自下而上的。从源代码层面到组件再到系统,组件结构图必须随着软件系统的变化而变化和扩展,不可能在系统构建之初就完美设计。稳定依赖原则:依赖必须朝着更稳定的方向发展。因为稳定性与改变的难度有关。每个组件都有一个可计算的稳定性指标,即出站依赖占所有依赖的比例。稳定依赖原则告诉我们,出站依赖越少越好,但并不是所有的组件都应该是稳定的。一般高层反而结构不稳定,底层越低应该越稳定。也就是说,最底层应该是最依赖的,最不依赖别人的。稳定抽象原则:组件的抽象层次应该与其稳定性保持一致。具体来说,稳定的组件也应该是抽象的。这样,它的稳定性不会影响它的扩展;相反,一个不稳定的组件应该包含特定的实现代码,这样它的不稳定性可以很容易地通过特定的代码来修改。组件的稳定性和抽象度应该接近下图中的一个主序。以上是我阅读《架构整洁之道》1-14章架构设计知识的收获。除此之外,我还有一些其他的感悟,分享给大家:读书有时候比百度学习更深刻。很多优秀的博客会帮助我们总结每一个知识点,但往往作者并没有像书里的作者那样把整个知识的背景和来龙去脉描述给你听。就像我的读书笔记一样,都是自己消化整理的。有很多是我自己的主观描述,我能理解,但是同样的话在你心里未必表达出同样的意思。.而且我们平时接触到的知识点太零散了,就像我们学习maven和它的每一条命令一样,却从来没有想过为什么要用maven来管理依赖,为什么要设计版本?只有及时输出,才能加深对知识的理解,或者说,只有能够用自己的话输出一个知识,才能证明自己真正掌握了这门知识。写博客永远不会太晚。一定要把它写下来。所以这就是为什么我做了这么多年的程序员,却是我发表第一篇文章的原因。但从现在开始,我会坚持使用输出。我们一起长大。我是春风,春风暖暖的,不负归期。公众号:程序员春风最后一个就是最后一个,码字不易,看的到这里,请大家点个赞!
