读书·架构整洁之道(原则篇)

十四年不修行,只读书,一年通幽。二十日不解碑,只静坐,一日看尽前陵碑 ----- 猫腻《择天记》

欢迎关注微信公众号“江湖喵的修炼秘籍”

近日在读Bob大叔的《架构整洁之道》,全书大体上可以分为两部分,前半部分讲设计原则,后半部分讲软件架构,这篇文章是对前半部分的一些观点的整理和思考。

一.设计与架构究竟是什么?

什么是设计?什么是架构?两者的区别是什么?

两者没有任何区别。

架构”这个词存在于“高层”级的讨论之中,这种讨论中往往会把“底层”的实现细节排除在外。

设计”往往用来指代具体的系统底层组织结构和实现细节。

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

软件架构和设计的终极目标是什么?

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

评估一个软件架构的优劣,可以用成本来衡量。如果满足一个用户需求的成本很低,而且在软件的整个生命周期中都可以保持低成本,那就是一个优良的设计,反之亦然。

麻乱系统的问题出在哪?

如果随着软件的迭代,作出变更所需要的成本不断增高,那就是一个典型的麻乱系统。

原因是工程师们持续低估了良好的设计、整洁的代码的重要性,无法平衡好系统架构和系统行为的关系。

如何平衡软件系统的行为价值和架构价值?

行为价值:软件的核心价值(最直观的价值维度),包括需求的实现,以及可用性保障(功能性bug 、性能、稳定性)

架构价值:保持软件的灵活性,便于理解、易于修改、方便维护、轻松部署。

按照艾森豪威尔矩阵划分的话,系统架构是重要但不特别紧急的,系统行为是紧急但不特别重要的。系统架构问题要优先于系统行为问题,如何平衡两者关系,应该是软件研发人员的职责。

思考:工程师所处的团队和所负责的项目可能是用户驱动、产品驱动或是技术驱动,不同的角色对系统的关注点不同,用户和产品自身也缺乏对软件架构的认识和评估重要性的能力,这个应该是软件开发人员的职责。软件开发人员有责任与其他部门进行抗争,避免不必要的妥协,将软件架构摆在第一位(事实上是很难的一件事)

二.SOLID设计原则

设计原则的意义?

通常来说,要想构建一个好的软件系统,应该从写整洁的代码开始做起。毕竟,如果建筑所使用的砖头质量不佳,那么架构所能起到的作用也会有限。反之亦然,如果建筑的架构设计不佳,那么其所用的砖头质量再好也没用。这就是SOLID设计原则索要解决的问题。

SOLID原则的主要作用是告诉我们如何将数据和函数组织成类,如何将类链接起来成为程序。

SOLID包括如下原则:

SRP:单一责任原则(Single Responsibility Principle)

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

这里的一个软件模块可以理解成一个源代码文件,比如java的一个类。

行为者可以理解为一个或多个有共同需求的人。

反面案例:

重复的假象:为了避免重复编码,开发者强行将不同行为者所依赖的代码强行凑在一起,SRP强调这类代码一定要分开。

代码合并:多人为了不同的目的修改了同一份源代码,很容易造成问题的产生。避免这种问题的方法就是将服务不同行为者的代码进行切分。类切分后可以使用facade模式(外观模式)提供统一的入口。

思考
1.接口一定要做到单一职责,类要尽量做到单一职责。

2.代码块独立,每个方法只提供单一的功能,复用性高。

3.函数功能简化以后,单个函数的代码量也会减少,可读性高,可以方便的使用函数名表明函数功能,由于方法功能明确,也会降低变更带来的风险。

4.很难有固化的标准去度量划分标准,如果根据SRP原则过分的细分,会导致类数量剧增。

5.如何区分真的重复还是假的重复?前提是开发者是否可以正确的判断出方法的的行为者是不是相同的。

OCP:开闭原则(Open Closed Principle)

设计良好的计算机软件应该易于扩展,同时抗拒修改

OCP原则的目的是让系统易于扩展,同时限制每次修改的范围。软件应该可以在不修改原有代码的基础上轻易的被扩展。

思考
1.SRP原则已经要求每个函数提供单一的功能,每个函数都可以作为一个基础组件。OCP原则要求我们将这些组件间的依赖关系按照层次结构进行组织,使高阶组件不会因低阶组件的修改而受到影响。

2.我们应该将会频繁变更的逻辑和基本稳定的逻辑分离开。

LSP:里氏替换原则(Liskov Substitution Principle)

子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。

LSP原则是一种指导继承关系以及接口和其实现方式的设计原则。

思考
1.理解LSP原则的关键,是理解design by contract(契约式设计),子类在设计的时候,要遵守父类的行为约定(包括函数声明要实现的功能;对输入、输出、异常的约定;以及注释中所包含的其他特殊说明)。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。接口和实现类之间的关系也应该符合这个原则。

2.LSP原则强调的是父类应该可以被子类随意替换,引用类不应该依赖它所使用的类,如果程序需要通过逻辑判断才能决定使用父类还是子类,则其违反了LSP原则。比如正方形/长方形问题,数学上我们可以认为正方形是一种特殊的长方形,但软件设计中,正方形不是长方形的子类。

ISP:接口隔离原则(Interface Segregation Principle)

在设计中应避免不必要的依赖

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

思考
1.如果认为“接口”指的是一个接口类对应的一系列接口组成的集合,那么如果使用者只用到这些接口中的一部分,那么我们就应该把这些接口抽离出来,单独提供给调用者。

