笔记架构整洁之道
Posted Whaleson
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了笔记架构整洁之道相关的知识,希望对你有一定的参考价值。
SRP:单⼀职责原则。
任何⼀个软件模块都应该只对某⼀类⾏为者负责。
该设计原则是基于康威定律(Conway’s Law)[1]的⼀个推论——⼀个软件系统的最佳结构⾼度依赖于开发这个系统的组织的内部结构。这样,每个软件模块都有且只有⼀个需要被改变的理由。
OCP:开闭原则。
设计良好的计算机软件应该易于扩展,同时抗拒修改。
⼀个设计良好的计算机系统应该在不需要修改的前提下就可以轻易被扩展。
⼀个好的软件架构设计师会努⼒将旧代码的修改需求量降⾄最⼩,甚⾄为0。
OCP是我们进⾏系统架构设计的主导原则,其主要⽬标是让系统易于扩展,同时限制其每次被修改所影响的范围。
该设计原则是由Bertrand Meyer在20世纪80年代⼤⼒推⼴的,其核⼼要素是:如果软件系统想要更容易被改变,那么其设计就必须允许新增代码来修改系统⾏为,⽽⾮只能靠修改原来的代码。
LSP:⾥⽒替换原则。
我们的普遍认知正如上⽂所说,认为LSP只不过是指导如何使⽤继承关系的⼀种⽅法,然⽽随着时间的推移,LSP逐渐演变成了⼀种更⼴泛的、指导接⼝与其实现⽅式的设计原则。
该设计原则是Barbara Liskov在1988年提出的⼀个著名的⼦类型定义。简单来说,这项原则的意思是如果想⽤可替换的组件来构建软件系统,那么这些组件就必须遵守同⼀个约定,以便让这些组件
可以相互替换。
ISP:接⼝隔离原则。
这项设计原则主要告诫软件设计师应该在设计中避免不必要的依赖。
对于Java这样的静态类型语⾔来说,它们需要程序员显式地import、use或者include其实现功能所需要的源代码。⽽正是这些语句带来了源代码之间的依赖关系,这也就导致了某些模块需要被重新编译和重新部署。
⼀般情况下,任何层次的软件设计如果依赖于不需要的东⻄,都会是有害的。
DIP:依赖反转原则。
该设计原则指出⾼层策略性的代码不应该依赖实现底层细节的代码,恰恰相反,那些实现底层细节的代码应该依赖⾼层策略性的代码。
如果想要设计⼀个灵活的系统,在源代码层次的依赖关系中就应该多引⽤抽象类型,⽽⾮具体实现。
也就是说,在Java这类静态类型的编程语⾔中,在使⽤use、import、include这些语句时应该只引⽤那些包含接⼝、抽象类或者其他抽象类型声明的源⽂件,不应该引⽤任何具体实现。
我们主要应该关注的是软件系统内部那些会经常变动的(volatile)具体实现模块,这些模块是不停开发的,也就会经常出现变更。
本质是将可变性和不变性分离。 依赖反转后,不再是依赖具体实现⽽是依赖接⼝。⽽在实际情况中,接⼝的变更频率要⽐具体实现低的多得多。
优秀的软件设计师和架构师会花费很⼤精⼒来设计接⼝,以减少未来对其进⾏改动。毕竟争取在不修改接⼝的情况下为软件增加新的功能是软件设计的基础常识。
应在代码中多使⽤抽象接⼝,尽量避免使⽤那些多变的具体实现类。这条守则适⽤于所有编程语⾔,⽆论静态类型语⾔还是动态类型语⾔。同时,对象的创建过程也应该受到严格限制,对此,我们通常会选择⽤抽象⼯⼚(abstract factory)这个设计模式。
不要在具体实现类上创建衍⽣类。上⼀条守则虽然也隐含了这层意思,但它还是值得被单独拿出来做⼀次详细声明。在静态类型的编程语⾔中,继承关系是所有⼀切源代码依赖关系中最强的、最难被修改的,所以我们对继承的使⽤应该格外⼩⼼。即使是在稍微便于修改的动态类型语⾔中,这条守则也应该被认真考虑。
不要覆盖(override)包含具体实现的函数。调⽤包含具体实现的函数通常就意味着引⼊了源代码级别的依赖。即使覆盖了这些函数,我们也⽆法消除这其中的依赖——这些函数继承了那些依赖关系。在这⾥,控制依赖关系的唯⼀办法,就是创建⼀个抽象函数,然后再为该函数提供多种具体实现。
应避免在代码中写⼊与任何具体实现相关的名字,或者是其他容易变动的事物的名字。
⼯⼚模式
如果想要遵守上述编码守则,我们就必须要对那些易变对象的创建过程做⼀些特殊处理,这样的谨慎是很有必要的,因为基本在所有的编程语⾔中,创建对象的操作都免不了需要在源代码层次上依赖对象的具体实现。
在⼤部分⾯向对象编程语⾔中,⼈们都会选择⽤抽象⼯⼚模式来解决这个源代码依赖的问题。
我们在软件系统中并不可能完全消除违反DIP的情况。通常只需要把它们集中于少部分的具体实现组件中,将其与系统的其他部分隔离即可。
组件构建原则
如果说SOLID原则是⽤于指导我们如何将砖块砌成墙与房间的,那么组件构建原则就是⽤来指导我们如何将这些房间组合成房⼦的。
组件聚合
REP:( The Reuse/Release Equivalence Principle)复⽤/发布等同原则。
软件复⽤的最⼩粒度应等同于其发布的最⼩粒度。
REP原则就是指组件中的类与模块必须是彼此紧密相关的。也就是说,⼀个组件不能由⼀组毫⽆关联的类和模块组成,它们之间应该有⼀个共同的主题或者⼤⽅向。
CCP:(The Common Closure Principle)共同闭包原则。
OCP原则认为⼀个类应该便于扩展,⽽抗拒修改。
正如SRP原则中提到的“⼀个类不应该同时存在着多个变更原因”⼀样,CCP原则也认为⼀个组件不应该同时存在着多个变更原因。
CRP:(The Common Reuse Principle )共同复⽤原则。
不要强迫⼀个组件的⽤户依赖他们不需要的东⻄。
共同复⽤原则(CRP)是另外⼀个帮助我们决策类和模块归属于哪⼀个组件的原则。该原则建
议我们将经常共同复⽤的类和模块放在同⼀个组件中。
组件耦合
⽆依赖环原则(ADP:The Acyclic Dependencies Principle)
组件依赖关系图中不应该出现环。
不管我们从该哪个节点开始,都不能沿着这些代表了依赖关系的边最终⾛回到起始点。也就是说,这种结构中不存在环,我们称这种结构为有向⽆环图(Directed Acyclic Graph,简写为DAG)。
判断出哪些组件会受变更的影响?
只需要按其依赖关系反向追溯即可。
任何⼀个我们预期会经常变更的组件都不应该被⼀个难于修改的组件所依赖,否则这个多变的组件也将会变得⾮常难以被修改。
稳定依赖原则(SDP)
依赖关系必须要指向更稳定的⽅向。
任何⼀个我们预期会经常变更的组件都不应该被⼀个难于修改的组件所依赖,否则这个多变的组件也将会变得⾮常难以被修改。
稳定性应该与变更的频繁度没有直接关系。
稳定性应该与变更所需的⼯作量有关。
软件组件难于修改的⼀个最直接的办法就是让很多其他组件依赖于它。带有许多⼊向依赖关系的组件是⾮常稳定的,因为它的任何变更都需要应⽤到所有依赖它的组件上。
不依赖于任何组件,所以不会有任何原因导致它需要被变更,我们称它为“独⽴”组件。
稳定性指标
如何来量化⼀个组件的稳定性呢?
其中⼀种⽅法是计算所有⼊和出的依赖关系。通过这种⽅法,我们就可以计算出⼀个组件的位置稳定性(positional stability)。
Fan-in:⼊向依赖,这个指标指代了组件外部类依赖于组件内部类的数量。
Fan-out:出向依赖,这个指标指代了组件内部类依赖于组件外部类的数量。
I:不稳定性,I=Fan-out/(Fan-in+Fan-out)。该指标的范围是[0,1],I=0意味着组件是最稳定的,I=1意味着组件是最不稳定的。
事实上,当每个源⽂件只包含⼀个类的时候,I指标是最容易计算的。同样在Java中,I指标也可以通过Import语句和全引⽤名字的数量来计算。
稳定依赖原则(SDP)的要求是让每个组件的I指标都必须⼤于其所依赖组件的I指标。也就是说,组件结构依赖图中各组件的I指标必须要按其依赖关系⽅向递减。
可变更的组件位于顶层,同时依赖于底层的稳定组件。
如何才能让⼀个⽆限稳定的组件(I=0)接受变更呢?
开闭原则(OCP)为我们提供了答案。这个原则告诉我们:创造⼀个⾜够灵活、能够被扩展,⽽且不需要修改的类是可能的,⽽这正是我们所需要的。哪⼀种类符合这个原则呢?答案是抽象类。
稳定抽象原则(SAP)
稳定抽象原则(SAP)为组件的稳定性与它的抽象化程度建⽴了⼀种关联。
⼀⽅⾯,该原则要求稳定的组件同时应该是抽象的,这样它的稳定性就不会影响到扩展性。另⼀⽅⾯,该原则也要求⼀个不稳定的组件应该包含具体的实现代码,这样它的不稳定性就可以通过具体的代码被轻易修改。
如果⼀个组件想要成为稳定组件,那么它就应该由接⼝和抽象类组成,以便将来做扩展。
将SAP与SDP这两个原则结合起来,就等于组件层次上的DIP。因为SDP要求的是让依赖关系指向更稳定的⽅向,⽽SAP则告诉我们稳定性本身就隐含了对抽象化的要求,即依赖关系应该指向更抽象的⽅向。
然⽽,DIP毕竟是与类这个层次有关的原则——对类来说,设计是没有灰⾊地带的。⼀个类要么是抽象类,要么就不是。SDP与SAP这对原则是应⽤在组件层⾯上的,我们要允许⼀个组件部分抽象,部分稳定。
衡量抽象化程度
Nc(number of class):组件中类的数量。
Na(Number of abstract):组件中抽象类和接⼝的数量。
A:抽象程度,A=Na÷Nc。
A指标的取值范围是从0到1,值为0代表组件中没有任何抽象类,值为1就意味着组件中只有抽象类。
主序列
组件的稳定性I与其抽象化程度A之间的关系。
离主序列线的距离
D指标:距离D=|A+I-1|,该指标的取值范围是[0,1]。值为0意味着组件是直接位于主序列线上的,值为1则意味着组件在距离主序列最远的位置。
通过计算每个组件的D指标,就可以量化⼀个系统设计与主序列的契合程度了。另外,我们也可以⽤D指标⼤于0多少来指导组件的重构与重新设计。
对于⼀个良好的系统设计来说,D指标的平均值和⽅差都应该接近于0。其中,⽅差还可以被当作组件的“达标红线”来使⽤,我们可以通过它找出系统设计中那些不合常规的组件。
究竟什么才是“软件架构”呢?软件架构师的⼯作内容究竟是什么?这项⼯作⼜是什么时候进⾏的呢?
⾸先,软件架构师⾃身需要是程序员,并且必须⼀直坚持做⼀线程序员,绝对不要听从那些说应该让软件架构师从代码中解放出来以专⼼解决⾼阶问题的伪建议。不是这样的!软件架构师其实应该是能⼒最强的⼀群程序员,他们通常会在⾃身承接编程任务的同时,逐渐引导整个团队向⼀个能够最⼤化⽣产⼒的系统设计⽅向前进。也许软件架构师⽣产的代码量不是最多的,但是他们必须不停地承接编程任务。如果不亲身承受因系统设计⽽带来的麻烦,就体会不到设计不佳所带来的痛苦,接着就会逐渐迷失正确的设计⽅向。
设计软件架构的⽬的,就是为了在⼯作中更好地对这些组件进⾏研发、部署、运⾏以及维护。
如果想设计⼀个便于推进各项⼯作的系统,其策略就是要在设计中尽可能⻓时间地保留尽可能多的可选项。
软件架构设计的主要⽬标是⽀撑软件系统的全⽣命周期,设计良好的架构可以让系统便于理解、易于修改、⽅便维护,并且能轻松部署。软件架构的终极⽬标就是最⼤化程序员的⽣产⼒,同时最⼩化系统的总运营成本。
为了让开发成为有效的⼯作,软件系统就必须是可部署的。在通常情况下,⼀个系统的部署成本越⾼,可⽤性就越低。因此,实现⼀键式的轻松部署应该是我们设计软件架构的⼀个⽬标。
软件有⾏为价值与架构价值两种价值。这其中的第⼆种价值⼜⽐第⼀种更重要,因为它正是软件之所以“软”的原因。
软件被发明出来就是因为我们需要⼀种灵活和便捷的⽅式来改变机器的⾏为。⽽软件的灵活性则取决于系统的整体状况、组件的布置以及组件之间的连接⽅式。
我们让软件维持“软”性的⽅法就是尽可能⻓时间地保留尽可能多的可选项。
基本上,所有的软件系统都可以降解为策略与细节这两种主要元素。策略体现的是软件中所有的业务规则与操作过程,因此它是系统真正的价值所在。
⽽细节则是指那些让操作该系统的⼈、其他系统以及程序员们与策略进⾏交互,但是⼜不会影响到策略本身的⾏为。它们包括I/O设备、数据库、Web系统、服务器、框架、交互协议等。
⼀个优秀的软件架构师应该致⼒于最⼤化可选项数量。
优秀的架构师会⼩⼼地将软件的⾼层策略与其底层实现隔离开,让⾼层策略与实现细节脱钩,使其策略部分完全不需要关⼼底层细节,当然也不会对这些细节有任何形式的依赖。另外,优秀的架构师所设计的策略应该允许系统尽可能地推迟与实现细节相关的决策,越晚做决策越好。
⼀个设计良好的软件架构必须⽀持以下⼏点。
- 系统的⽤例与正常运⾏。
- 系统的维护。
- 系统的开发。
- 系统的部署。
⽤例
如果某系统是⼀个购物⻋应⽤,那么该系统的架构就必须⾮常直观地⽀持这类应⽤可能会涉及的所有⽤例。
⼀个设计良好的架构在⾏为上对系统最重要的作⽤就是明确和显式地反映系统设计意图的⾏为,使其在架构层⾯上可⻅。
开发
任何⼀个组织在设计系统时,往往都会复制出⼀个与该组织内沟通结构相同的系统。
部署
⼀个系统的架构在其部署的便捷性⽅⾯起到的作⽤也是⾮常⼤的。设计⽬标⼀定是实现“⽴刻部署”。⼀个设计良好的架构通常不会依赖于成堆的脚本与配置⽂件,也不需要⽤户⼿动创建⼀堆“有严格要求”的⽬录与⽂件。总⽽⾔之,⼀个设计良好的软件架构可以让系统在构建完成之后⽴刻就能部署。
保留可选项
⼀个系统的架构在其部署的便捷性⽅⾯起到的作⽤也是⾮常⼤的。设计⽬标⼀定是实现“⽴刻部署”。⼀个设计良好的架构通常不会依赖于成堆的脚本与配置⽂件,也不需要⽤户⼿动创建⼀堆“有严格要求”的⽬录与⽂件。总⽽⾔之,⼀个设计良好的软件架构可以让系统在构建完成之后⽴刻就能部署。
⼀个设计良好的架构应该通过保留可选项的⽅式,让系统在任何情况下都能⽅便地做出必要的变更。
⽤例的解耦
如果我们按照变更原因的不同对系统进⾏解耦,就可以持续地向系统内添加新的⽤例,⽽不会影响旧有的⽤例。如果我们同时对⽀持这些⽤例的UI和数据库也进⾏了分组,那么每个⽤例使⽤的就是不同⾯向的UI与数据库,因此增加新⽤例就更不太可能会影响旧有的⽤例了。
解耦的模式
请记住,⼀个设计良好的架构总是要为将来多留⼀些可选项,这⾥所讨论的解耦模式也是这样的可选项之⼀。
开发的独⽴性
只要系统按照其⽔平分层和⽤例进⾏了恰当的解耦,整个系统的架构就可以⽀持多团队开发,不管团队组织形式是分功能开发、分组件开发、分层开发,还是按照别的什么变量分⼯都可以。
重复
如果有两段看起来重复的代码,它们⾛的是不同的演进路径,也就是说它们有着不同的变更速率和变更缘由,那么这两段代码就不是真正的重复。
我们经常遇到⼀些不同的⽤例为了上述原因被耦合在了⼀起。不管是因为它们展现形式类似,还是使⽤了相似的语法、相似的数据库查询/表结构等,总之,我们⼀定要⼩⼼避免陷⼊对任何重复都要⽴即消除的应激反应模式中。⼀定要确保这些消除动作只针对那些真正意义上的重复。
划分边界
软件架构设计本身就是⼀⻔划分边界的艺术。边界的作⽤是将软件分割成各种元素,以便约束边界两侧之间的依赖关系。
在项⽬初期划分这些边界的⽬的是⽅便我们尽量将⼀些决策延后进⾏,并且确保未来这些决策不会对系统的核⼼业务逻辑产⽣⼲扰。
架构师们所追求的⽬标是最⼤限度地降低构建和维护⼀个系统所需的⼈⼒资源。那么我们就需要了解⼀个系统最消耗⼈⼒资源的是什么?答案是系统中存在的耦合——尤其是那些过早做出的、不成熟的决策所导致的耦合。
怎样的决策会被认为是过早且不成熟的呢?
答案是那些决策与系统的业务需求(也就是⽤例)⽆关。这部分决策包括我们要采⽤的框架、数据库、Web服务器、⼯具库、依赖注⼊等。在⼀个设计良好的系统架构中,这些细节性的决策都应该是辅助性的,可以被推迟的。⼀个设计良好的系统架构不应该依赖于这些细节,⽽应该尽可能地推迟这些细节性的决策,并致⼒于将这种推迟所产⽣的影响降到最低。
数据库应该是业务逻辑间接使⽤的⼀个⼯具。业务逻辑并不需要了解数据库的表结构、查询语⾔或其他任何数据库内部的实现细节。业务逻辑唯⼀需要知道的,就是有⼀组可以⽤来查询和保存数据的函数。这样⼀来,我们才可以将数据库隐藏在接⼝后⾯。
业务逻辑
我们通常称这些逻辑为“关键业务逻辑”,因为它们是⼀项业务的关键部分,不管有没有⾃动化系统来执⾏这项业务,这⼀点是不会改变的。
“关键业务逻辑”通常会需要处理⼀些数据,例如,在借贷的业务逻辑中,我们需要知道借贷的数量、利率以及还款⽇程。
我们将这些数据称为“关键业务数据”,这是因为这些数据⽆论⾃动化程序存在与否,都必须要存在。
关键业务逻辑和关键业务数据是紧密相关的,所以它们很适合被放在同⼀个对象中处理。我们将这种对象称为“业务实体(Entity)”。
软件的系统架构应该为该系统的⽤例提供⽀持。这就像住宅和图书馆的建筑计划满篇都在⾮常明显地凸显这些建筑的⽤例⼀样,软件系统的架构设计图也应该⾮常明确地凸显该应⽤程序会有哪些⽤例。
架构设计不是(或者说不应该是)与框架相关的,这件事不应该是基于框架来完成的。对于我们来说,框架只是⼀个可⽤的⼯具和⼿段,⽽不是⼀个架构所规范的内容。如果我们的架构是基于框架来设计的,它就不能基于我们的⽤例来设计了。
⼀个良好的架构设计应该围绕着⽤例来展开,这样的架构设计可以在脱离框架、⼯具以及使⽤环境的情况下完整地描述⽤例。
良好的架构设计应该尽可能地允许⽤户推迟和延后决定采⽤什么框架、数据库、Web服务以及其他与环境相关的⼯具。
良好的架构设计应该只关注⽤例,并能将它们与其他的周边因素隔离。
事实上,关于⼀个应⽤程序是否应该以Web形式来交付这件事,它本身就应该是⼀个被推迟和延后的决策。⼀个系统应该尽量保持它与交付⽅式之间的⽆关性。
我们⼀定要带着怀疑的态度审视每⼀个框架。是的,采⽤框架可能会很有帮助,但采⽤它们的成本呢?我们⼀定要懂得权衡如何使⽤⼀个框架,如何保护⾃⼰。⽆论如何,我们需要仔细考虑如何能保持对系统⽤例的关注,避免让框架主导我们的架构设计。
⼀个系统的架构应该着重于展示系统本身的设计,⽽并⾮该系统所使⽤的框架。如果我们要构建的是⼀个医疗系统,新来的程序员第⼀次看到其源码时就应该知道这是⼀个医疗系统。新来的程序员应该先了解该系统的⽤例,⽽⾮系统的交付⽅式。
框架可以被当成⼯具来使⽤,但不需要让系统来适应框架。
外层圆代表的是机制,内层圆代表的是策略。
源码中的依赖关系必须只指向同⼼圆的内层,即由低层机制指向⾼层策略。
任何属于内层圆中的代码都不应该牵涉外层圆中的代码,尤其是内层圆中的代码不应该引⽤外层圆中代码所声明的名字,包括函数、类、变量以及⼀切其他有命名的软件实体。
外层圆中使⽤的数据格式也不应该被内层圆中的代码所使⽤,尤其是当数据格式是由外层圆的框架所⽣成时。总之,我们不应该让外层圆中发⽣的任何变更影响到内层圆的代码。
业务实体
业务实体这⼀层中封装的是整个系统的关键业务逻辑,⼀个业务实体既可以是⼀个带有⽅法的对象,也可以是⼀组数据结构和函数的集合。
接⼝适配器
软件的接⼝适配器层中通常是⼀组数据转换器,它们负责将数据从对⽤例和业务实体⽽⾔最⽅便操作的格式,转化成外部系统(譬如数据库以及Web)最⽅便操作的格式。
强⼤的可测试性是⼀个架构的设计是否优秀的显著衡量标准之⼀。
数据结构指的就是数据的载体,暴露数据,⽽⼏乎没有有意义的⾏为。数据传输对象(DTO)模式,DTO(Request/Response)就是⼀个很典型的数据载体,只存在简单的get,set属性,并且更倾向于作为值对象存在。⽽对象则刚好相反作为⾯向对象的产物,必须封装隐藏数据,⽽暴露出⾏为接⼝。
迪⽶特法则(Law of Demeter)⼜叫作最少知识原则(The Least Knowledge Principle),⼀个类对于其他类知道的越少越好,就是说⼀个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌⽣⼈说话。英⽂简写为: LOD。
对象关系映射器(ORM)事实上是压根就不存在的。道理很简单,对象不是数据结构。⾄少从⽤户的⻆度来说,对象内部的数据应该都是私有的,不可⻅的,⽤户在通常情况下只能看到对象的公有函数。因此从⽤户⻆度来说,对象是⼀些操作的集合,⽽不是简单的数据结构体。
所以ORM更应该被称为“数据映射器”,因为它们只是将数据从关系型数据库加载到了对应的数据结构中。
因为跨边界的通信肯定需要⽤到某种简单的数据结构,⽽边界会⾃然⽽然地将系统分割成难以测试的部分与容易测试的部分,所以通过在系统的边界处运⽤谦卑对象模式,我们可以⼤幅地提⾼整个系统的可测试性。
谦卑对象模式
谦卑对象模式[11]最初的设计⽬的是帮助单元测试的编写者区分容易测试的⾏为与难以测试的⾏为,并将它们隔离。其设计思路⾮常简单,就是将这两类⾏为拆分成两组模块或类。其中⼀组模块被称为谦卑(Humble)组,包含了系统中所有难以测试的⾏为,⽽这些⾏为已经被简化到不能再简化了。另⼀组模块则包含了所有不属于谦卑对象的⾏为。
例如,GUI通常是很难进⾏单元测试的,因为让计算机⾃⾏检视屏幕内容,并检查指定元素是否出现是⾮常难的事情。然⽽,GUI中的⼤部分⾏为实际上是很容易被测试的。这时候,我们就可以利⽤谦卑对象模式将GUI的这两种⾏为拆分成展示器与视图两部分。
不完全边界
构建不完全边界的⼀种⽅式就是在将系统分割成⼀系列可以独⽴编译、独⽴部署的组件之后,再把它们构建成⼀个组件。
Kent Beck描述了软件构建过程中的三个阶段(引号部分是他的原话,楷体部分是我的注解):
- 1.“先让代码⼯作起来”——如果代码不能⼯作,就不能产⽣价值。
- 2.“然后再试图将它变好”——通过对代码进⾏重构,让我们⾃⼰和其他⼈更好地理解代码,并能按照需求不断地修改代码。
- 3.“最后再试着让它运⾏得更快”——按照性能提升的“需求”来重构代码
这三点即是开发的三个准则。但是第⼀点和第⼆点是可以同步进⾏的。真正的好代码是不需要优化的,因为在写的时候就已经优化了,不⽤再次特意去优化。但如果时间很急的话,那只能把第⼀步和第⼆步拆开,但是后⾯⼀定抽时间尽快优化,因为"破窗效应",很可能代码会越来越糟。
在实践中学习正确的⼯作⽅法,然后再重写⼀个更好的版本。
关于框架
我们可以使⽤框架——但要时刻警惕,别被它拖住。我们应该将框架作为架构最外圈的⼀个实现细节来使⽤,不要让它们进⼊内圈。
如果框架要求我们根据它们的基类来创建派⽣类,就请不要这样做!我们可以创造⼀些代理类,同时把这些代理类当作业务逻辑的插件来管理。
另外,不要让框架污染我们的核⼼代码,应该依据依赖关系原则,将它们当作核⼼代码的插件来管理。
以Spring为例,它作为⼀个依赖注⼊框架是不错的,也许我们会需要⽤Spring来⾃动连接应⽤程序中的各种依赖关。这不要紧,但是千万别在业务对象⾥到处写@autowired注解。业务对象应该对Spring完全不知情才对。
反之,我们也可以利⽤Spring将依赖关系注⼊到Main组件中,毕竟Main组件作为系统架构中最低层、依赖最多的组件,它依赖于Spring并不是问题。
系统架构设计中的第⼀步,是识别系统中的各种⻆⾊和⽤例。
Web控制器永远不应该直接访问数据层。
静态分析⼯具(例如Ndepend、Structure101、Checkstyle)来在构建阶段⾃动检查违反架构设计规则的代码。
按组件封装
⽬标是将⼀个粗粒度组件相关的所有类放⼊⼀个Java包中。
架构整洁之道 3~6章读书笔记
第2部分 从基础构件开始:编程范式
第3章 编程范式总览
三个编程范式包括:结构化编程(structured programming)、面向对象编程(object-oriented programming)以及函数式编程(functional programming)。
结构化编程
结构化编程对程序控制权的直接转移进行了限制和规范。
面向对象编程
面向对象编程对程序控制权的间接转移进行了限制和规范。
函数式编程
函数式编程对程序中的赋值进行了限制和规范。
仅供思考
没有一个范式是增加新能力的。也就是说,每个编程范式的目的都是设置限制。这些范式主要是为了告诉我们不能做什么,而不是可以做什么。
这三个编程范式分别限制了goto语句、函数指针和赋值语句的使用。
本章小结
多态是我们跨越架构边界的手段,函数式编程是我们规范和限制数据存放位置与访问权限的手段,结构化编程则是各模块的算法实现基础。这和软件架构的三大关注重点不谋而合:功能性、组件独立性以及数据管理。
第4章 结构化编程
科学理论和科学定律的特点:它们可以被证伪,但是没有办法被证明。
我们可以说数学是要将可证明的结论证明,而与之相反,科学研究则是要将可证明的结论证伪。
Dijkstra曾经说过“测试只能展示Bug的存在,并不能证明不存在Bug”
结构化编程范式中最有价值的地方就是,它赋予了我们创造可证伪程序单元的能力。
无论在哪一个层面上,从最小的函数到最大组件,软件开发的过程都和科学研究非常类似,它们都是由证伪驱动的。软件架构师需要定义可以方便地进行证伪(测试)的模块、组件以及服务。为了达到这个目的,他们需要将类似结构化编程的限制方法应用在更高的层面上。
第5章 面向对象编程
封装
C程序在头文件中进行数据结构以及函数定义的前置声明(forward declare),然后在程序文件中具体实现。程序文件中的具体实现细节对使用者来说是不可见的。C++作为一种面向对象编程语言,反而破坏了C的完美封装性。
继承
继承的主要作用是让我们可以在某个作用域内对外部定义的某一组变量与函数进行覆盖。
虽然面向对象编程在继承性方面并没有开创出新,但是的确在数据结构的伪装性上提供了相当程度的便利性。
多态
归根结底,多态其实不过就是函数指针的一种应用。
采用面向对象编程语言让多态实现变得非常简单,让一个传统C程序员可以去做以前不敢想的事情。综上所述,我们认为面向对象编程其实是对程序间接控制权的转移进行了约束。
程序应该与设备无关
依赖反转
关系的方向和控制流正好是相反的,我们称之为依赖反转。这种反转对软件架构设计的影响是非常大的。
通过依赖反转,软件架构师可以完全控制采用了面向对象这种编程方式的系统中所有的源代码依赖关系,而不再受到系统控制流的限制。不管哪个模块调用或者被调用,软件架构师都可以随意更改源代码依赖关系。
简单来说,当某个组件的源代码需要修改时,仅仅需要重新部署该组件,不需要更改其他组件,这就是独立部署能力。
如果系统中的所有组件都可以独立部署,那它们就可以由不同的团队并行开发,这就是所谓的独立开发能力。
本章小结
面向对象编程就是以多态为手段来对源代码中的依赖关系进行控制的能力,这种能力让软件架构师可以构建出某种插件式架构,让高层策略性组件与底层实现性组件相分离,底层组件可以被编译成插件,实现独立于高层组件的开发和部署。
第6章 函数式编程
函数式编程语言中的变量(Variable)是不可变(Vary)的。
不可变性与软件架构
一切并发应用遇到的问题,一切由于使用多线程、多处理器而引起的问题,如果没有可变变量的话都不可能发生。
可变性的隔离
一种常见方式是将应用程序,或者是应用程序的内部服务进行切分,划分为可变的和不可变的两种组件。不可变组件用纯函数的方式来执行任务,期间不更改任何状态。这些不可变的组件将通过与一个或多个非函数式组件通信的方式来修改变量状态。
一个架构设计良好的应用程序应该将状态修改的部分和不需要修改状态的部分隔离成单独的组件,然后用合适的机制来保护可变量。
软件架构师应该着力于将大部分处理逻辑都归于不可变组件中,可变状态组件的逻辑应该越少越好。
事件溯源
在事件溯源体系下,我们只存储事务记录,不存储具体状态。当需要具体状态时,我们只要从头开始计算所有的事务即可。
这种数据存储模式中不存在删除和更新的情况,我们的应用程序不是CRUD,而是CR。因为更新和删除这两种操作都不存在了,自然也就不存在并发问题。
本章小结
软件,或者说计算机程序无一例外是由顺序结构、分支结构、循环结构和间接转移这几种行为组合而成的,无可增加,也缺一不可。
以上是关于笔记架构整洁之道的主要内容,如果未能解决你的问题,请参考以下文章