架构整洁之道笔记 第一篇 编程范式及单体模块设计原则

本文探讨了软件架构的重要性,如何通过设计原则(SOLID)如单一职责原则、开闭原则等来构建高质量架构,以及编程范式(结构化、面向对象、函数式)在架构中的应用。强调了设计整洁、可维护性和灵活性在软件开发中的核心价值。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        采用好的软件架构可以大大节省软件项目构建与维护的人力成本。让每次变更都短小简单,易于实施,并且避免缺陷,用最小的成本,最大程度地满足功能性和灵活性的要求。

        《架构整洁之道》整本书从单体模块函数设计编写原则,再到组件构建原则,最后到怎么构建成一个易于修改,维护,测试的软件架构逐渐深入,不仅有理论基础,还有具体实施,是一本架构入门不可多得的好书,可以多读几遍,在实际的工作中学以致用。本博文主要记录学习过程中总结的一些关键点及个人的一些想法。

一、设计与架构究竟是什么

       本书的一个重要目标就是要清晰、明确地对二者进行定义。首先要明确地说,二者没有任何区别。一丁点区别都没有!

      “架构”这个词往往使用于“高层级”的讨论中。这类讨论一般都把“底层”的实现细节排除在外。而“设计”一词,往往用来指代具体的系统底层组织结构和实现的细节。但是,从一个真正的系统架构师的日常工作来看,这样的区分是根本不成立的。

        底层设计细节和高层架构信息是不可分割的。它们组合在一起,共同定义了整个软件系统,缺一不可。所谓的底层和高层本身就是一系列决策组成的连续体,并没有清晰的分界线。

       我后续文章也会讲到,软件的可拓展性,维护性的第一责任人是软件开发人员,所以如果我们是一个富有责任心的开发人员,在我们平常的软件设计中,就不仅会关注我们负责模块的设计细节,还会关联考虑和系统不同组件间的依赖等,便于后期维护拓展。

软件架构目标是什么

       软件架构的终极目标是,用最小的人力成本来满足构建和维护该系统的需求。

       一个软件架构的优劣,可以用它满足用户需求所需要的成本来衡量。如果该成本很低,并且在系统的整个生命周期内一直都能维持这样的低成本,那么这个系统的设计就是优良的。如果该系统的每次发布都会提升下一次变更的成本,那么这个设计就是不好的。就这么简单。

在架构方面我们常犯的错误    

        现在好多软件研发工程师都有点过于自信,会持续低估那些好的、良好设计的、整洁的代码的重要性。这些工程师们普遍用一句话来欺骗自己:“我们可以未来再重构代码,产品上线最重要!”但是结果大家都知道,产品上线以后重构工作就再没人提起了。市场的压力永远也不会消退,作为首先上市的产品,后面有无数的竞争对手追赶,必须要比他们跑得更快才能保持领先。

        所以,重构的时机永远不会再有了。工程师们忙于完成新功能,新功能做不完,哪有时间重构老的代码?循环往复,系统成了一团乱麻,生产效率持续直线下降,直至为零。

        结果就像龟兔赛跑中过于自信的兔子一样,软件研发工程师们对自己保持高产出的能力过于自信了。但是乱成一团的系统代码可没有休息时间,也不会放松。如果不严加提防,在几个月之内,整个研发团队就会陷入困境。

        工程师们经常相信的另外一个错误观点是:“在工程中容忍糟糕的代码存在可以在短期内加快该工程上线的速度,未来这些代码会造成一些额外的工作量,但是并没有什么大不了。”相信这些鬼话的工程师对自己清理乱麻代码的能力过于自信了。但是更重要的是,他们还忽视了一个自然规律:无论是从短期还是长期来看,胡乱编写代码的工作速度其实比循规蹈矩更慢。

        综上所述,管理层扭转局面的唯一选择就是扭转开发者的观念,让他们从过度自信的兔子模式转变回来,为自己构建的乱麻系统负起责任来。

        要想提高自己软件架构的质量,就需要先知道什么是优秀的软件架构。而为了在系统构建过程中采用好的设计和架构以便减少构建成本,提高生产力,又需要先了解系统架构的各种属性与成本和生产力的关系。

