对软件架构三大原则的思考与批判:KISS、SOLID、内紧外松

按:《原则》、《穷宝典》都在告诉你要学会去构建自己的原则。关于互联网技术的原则Rule很多、Law很少,大多都是经验性的。在架构设计过程中大家都容易互喷,因为极少人思考Law。KISS、SOLID、内紧外松是经典的三大原则,没有经过反思和批判的学习,是不彻底的,是一知半解的。本文来自王概凯老师的分享,希望给大家一些启发。

在架构设计的领域,总结出了很多原则。这些原则的很简略,容易传播。但是提出这些原则的往往不会告诉你,为什么应该是这样的原则。哪怕说了背景,过了时间,听的能已经不知道原则提出初衷。这些原则,粗看起来是很有道理,可是在实践中,却往往不是这么回事,那么就沦为鸡汤了。在看这些原则的时候,每个要形成的判断能不要亦云才好。以下是个设计原则的思考,不正确,期望能够引发读者的思考,形成读者的判断。

 

KISS 原则

KISS(Keep It Simple, Stupid) 原则,翻译成中是“保持简单、愚蠢”。这是没有主语的话,猜想主语应该是指设计师,并且这个“It”应该指的是设计师所设计的系统。这条原则应该是告诉设计师在设计时要保持系统的“Simple and Stupid”。这个原则仔细分析,有两个题:

,“Simple and Stupid”的判断原则是什么,怎样才算是“Simple and Stupid”是这个原则中最让惑的地意识给出的是所认知的“Simple and Stupid”,这未必是提出这条原则的理解的“Simple andStupid”。比如让体操队员来做后空翻,对于他来说,这是“Simple and Stupid”。可是让没经过体操训练的普通后空翻,这绝对不“Simple and Stupid”,可能会摔断脖的。

也就是说“Simple and Stupid”是因异的,对于技术不同的他会的或者他能够熟练掌握的技术才是“Simple and Stupid”。很设计师,看到系统,他可以给出他设计的“Simple and Stupid”,可是如果实现的平很糟糕、技术达不到,勉强去实施的话,有可能会实现不出来的,哪怕做出来了也是问题多多,弄不好要搞出情。因此,要设计系统,要根据实施团队的,做适合他们的架构设计,这样才可以算得上是“Simple and Stupid”。

其次,系统的是为了给来使。设计师设计了“Simple and Stupid”的系统,那么这个系统对来说就好果了设计师,或者说了实施,说不定会影响使统的呢恰是让来“Simple and Stupid”的系统,才是好系统。那么给系统的设计师“Simple and Stupid”的原则,到底是在帮助谁害谁合业务吗多公司的设计师在设计系统的时候,不断的和业务团队冲突,为了保持设计师所持的“Simple and Stupid”理念,很容易会降低的体验,导致最后也都不“Simple and Stupid”。

可见,这个设计原则软件是有问题的,我们不能够为了“Simple and Stupid”去设计,不能够设计系统时,依照“Simple and Stupid”为去设计。因为设计系统的是为了更好的完成系统所服务的的访问,是为了设计者或者实施者。考虑设计者和实施者的时候,是在这个完成访问的前提下,怎么做到低成本的、可持续的迭代。因此,如果设计者不能够理解的需求,不能够理解通过不同的访问周期来达到的,是无法设计好系统的。

只有通过对的业务周期、访问周期进析,根据流量的压同,进理的树状拆分,也因此形成不同的系统,那么这些所形成的系统是内聚的,边界是清晰的,也是“Simple and Stupid”。也就是说,只有从业务上去分析、去拆分,才能够得到“Simple and Stupid”的结构,这是副产品,是。如果依照这个原则为去设计,则可能会破坏业务本身的整体。

