后端面试38讲_21_19_组件设计原则组件的边界在哪里
你好,我是李智慧。
软件的复杂度呢和它的规模是成指数关系的。
软件开发这个行业很久以来就形成一个共识,应该将复杂的软件系统进行拆分拆,分成多个更低复杂度的子系统,子系统,还可以继续拆分成更小粒度的组件。
也就是说,软件需要进行模块化组件化的设计。
事实上,早在打孔纸袋编程时代,程序员们就开始尝试进行软件的组件化设计。
那些相对独立可以被复用的程序,被打在纸袋卡片上,放在一个盒子里。
当某个程序需要复用这个程序组件的时候呢,就把这一类纸带从盒子里取出来,放在要用行的其他纸带的前面,或者后面被光电读卡器一起扫描一起执行。
其如我们现在的组件开发与复用,跟这个也差不多。
比如我们用java开发,会把独立的组件编译成一个一个的架包,相当于把这些组件分装在一个一个的盒子里。
需要复用的时候呢,程序员只需要依赖这些架包运行的时候呢,只需要把这些依赖的架包放在class pass路径下,最后被GVM统一装载一起执行。
现在稍有规模的软件系统一定被拆分成很多个组件。
正是因为组件化设计,我们才能够开发出复杂的系统。
那么如何进行组件的设计呢?组件的力度应该多大?如何对组件的功能进行划分?组件的边界又在哪里?我们之前说过,软件设计的核心目标就是高内聚、低五核。
那么我们今天从这两个维度看看组件的设计原则。
组件内聚原则主要讨论哪些类应该聚合在同一个组件中,以便组件既能够提供相对完整的功能,又不至于太过庞大。
在具体的设计中,可以遵循以下三个原则。
第一个原则是复用发布、等同原则。
复用发布等同原则说,软件复用的最小力度应该等同于其发布的最小力度。
也就是说,如果你希望别人以怎样的力度复用你的软件,你就应该以怎样的力度发布你的软件。
这其实就是组件的定义呢。
组件是软件复用和发布的最小力度。
软件单元这个力度既是复用的力度,也是发布的力度。
同时如果你发布的组件会不断的变更,那么你就应该用版本号做好组件的版本管理。
以使组件的使用者能够知道自己是否需要进行组件版本升级,以及是否会出现组件不兼容的情况。
因此,组件的版本号应该遵循一个,大家都能够接受的约定,同时有一个版本号约定,建议供你参考,版本号格式主版本号点次版本号点修订号。
比如一点三点一二,以及应该m号中主版本号是一次,版本号是三,修订号是十二,主版本号升级,表示组件发生了不相嵌兼容的重大修订。
次版本号升级表示,组件进行了重要的功能修订或者是bug修复,但是组件是向前兼容的修订号升级,但示组件进行了不重要的功能修订或者bug修复。
第二个原则是共同封闭原则。
共同封闭原则说,我们应该将那些会同时修改,并且为了相同目的而修改的类放在同一个组件中,我将不会同时修改,并且不会为了相同目的而修改的类放到不同的组件中。
组件的目的虽然是为了复用,但是开发中常常引发问题的,恰恰在于组件本身的可维护性。
如果组件在自己的生命周期中必须经历各种变革,那么最好不要涉及其他组件相关的变更都在同一个组件中。
这样当变更发生的时候,只需要重新发布这个组件就可以了,而不是一大堆组件都受到牵连。
也许将某个类放在这个组件中,对于复用是便利的、合理的。
但是,如果组件的复用与维护发生冲突,比如这些类将来的变更和整个组件将来的变更是不同步的,不会由于相同的原因发生变更。
那么为了可维护性应该谨慎考虑,是不是应该将这些类和组件放在一起。
第三个原则是共同复用原则。
共同复用原则说不要强迫一个组件的用户依赖他们不需要的东西。
这个组件一方面是说我们应该将相互依赖共同复用的类放在一个组件中。
比如说一个数据结构容器,提供数组、哈希表等数据结构容器。
那么对于数据结构便利的类,培训的类,也应该放在这个组件中,以使这个组件中的类共同对外提供服务。
另一方面,这个原则也说明,如果不是被共同依赖的类,就不应该放在同一个组件中。
如果不被依赖的类发生了变更,那么就会引起组件变更,进而引起使用组件的程序发生变更,这样就会导致组件的使用者产生不必要的困扰,甚至讨厌使用这样的组件,也造成了组件复用的困难。
其实,以上三个组件内聚原则,相互之间也存在一些冲突。
比如共同复用原则和共同封闭原则。
一个强调易复用,一个强调易维护。
而这两者是有冲突的,但此这些原则可以用来指导组件设计师的考量。
但是想真正做出正确的设计决策,还需要架构师自己的经验和对场景的理解。
对这些原则进行权衡。
组件内聚原则讨论的是组件应该包含哪些功能和类。
而组件耦合原则讨论组件之间的耦合依赖关系应该如何设计?组件耦合关系设计也应该遵循三个原则。
第一个原则是无循环依赖原则,无循环依赖。
原则说,组件依赖关系中不应该出现环。
如果组件a依赖组件b组件b依赖组件c组件c又依赖组件a就形成了循环依赖。
很多时候循环依赖是在组件的变更过程中逐渐形成的。
组件a版本一点零依赖组件b版本一点零,后来组件b升级到了一点一升级的某个功能,依赖的组件a的一点零版本,于是就形成了循环依赖。
如果组件设计的边界不清晰,组件开发设计缺乏评审,开发者只关注自己开发的组件。
整个项目对组件依赖管理没有统一的版,则就很有可能会出现循环依赖。
而一旦系统内出现组件胸化,依赖系统就会变得非常不稳定,一个微小的bug就可能导致发生连锁反应。
在其他地方出现莫名其妙的问题,有时候甚至什么都没做头,一天还好好的系统,第二天就启动不了了。
在有严重胸化依赖的系统内开发代码,整个技术团队就好像在焦友坑里编程,什么也不敢动,什么也动不了,只有焦躁和沮丧。
第二个原则是稳定依赖原则,稳定依赖原则。
说组件依赖关系必须指向更稳定的方向,较少,变更的组件是稳定的,相反的,经常变更的组件是不稳定的,根据稳定依赖原则,不稳定的组件应该依赖稳定的主见,而不是反过来。
反过来说,如果一个组件被更多的组件依赖,那么它相对是稳定的。
因为想要变更一个被很多组件依赖的组件,本身就是一件困难的事。
相对应的。
如果一个组件依赖了很多的组件,那么它相对的也是不稳定的。
因为它依赖的任何抽件原则都可能会导致自己的变更稳定依赖原则。
通俗的说就是组件不应该依赖一个比自己还不稳定的组件。
第三个原则是稳定抽象原则。
这个原则说,一个组件的抽象化程度应该与其稳定性程度一致。
相就是说一个稳定的组件应该是抽象的,而不稳定的组件应该是具体的这个原则。
对具体开发的指导意义就是如果你设计的组件是具体的不稳定的,那么可以为这个组件对为提供服务的类设计一组接口,并且把这种接口分装在一个专门的组件中,那么这个组件相对就比较稳定。
抽象。
在具体实践中,这个抽象接口的组件设计也应该遵循前面专栏讲到的依赖倒置原则。
也就是说啊抽象的组件接口不应该由低层具体实现的组件定义。
而应该由高层使用组件定义高层使用组件依赖接口组件进行编程。
而低层实现组件依赖接口组件java的GDBC就是这样一个例子。
在GDK中定义GDBC接口组件。
这个接口组件位于java点cirq l包,我们开发应用程序的时候,只需要使用GDBC的接口编程就可以了。
而发布应用的时候,我们指定具体的实现组件,可以使mysql实现的GDBC组件也可以是oracle实现的GDBC组件。
组件的边界与依赖关系划分,不仅需要考虑技术问题,也需要考虑业务场景问题。
异变与稳定依赖与被依赖,都需要放在业务场景中去考察。
有的时候甚至不只是技术和业务的问题,还需要考虑人的问题。
而一个复杂的组织中,组件的依赖与设计需要考虑人的因素。
如果组件的功能划分涉及到部门的职责边界,甚至会和公司内的政治关联起来。
因此,公司的技术沉淀与实力,公司的业务情况、部门与团队的人情世故,甚至公司的过往历史都可能会对组件的设计产生影响。
而能够深刻了解这些情况的通常都是公司的一些老人。
所以,年龄大的程序员并不一定要和年轻程序员拼技术,甚至拼体力,应该发挥自己的所长,去做一些对自己、对公司更有价值的事。
在稳定抽象原则里,类似GDBC的例子还有很多,你能举几个吗?欢迎你在评论区写下你的思考。
也欢迎把这篇文章分享给你的朋友,或者同事一起交流一下。