二、两个价值维度

        对于每个软件系统,我们都可以通过行为和架构两个维度来体现它的实际价值。

行为价值

        软件系统的行为是其最直观的价值维度。程序员的工作就是让机器按照某种指定方式运转,给系统的使用者创造或者提高利润。程序员们为了达到这个目的,往往需要帮助系统使用者编写一个对系统功能的定义,也就是需求文档。然后,程序员们再把需求文档转化为实际的代码。当机器出现异常行为时,程序员要负责调试,解决这些问题。

        大部分程序员认为这就是他们的全部工作。他们的工作是且仅是:按照需求文档编写代码,并且修复任何Bug。这真是大错特错。

架构价值

        软件系统的第二个价值维度,就体现在软件这个英文单词上:software。“ware”的意思是“产品”,而“soft”的意思,不言而喻,是指软件的灵活性。软件发明的目的,就是让我们可以以一种灵活的方式来改变机器的工作行为。对机器上那些很难改变的工作行为,我们通常称之为硬件(hardware)。

        为了达到软件的本来目的,软件系统必须够“软”——也就是说,软件应该容易被修改。当需求方改变需求的时候,随之所需的软件变更必须可以简单而方便地实现。变更实施的难度应该和变更的范畴(scope)成等比关系,而与变更的具体形状(shape)无关。

        需求变更的范畴与形状,是决定对应软件变更实施成本高低的关键。这就是为什么有的代码变更的成本与其实现的功能改变不成比例。

        从系统相关方(Stakeholder)的角度来看,他们所提出的一系列的变更需求的范畴都是类似的,因此成本也应该是固定的。但是从研发者角度来看,系统用户持续不断的变更需求就像是要求他们不停地用一堆不同形状的拼图块,拼成一个新的形状。整个拼图的过程越来越困难,因为现有系统的形状永远和需求的形状不一致。

        我们在这里使用了“形状”这个词,这可能不是该词的标准用法,但是其寓意应该很明确。毕竟,软件工程师们经常会觉得自己的工作就是把方螺丝拧到圆螺丝孔里面。

        问题的实际根源当然就是系统的架构设计。如果系统的架构设计偏向某种特定的“形状”,那么新的变更就会越来越难以实施。所以,好的系统架构设计应该尽可能做到与“形状”无关。

哪个更重要

软件系统的第一个价值维度:系统行为,是紧急的,但是并不总是特别重要。

软件系统的第二个价值维度:系统架构,是重要的,但是并不总是特别紧急。

按照艾森豪威尔矩阵

1.重要且紧急

2.重要不紧急

3.不重要但紧急

4.不重要且不紧急

在这里你可以看到,软件的系统架构——那些重要的事情——占据了该列表的前两位,而系统行为——那些紧急的事情——只占据了第一和第三位

        研发人员还忘了一点,那就是业务部门原本就是没有能力评估系统架构的重要程度的,这本来就应该是研发人员自己的工作职责!所以,平衡系统架构的重要性与功能的紧急程度这件事,是软件研发人员自己的职责。

        有成效的软件研发团队会迎难而上,毫不掩饰地与所有其他的系统相关方进行平等的争吵。请记住,作为一名软件开发人员,你也是相关者之一。软件系统的可维护性需要由你来保护,这是你角色的一部分,也是你职责中不可缺少的一部分。公司雇你的很大一部分原因就是需要有人来做这件事。

        如果你是软件架构师,那么这项工作就加倍重要了。软件架构师这一职责本身就应更关注系统的整体结构,而不是具体的功能和系统行为的实现。软件架构师必须创建出一个可以让功能实现起来更容易、修改起来更简单、扩展起来更轻松的软件架构。