这个原则本身是从军业出来的,说明“Simple and Stupid”含义是有军景的,有军业的标准,读者感兴趣的话可以去研究。不去了解概念的历史,盲直接引软件来,很容易吃亏。其中“Stupid”的含义是为了形容系统组件的修理维护简单程度,不知道这是不是这个原则提出者的“Simple and Stupid”的本意。按照这个意思,如果所设计的系统根本不允许或不需要考虑维护或者修理的话,还需要考虑“Simple and Stupid”吗过来,我们回头去审查系统的时候,如果发现所设计的系统对于访问的拆分不够清晰,不是树状结构的时候,那么是不够“Simple and Stupid”的,倒是可以作为架构审查的判断点,帮助改进设计,但是不能够作为设计的依据。

 

SOLID 原则

SOLID 原则,据 WikiPedia 所说是由 Robert C. Martin 总结的对象设计的原则。这个名字其实是以下原则的母简写:

  • Single responsibility principle;

  • Open/closed principle;

  • Liskov substitution principle;

  • Interface segregation principle;

  • Dependency inversion principle。

“Single responsibility principle” ,翻译成中“单责原则”。这也是缺乏主语的话,但推断应该是指所设计的系统吧,这个系统应该是单责的。可是这个“职责”的“单,如何来判定呢同的有不同的认知。据作者原给出的参考所描述的:“This principle was described in the work of Tom DeMarco and Meilir Page-Jones . They called it cohesion”,原本叫做 Cohesion,翻译成中“内聚”。说成内聚很容易理解,但是作者给出的解释是“A class should have only one reason to change”,就很难理解了。据给出的例说的是保龄球的游戏编程,原本 Game 类有两个责任,是负责跟踪当前帧,是负责计算分数,最后把这两个责任分别给了两个不同的类。作者给出这个拆分的理由是,“Because each responsibility is an axis of change”,意思是“因为每个职责是变化的维度”。猜想作者想表达的意思是两个正交的维度,拆开可以互不影响的意思。

原本现实中打保龄球,可以算分,也可以让别忙算。为什么可以拆分开来,这是因为打保龄球的核命周期是打球,算分只是游戏规则,没有这个规则,保龄球也可以打的,因此这个分数计算规则可以拆分出来。并且保龄球游戏产结果是计算分数的输入,这两个步骤是打保龄球游戏的两个连续的周期活动,因此非核命周期可以拆分出去,形成树状结构。Game 的原本功能没变,只不过其中步骤的实现分离出去,通过调回归了。这样 Game 的职责更专注,分数计算也更专注,修改时可以互不影响,确实叫“内聚”比较好。

可是改成“单责”,意思就变化了。后把详细解释的内容从“an axis of change”改成“one reason to change”,意思进不同了。“an axis of change”指的是维度,one reason to change”指的是理由,很难等同,应该是有很争议的。

那么怎么样才算“单呢个是没有确定的标准的。Game 包含打球和算分两个步骤,难道 Game 就不“单了吗龄球要打球和算分的话,这是“单的运动,放在并不算职责不单这样做并不错。但是后续修改和维护的角度来看,如果分数计算规则要频繁的修改,但不希望动 Game 的话,分数计算可以拆分出来,这是架构拆分,但并不是因为“单责”的缘由才拆分的。那么打球和分数计算分离开来了后,难道分数计算职责就“单了吗,如果分数计算有很多不同规则,还可以把规则做架构拆分,分数计算职责也并不“单呀。

因此,我们说“单职责,并不能表述“内聚”的含义。“单是相对的词语,要看针对什么来说是“单的。事情分两个步骤,并不能说这个事情不“单,这是事情,是单。把这两个步骤分开后,由两个分别执对于这两个说,各职责是单,但是不能因此否认原来这个事情就不“单了,因为这两个单职责的完成,组成了原本的那个“单的事情。其实从原作者的本意来看,不过是想表述“内聚”,“内聚”这个词最贴切。