2.如果认为“接口”指定是单个接口或者单个函数,如果调用者只用到这个接口功能的一部分,那么这个接口也不符合SRP(单一责任原则),接口粒度不够细,需要进行拆分,ISP原则也为是否符合SRP原则提供了一种评判标准。

DIP:依赖倒置原则(Dependence Inversion Principle)

细节应当依赖于抽象,抽象不应当依赖于细节

1.抽象的接口层远比具体实现稳定

2.应在代码中多使用抽象接口,尽量避免使用多变的具体实现(比如使用抽象工厂模式)

3.不要在具体实现上创建衍生类

4.不要覆盖包含具体实现的函数

5.应该避免在代码中写入任何与具体实现相关的名字,或者其他容易变动的事物的名字

思考
1.依赖反转的核心是面向接口编程,代码依赖时,应该依赖接口,而不是具体实现。其实也不一定都能一步到位的做到,可能前期并不存在抽象和继承的可能性,如果全部都抽象出来,工作量也很繁重,可以在前期评估,如果有扩展的可能性,可以直接一步到位,如果未来几乎不存在扩展的可能性,可以在后期扩展的尝试重构。

2.我们重点关注的应该是经常会变动的实现模块

三.组件构建原则

组件聚合

REP:复用/发布等同原则

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

Maven等模块管理工具的出现推动出现了大量可复用的组件和组件库,基于此,REP原则要求组件库必须由某种流程来驱动,并提供明确的版本号、发布日期、变更内容、发布文档等信息,以便依赖方对是否需要升级组件库提供决策依据,从而真正的实现软件复用。

思考:REP原则的重点是告诉软件开发者应该怎么做,而不是如何去做。REP原则要求组件中的类和模块必须是紧密相关的,一个组件不能由一组没有关联的类和模块组成,但是并没有定义如何将类和模块组合成组件。

CCP:共同闭包原则

我们应该将那些会同时修改,并且为相同目的而修改的类放到同一个组件中,而将不会同时修改,并且不会为了相同的目的而修改的那些类放到不同的组件中

CCP原则是组件层面对于SRP原则和OCP原则的进一步阐述。

首先SRP原则认为一个类只提供一种单一的职责,CCP原则认为一个组件因为不同的目的而同时修改。

OCP原则认为一个类应该对扩展开放,对修改关闭,CCP原则中闭包的概念就是OCP原则中对修改关闭的范围。

思考

将SRP原则和OCP原则的理念进行融合,并提升到组件层面,首先我们应该将具备相同职责,即开发过程中会为了同一个目的进行修改的代码聚合在一起,放在同一个组件中,相反,如果需要为了不同的目的而修改,则需要拆分开来。同时,我们需要将比较稳定,不会发生修改的部分聚合在一起,将需要一同变更的聚合在一起,从而合理的控制闭包的范围。

OCP原则的重点是告诉软件开发者应该将哪些类聚合在一起,这些要求的目的是为了在需要对组件进行修改时,可以尽可能的缩小影响范围。

CRP:共同复用原则

不要强迫一个组件的用户强制依赖它们不需要的东西

CRP原则是ISP原则在组件层面的阐述,两者的重点都是不要依赖不需要的东西。

思考
1.CRP原则的重点是告诉开发者需要将哪些类分开

2.可以通过现实中的例子进一步加强理解。

第一个例子是一个项目将服务发布成组件时,在pom依赖中不应该依赖没有用的依赖,这种操作的后果是,组件的依赖方也必须继承这些无用的依赖,并且在解决版本冲突上需要花费一些精力

第二个是类似比较有争议的lombok组件,如果在组件中使用lombok,则组件的依赖方也必须依赖lombok组件,侵入性太强

REP原则和CCP原则是黏性原则,CRP原则是排他性原则,三者之间的关系和侧重点是需要软件设计师去根据实际情况进行取舍的。

组件耦合

ADP:无依赖环原则

组件依赖关系中不应该出现环

系统中不同组件之间的依赖应该是单向进行的,不应该出现循环依赖

如果出现的循环依赖 可以尝试使用依赖反转原则(DIP)或者创建创建一个包含涉及到依赖类的新组建 纠正依赖关系

组件依赖关系必须要随着项目的逻辑设计一起扩展和演进,在最初就设计完美的依赖关系是不现实的

SDP:稳定依赖原则

依赖关系必须要指向更稳定的方向

1.程序不会是一尘不变的,修改是必然的。

2.稳定与不稳定:被依赖的组件时稳定的,依赖其他组件的组件是不稳定的,依赖的组件越多越不稳定,因为当所依赖的组件被修改时,该组件被修改的概率也会增大。

不稳定性=入向依赖/(入向依赖+出现依赖),SDP原则的目标是每个组件的不稳定性都应该大于其所依赖的组件,即依赖关系的稳定性方向是单向的,并且被依赖的组件要更稳定。

3.SDP原则的重点是稳定性的方向,对于稳定性本身来说并无要求,并不是所有的组件都应该是稳定的。当所有组件的稳定性都为1时,系统将无法变更

SAP:稳定抽象原则

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

1.一个组件如果是稳定的,它就应该是抽象的,这样符合OCP原则,既稳定又方便扩展

2.一个组件如果是不稳定的,它就应该有具体的实现,方便进行修改

3.SAP原则将抽象与稳定性之间建立了关联,根据SDP原则可以引申出:依赖的关系必须要指向更抽象的方向

4.抽象度=抽象类+接口数量/组件中类的总数量 范围在0和1之间,抽象度过高和过低都不是好事,我们软件设计的目标应该保持抽象度中间值附近

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

光光-Leo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值