请记住:如果忽视软件架构的价值,系统将会变得越来越难以维护,终会有一天,系统将会变得再也无法修改。如果系统变成了这个样子,那么说明软件开发团队没有和需求方做足够的抗争,没有完成自己应尽的职责。

三、从基础构件开始:编程范式

        编程范式指的是程序的编写模式,与具体的编程语言关系相对较小。这些范式会告诉你应该在什么时候采用什么样的代码结构。直到今天,我们也一共只有三个编程范式,而且未来几乎不可能再出现新的,接下来我们就看一下这三个编程范式都讲了什么及为什么这三个范式基本概括了所有。

三个编程范式分别是:

        结构化编程(structured programming)

        面向对象编程(object-oriented programming)

        函数式编程(functional programming)

结构化编程

        一句话概括:结构化编程对程序控制权的直接转移进行了限制和规范,限制了goto语句的使用。

        goto语句的某些用法会导致某个模块无法被递归拆分成更小的、可证明的单元,这会导致无法采用分解法来将大型问题进一步拆分成更小的、可证明的部分,但其实goto 的实际效果和更简单的分支结构以及循环结构是一致的。如果代码中只采用了这两类控制结构,则一定可以将程序分解成更小的、可证明的单元(Bohm和Jocopini证明了人们可以用顺序结构、分支结构、循环结构这三种结构构造出任何程序)。

        既然结构化编程范式可将模块递归降解拆分为可推导的单元,这就意味着模块也可以按功能进行降解拆分。这样一来,我们就可以将一个大型问题拆分为一系列高级函数的组合,而这些高级函数各自又可以继续被拆分为一系列低级函数,如此无限递归。更重要的是,每个被拆分出来的函数也都可以用结构化编程范式来书写。思考下,我们开发时,是不是很多时候都是这个做的。

        结构化编程范式中最有价值的地方就是,它赋予了我们创造可证伪程序单元的能力。这就是为什么现代编程语言一般不支持无限制的goto语句。更重要的是,这也是为什么在架构设计领域,功能性降解拆分仍然是最佳实践之一。

面向对象编程

        一句话概括:面向对象编程对程序控制权的间接转移进行了限制和规范,限制了函数指针的使用。

究竟什么是面向对象?

        业界在这个问题上存在着很多不同的说法和意见。然而对一个软件架构师来说,其含义应该是非常明确的:面向对象编程就是以多态为手段来对源代码中的依赖关系进行控制的能力,这种能力让软件架构师可以构建出某种插件式架构,让高层策略性组件与底层实现性组件相分离,底层组件可以被编译成插件,实现独立于高层组件的开发和部署。

        UNIX操作系统强制要求每个IO设备都要提供open、close、read、write和seek这5个标准函数。也就是说,每个IO设备驱动程序对这5种函数的实现在函数调用上必须保持一致。这个简单的编程技巧正是面向对象编程中多态的基础。多态其实不过就是函数指针的一种应用。插件式架构就是为了支持这种IO不相关性而发明的,它几乎在随后的所有操作系统中都有应用。但即使多态有如此多优点,大部分程序员还是没有将插件特性引入他们自己的程序中,因为函数指针实在是太危险了。而面向对象编程的出现使得这种插件式架构可以在任何地方被安全地使用。

函数式编程

        一句话概括:函数式编程对程序中的赋值进行了限制和规范,限制了赋值语句的使用。

        所有的竞争问题、死锁问题、并发更新问题都是由可变变量导致的。如果变量永远不会被更改,那就不可能产生竞争或者并发更新问题。如果锁状态是不可变的,那就永远不会产生死锁问题。

        不可变性是否实际可行?如果我们能忽略存储器与处理器在速度上的限制,那么答案是肯定的。否则的话,不可变性只有在一定情况下是可行的。所以我们要尽量进行可变性的隔离,一种常见方式是将应用程序,或者是应用程序的内部服务进行切分,划分为可变的和不可变的两种组件。不可变组件用纯函数的方式来执行任务,期间不更改任何状态。这些不可变的组件将通过与一个或多个非函数式组件通信的方式来修改变量状态

        一个架构设计良好的应用程序应该将状态修改的部分和不需要修改状态的部分隔离成单独的组件,然后用合适的机制来保护可变量。软件架构师应该着力于将大部分处理逻辑都归于不可变组件中,可变状态组件的逻辑应该越少越好。