从“单的思路去看,最近出现的 CQRS(Command Query Responsibility Segregation),“命令查询责任分离”,把命令和查询拆分开来,分开后这个职责“单吧,可是这个做法却完全破坏了业务本身的内聚。还是前个保龄球的例想象算分和查询分数是不同的类,那么算分的规则发化,那么查询分数的规则不能够跟着算分来变化,Bug 就很容易出现。如果这两个类分为两个不同的维护的话,出现问题的话,这两个可以没完没了的扯,责任也很难分清楚,需要的沟通成本,最后会糟,这就是破坏了业务本身的内聚所带来的后果。CQRS 这个做法往往是数据读写的场景,提升读或写的性能,只有当读、写时不存在业务逻辑的时候、仅仅是做读写通道的拆分的时候才。

Open/closed principle ,也就是“开 / 闭原则”。作者总结了这个原则的发明者 Bertrand Meyer 的观点“Software Entities (Classes, Modules, Functions, etc.)should be open for extension, but closed for modification.”,作者对这句话的理解是,模块同时要能够适应新的变化,还要不允许修改,这是相冲突的。为解决这个冲突,作者得出,那就是象类来解决这个问题。象类则会带来许许多多的其他问题,但仍然无法完全做到对修改关闭。

为什么会出现这个原则呢这个原则出现的时期来看,是在 1980 年代提出来的,猜想是因为软件不够发达,开发惧怕改变所导致的。为什么要对修改关闭呢为害怕修改所带来的连锁反应。在早期的开发实践中,没有完善的版本控制、依赖管理等的帮助,无法承担频繁的修改所带来的对项冲击。另可能是想要去修改别代码或者类库,许多此的解释。或许也有可能也不得。如果能够去修改别代码或者类库,说明已经有源代码了,做好版本控制即可,可以维护。如果想让的修改还要兼容别后续升级,无论是修改还是继承,都会遇到兼容的问题,这种情况最好的办法是去请求原作者来修改,保持权责对等是成本最低的。修改的话,就要做好维护的准备,并且后续升级的代价会比较可是为什么要去修改别代码或类库呢果是功能不满就不要去代码,换别的类库或者写果是有 bug,就去请求原作者去修复,尽量不要动改者参加进去成为贡献者也可以。总之要保持代码创造者对其代码的权责对等。

随着现代开发理念的发展,越来越多的到了抽象、继承的坏处,越来越多的合的来协作,其实抽象类可以看成是组合的特殊情况。随着代码的变化越来越频繁,拥抱变化反为了风只要代码中的类做到了“内聚”,只要业务代码能够做到内聚、访问通道做到不重那么要重只会是业务代码,这样修改的范围会多,同时依靠版本与依赖管理,完全可以避免修改所产影响。因此这个“开 / 闭原则”,也需要重新再看待,理性使使闭原则,就意味着的抽象类、的继承,意味着内聚的丧失,意味着要付出耦合的代价。

Liskov substitution principle ,中“代换原则”。前“开 / 闭原则”导致了抽象与继承,“代换原则”则是继承的进体现,也最终形成了多态的特性。作者总结了发明者 Barbara Liskov 的话“Function that use pointers or references to base class must be able to use objects of derived classes without knowing it”,意思就是功能如果引是某个,如果实际传的是该的的话,这个功能本身的不会发化。这个原则是很多程序员喜欢抽象的理论来源,这着分析。

代换原则的本意,应该是对开闭原则的拓展,实现开闭原则。只有能够类来代换,才能够符合开闭原则。但是总不能够每次修改都创建吧因此可以看到,开闭原则也是无奈之举。正确的做法是针对修改创建不同的版本,针对不同的版本来进建、发布。

但是有了这个代换的办法,结果倒是不遵守开闭原则了,尽可能的抽象,结果把本来应该内聚在类中的和属性,分散到许多不同的中去了,这是很弊病。记得以前 Java 认证考试就专门考继承时的变量初始化,许多进这个坑并且这种情况非常容易造成事故,因为这种错误只有在运才能够发现,还不好排查,往往修改时,的 bug 就出现了。没有做到内聚的后果是很严重的。