总结

        每个编程范式的目的都是设置限制。这些范式主要是为了告诉我们不能做什么,而不是可以做什么。这些编程范式与软件架构有关系吗?当然有,而且关系相当密切。譬如说,多态是我们跨越架构边界的手段,函数式编程是我们规范和限制数据存放位置与访问权限的手段,结构化编程则是各模块的算法实现基础。这和软件架构的三大关注重点不谋而合:功能性、组件独立性以及数据管理。

四、设计原则(SOLID)

        通常来说,要想构建一个好的软件系统,应该从写整洁的代码开始做起。毕竟,如果建筑所使用的砖头质量不佳,那么架构所能起到的作用也会很有限。反之亦然,如果建筑的架构设计不佳,那么其所用的砖头质量再好也没有用。这就是SOLID设计原则所要解决的问题,我的理解SOLID设计原则主要指导模块级别 或者是单组件内的编程,SOLID原则的主要作用就是告诉我们如何将数据和函数组织成为类,以及如何将这些类链接起来成为程序。

       

         一般情况下,我们为软件构建中层结构的主要目标如下:

        使软件可容忍被改动。

        使软件更容易被理解。

        构建可在多个软件系统中复用的组件。

        我们在这里之所以会使用“中层”这个词,是因为这些设计原则主要适用于那些进行模块级编程的程序员。SOLID原则应该直接紧贴于具体的代码逻辑之上,这些原则是用来帮助我们定义软件架构中的组件和模块的。

SRP:单一职责原则

        任何一个软件模块都应该只对某一类行为者负责。

        这里提到的“软件模块”究竟又是在指什么呢?大部分情况下,其最简单的定义就是指一个源代码文件。然而,有些编程语言和编程环境并不是用源代码文件来存储程序的。在这些情况下,“软件模块”指的就是一组紧密相关的函数和数据结构。在这里,“相关”这个词实际上就隐含了SRP这一原则。代码与数据就是靠着与某一类行为者的相关性。

        多人为了不同的目的修改了同一份源代码,这很容易造成问题的产生。

        单一职责原则主要讨论的是函数和类之间的关系——但是它在两外两个讨论层面上会以不同的形式出现。在组件层面,我们可以将其称为共同闭包原则(Common Closure Principle),在软件架构层面,它则是用于奠定架构边界的变更轴心(Axis of Change)。

OCP:开闭原则

        其核心要素是:如果软件系统想要更容易被改变,那么其设计就必须允许新增代码来修改系统行为,而非只能靠修改原来的代码。

        设计良好的计算机软件应该易于扩展,同时抗拒修改。换句话说,一个设计良好的计算机系统应该在不需要修改的前提下就可以轻易被扩展。

        其实这也是我们研究软件架构的根本目的。如果对原始需求的小小延伸就需要对原有的软件系统进行大幅修改,那么这个系统的架构设计显然是失败的。

        如何实现?我们可以先将满足不同需求的代码分组(即SRP),然后再来调整这些分组之间的依赖关系(即DIP)。软件架构师可以根据相关函数被修改的原因、修改的方式及修改的时间来对其进行分组隔离,并将这些互相隔离的函数分组整理成组件结构,使得高阶组件不会因低阶组件被修改而受到影响。