另外问题是代换的时候,比如中有 Instrument.play(),可以iano.play(),Violin.play() 来代换,虽然引类时可以的很正常,但是 play() 出来的声是不确定的,因此也不能说没有发化,只能说都能够 play,但是 play 的结果是不的。但是当实际业务很复杂,不光要 play,后续还要调整具体的乐器的话,这个抽象就比较麻烦了,因为不同乐器调整各式各样不同,然后就发现原来的抽象不够要费很进抽象。慢慢的在业务的变化下,抽象就变成糟了,最后连也看不懂代码了。

可是何必要花去抽象呢接引际的乐器就好了。除非能够做到抽象能够适应以后所有的变化,否则还是实实的实际情况吧,哪怕有多个乐器需要选择,写个 if-else 就好了,没代码,并且还是可测试的,并且错误是编译期可以发现的,以后修改、扩展也容易。为了这代码,引入那么多抽象,破坏“内聚”不说,代换时,都是运才能确定的,反致运探查问题的麻烦,同时,代码也很难阅读,没去修改,影响质量。

Interface segregation principle ,即“接离原则”。这个原则相当于是预设了调与被调两前提,对于调来讲,被调的接量应该最。这个原则其实就是通道访问的隔离。在访问通道上,不同的客户端,不可以使样的访问通道,因为会导致它们之间的访问互相影响,这是很简单的道理。比如居民的车道和道必须要分离,否则两者通道混杂的话,会出事情的,很容易有危险,产外的问题。

可是为什么会变成恰就是为了要重个接以便让各种不同的调来访问。所以访问通道上的重万万不可的,因此也会导致服务端会变成从慢会变成团队之间的纠纷点,故障点。

Dependency inversion principle ,即“依赖倒置原则”。作者举了Copy 的例本来是把字符从键盘 copy 到打印机,后来增加了需求要 copy 到磁盘,因此要重opy 的,以后可能还要有多个设备要读写,不希望 Copy 程序依赖设备,因此引入了抽象类,放在 Copy 程序和具体的设备之间。

其实这个例业务背景只是利opy 程序作了字符传递的通道,这个通道本身是没有任何逻辑的。作者强制性的把不同设备之间的通道绑在了,相当于是共享了设备之间 copy 的访问通道,在业务上来说,这是不符合通道独原则的,因为这些不同的通道可能是属于不同的业务与不同的,他们之间的需求后续可能会不,很有可能因为某个通道的修改,导致其他的通道受到影响。

个例仅是为了在编译期间不依赖于设备,为了这个这么代价,有什么意义吗终运间对设备的依赖是逃不掉的。其实访问通道依赖于设备是没有关系的,因为通道没有逻辑,不需要测试。所以这种通道的共享是过度设计,根本没有必要。copy 有的逻辑、这个逻辑可能还与通道有关的话,那么这么多通道混杂在,反外的增加了复杂度。并且这个 Copy 程序根本没有必要重因为没有逻辑就没有重价值。如果要从键盘写到磁盘,不如重新写CopytoDisk 更简单,因为可能不,来也更简单,也更独

作者这个 Copy 程序例场景可能举的不好,但是依赖倒置也是有的场景的,不是什么时候都需要。依赖倒置的做法,无非是在两个步骤之间增加节点,其实就是作了架构拆分,形成了新的访问通道,形成了树状的结构。这个依赖叫“倒置”也不太对,只不过变成了依赖中间增加的那个节点,避免了直接依赖,但是间接依赖还是在的。真正正确的架构拆分,其依赖是树状的,从拆分的起点开始往树的下层依赖,不会出现下层依赖上层的情况,甚至不会出现兄弟节点之间的依赖,因为他们都是从顶层拆分下来的,是访问通道上的不同节点。

发构拆分,就意味着要管理这个新增加节点的周期,也意味着额外的成本。只有相依赖的两对对造成影响的时候,才需要通过拆分增加节点,以便让两以独,互不影响。并且增加的这个节点不是 Abstract 的,也可以是实体。建议体,不要bstract,因为依赖于 Abstract 意味着依赖继承树,成本太俗的话说,尽量去正规公司,不要去依赖公司,层级越少,沟通越少,效率越所以,不要始就去架构拆分,要根据当时所的情况,合理的采

所以,对于 SOLID 原则,第其实是说内聚,只是“单责”的提法不好。第第三个说的是继承的问题,这是对象语特性。继承会有很多的坑,会破坏内聚,也不合适。第四第五个,其实说的是访问通道的问题,只要做好访问通道的隔离就不会有问题。如果从这些原则的字的意思去理解,怕是要误区了。

 

业务内聚与访问通道内聚

当然,很多会提到“聚、低耦合”的原则。这个“低”的说法不够严谨。只要某个业务的周期活动不在类中确定,那么这个类就没有形成内聚,反之就是做到了内聚。只要做到内聚,就没耦合了,就只有依赖关系,这个依赖是树状的结构;只要没做到内聚,肯定耦合了,没有之分,最后都会带来麻烦,区别在于带来麻烦的多少。所以应么没有内聚,只有耦合,要么只有内聚,没有耦合,只有其中情况。

可是要做到业务的内聚,却离不开业务访问通道的隔离,这个原则我把它称作“访问通道不重原则。观察的关系可以发现,重务与重问通道,只能够选。因为重问通道会导致业务无法内聚、也就无法重重务则会导致访问通道无法重如果想两者都达成,那么最后的结果是只成功的重访问通道,务内聚则会被破坏。

为什么会是这样呢为事物对物理空间的占有是独享的,问通道则是事物跨越物理空间的通路。必须确保对事物的访问通道是独享的,才能够保证这个访问通道是内聚的。如果不同类型的共享同访问通道,就意味着访问通道不再是独占的了,这就是对访问通道内聚的破坏,最终这个访问通道就变成不确定的通路,内步冲突不断、阻碍重重,会反应到对业务内聚的破坏。

比如做公共的交通,往往不允许带宠物,这就是要遵守宠物和的访问通道内聚原则,因为宠物和在狭窄的空间存,会产出非常多额外、不必要的冲突。所以我们说“内聚”,绝对不能只提业务的内聚,访问也是独特的业务,也需要达到内聚的原则。也就是说,“访问通道不重原则其实说的就是“访问通道的内聚”。

如果做好了业务的内聚,并隔离不同类型客户端对业务的访问通道,形成访问通道的内聚,基本上程序就不会太差,代码就会很稳定。有了这个基础,再根据运营过程中所产瓶颈点,有针对性的做业务架构拆分或访问通道架构拆分就很容易了。做为架构设计师或者程序员,如果不把“内聚”放在最重要的位置,最终会被需求给淹没的。

因此,架构设计的的核则就是“内聚”,任何架构原则都不能违反此原则。这个“内聚”包括两部分:“业务内聚”,“业务访问通道内聚”。所以,对于我们遇到的任何架构原则都可以这样去判断:如果发现它违反了“业务内聚”原则,我们都要三思,因为会导致业务分散、无法重如果它违反“业务访问通道内聚”原则,也就是“业务访问通道不重原则,我们也要三思,不要去追求访问通道重

“访问通道内聚”原则是软件普遍忽视的,这个原则太不起眼,也太容易被破坏,都忽视了。“访问通道内聚”的缺失会导致“业务内聚”原则的破坏,导致业务无法重于是,系统就开始陷入困境了。

 


=>更多文章请参考:《中国互联网业务研发体系架构指南》

=>更多TOP权威案例及行业标准资料请关注微信公众号:

 

对软件架构三大原则的思考与批判:KISS、SOLID、内紧外松
更多内容关注公众号:软件真理与光

来源:软件真理与光

声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2020年1月15日
下一篇 2020年1月15日

相关推荐