LSP:  里氏替换原则     

        可替换性:如果对于每个类型是S的对象o1都存在一个类型为T的对象o2,能使操作T类型的程序P在用o2替换o1时行为保持不变,我们就可以将S称为T的子类型

        LSP除了是指导如何使用继承关系的一种方法,随着时间的推移,LSP逐渐演变成了一种更广泛的、指导接口与其实现方式的设计原则。这里提到的接口可以有多种形式——可以是Java风格的接口,具有多个实现类;也可以像Ruby一样,几个类共用一样的方法签名,甚至可以是几个服务响应同一个REST接口。

        LSP适用于上述所有的应用场景,因为这些场景中的用户都依赖于一种接口,并且都期待实现该接口的类之间能具有可替换性。

        什么是可替换行? 替换的前提是面向对象语言所支持的多态特性,同一个行为具有多个不同表现形式或形态的能力。以JDK的集合框架为例,List接口的定义为有序集合,List接口有多个派生类,比如大家耳熟能详的ArrayListLinkedList。那当某个方法参数或变量是List接口类型时,既可以是ArrayList的实现, 也可以是LinkedList的实现,这就是替换。

ISP:接口隔离原则

        在一般情况下,任何层次的软件设计如果依赖于不需要的东西,都会是有害的。从源代码层次来说,这样的依赖关系会导致不必要的重新编译和重新部署,对更高层次的软件架构设计来说,问题也是类似的。

        任何层次的软件设计如果依赖了它并不需要的东西,就会带来意料之外的麻烦

DIP:依赖反转原则

        如果想要设计一个灵活的系统,在源代码层次的依赖关系中就应该多引用抽象类型,而非具体实现。

        在应用DIP时,我们不必考虑稳定的操作系统或者平台设施,因为这些系统接口很少会有变动。我们主要应该关注的是软件系统内部那些会经常变动的(volatile)具体实现模块,这些模块是不停开发的,也就会经常出现变更。

        为什么要多引用抽象类型?我们每次修改抽象接口的时候,一定也会去修改对应的具体实现。但反过来,当我们修改具体实现时,却很少需要去修改相应的抽象接口。所以我们可以认为接口比实现更稳定。

        优秀的软件设计师和架构师会花费很大精力来设计接口,以减少未来对其进行改动。毕竟争取在不修改接口的情况下为软件增加新的功能是软件设计的基础常识。也就是说,如果想要在软件架构设计上追求稳定,就必须多使用稳定的抽象接口,少依赖多变的具体实现。

下面,我们将该设计原则归结为以下几条具体的编码守则:
        1)应在代码中多使用抽象接口,尽量避免使用那些多变的具体实现类。这条守则适用于所有编程语言,无论静态类型语言还是动态类型语言

         2)不要在具体实现类上创建衍生类。上一条守则虽然也隐含了这层意思,但它还是值得被单独拿出来做一次详细声明。在静态类型的编程语言中,继承关系是所有一切源代码依赖关系中最强的、最难被修改的,所以我们对继承的使用应该格外小心。即使是在稍微便于修改的动态类型语言中,这条守则也应该被认真考虑。

         3)不要覆盖(override)包含具体实现的函数。调用包含具体实现的函数通常就意味着引入了源代码级别的依赖。即使覆盖了这些函数,我们也无法消除这其中的依赖——这些函数继承了那些依赖关系。在这里,控制依赖关系的唯一办法,就是创建一个抽象函数,然后再为该函数提供多种具体实现。

        4)应避免在代码中写入与任何具体实现相关的名字,或者是其他容易变动的事物的名字。这基本上是DIP原则的另外一个表达方式。

        如果想要遵守上述编码守则,我们就必须要对那些易变对象的创建过程做一些特殊处理,这样的谨慎是很有必要的,因为基本在所有的编程语言中,创建对象的操作都免不了需要在源代码层次上依赖对象的具体实现。

        抽象接口组件中包含了应用的所有高阶业务规则,而具体实现组件中则包括了所有这些业务规则所需要做的具体操作及其相关的细节信息。请注意,这里的控制流跨越架构边界的方向与源代码依赖关系跨越该边界的方向正好相反,源代码依赖方向永远是控制流方向的反转——这就是DIP被称为依赖反转原则的原因。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值