软件架构的设计原则----只重剑意不重剑招

说明

  • 没有必要刻意地去学习那些前人总结出来的设计模式,因为看完以后过段时间肯定会全部忘掉,而且在平时的设计开发中很难准确地复用这些设计模式,如果生搬硬套地去套用设计模式,效果肯定是适得其反的。
  • 本文将会详细讲解软件设计中的五大设计原则,只要能深刻理解这五大设计原则、掌握基本的UML图知识(本文不介绍UML的基本知识,如果读者不了解UML图,那么请自己先Google一下,UML的基本知识10分钟完全可以学会,看不懂UML图,本文读起来会比较吃力)、再结合面向对象的三大特性,这样在平时的开发中就能熟练应用设计模式的技巧
  • 这五大设计原则是所有设计模式的灵魂与原则,那些前人总结出来的设计模式相当于是普通的剑招,而这五大设计原则相当于剑意。一名剑客如果对敌之时总是死板地套用用剑招,那么一定死的很难看,风清扬批评令狐冲就是这个意思。张三丰传给张无忌的太极剑只重剑意不重剑招,风清扬传给令狐冲的独孤九剑注重的也是剑意。最初风清扬还批评令狐冲岳不群的徒弟怎么这么死板,因为当风清扬地上捡起一块人骨随便摆出一个姿势时,死板的令狐冲便不知道如何应对这种招式。这根本原因在于令狐冲之前死记硬背的22种设计模式没有一个能完全应对这个场景,有两三种设计模式好像能套用到这个场景中去,但又不是完全切合,想生搬硬套但又无从下手。当令狐冲领悟了软件设计五大原则以后,面对任何场景都能灵活应对,即使面对武当冲虚道长这种顶尖高手,也能灵活运用剑意,看出冲虚道长太极的特点与破绽后取得胜利。试想如果令狐冲还是死板地套用所谓22种设计模式,当他面对自己从未见过的太极剑,只有挨打的份了
  • 本文先逐一介绍五大设计原则,然后结合笔者的项目经验进行一次模拟实战演练,充分运用这五大原则,帮助读者更深一步地了解这五大设计原则以及边界划分,这样才能在平时的开发设计中灵活运用,并且根据这五大原则介绍组件的概念和用法

五大设计原则

  • 单一职责
  • 开闭原则
  • 里氏替换
  • 接口隔离
  • 依赖反转

单一职责

概念:任何一个软件模块都应该只对某一类行为负责
说明:这里说的软件模块大到一个组件,小到一个类、一个函数,如果一个软件模块中掺杂着好几种不同的功能逻辑,代码维护起来就会很混乱,不同的行为之间也难以保证互不干扰。最好的方案就是将不同的功能逻辑解耦合,放到不同的软件模块中去。一般在软件设计中,变更原因相同的部分被放到同一个软件模块中,变更原因不同的部分被放到不同的软件模块中,这样就方便了每个软件模块各自的变更,各个软件模块以不同的变更速率朝着各自不同的变更方向演变,互不干扰(比如GUI显示、业务逻辑、数据存储系统就是三个不同的软件模块,分别有各自不同的变更原因和变更方向)

平时容易犯的错误—刻意消除重复代码

我们经常钻牛角尖-必须尽全力消除重复代码,因为在我们的观念里重复代码是万恶之源。有些情况是真重复,比如每个实例上发生的变更都必须同步到所有的副本上,这种是真重复。有两段看起来重复的代码,但是它们走的是不同的演化路径,有不同的变更原因和变更速率,过几年甚至几周以后这两段代码就会变得非常不一样了,这种是假重复。有时候为了刻意消除这种假重复的代码,会让两个职责单一独立的软件模块互相耦合在一起,那么将来肯定会花很大代价解耦合

开闭原则

概念:良好的计算机软件应该易于扩展,同时抗拒修改
说明:我们设计软件的时候,应该秉持着将来开发新功能只是纯粹地开发新代码而不会改动原来老代码的原则去设计,毕竟改动已经稳定运行很长时间的老代码是一件有风险的事情。开闭原则中,所谓的指的是开放增加新代码的自由,指的是关闭修改老代码的门路

里氏替换

概念:面向接口编程,能使用抽象接口的地方就不要使用实例化的类
说明:转运系统类中使用了transit_operation这个抽象接口,这个抽象接口可以被不同类进行实例化,这样transit_operation抽象接口相当于是一个可插拔设备,我可以根据自己的需要用台达电机驱动器 ABB电机驱动器 仿真电机驱动器三个不同功能的实例化类进行替换,如果产品再增加其他型号的电机需求,只需要按照规则再写一个类来实例化transit_operation抽象接口就行,大大增加了代码的灵活性
在这里插入图片描述

接口隔离原则

概念:设计单一接口,不要设计庞大臃肿的接口,尽量细化接口,接口中的方法尽量少,要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用
说明:依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的契约,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。当然接口设计的太多太细也会使设计变得过于复杂,这个设计的平衡点需要根据实际场景来判断

举例说明:图1-1中的接口就特别臃肿,类A只需要接口中的方法1、2、3,但是类B在实例化这个接口的时候需要把不需要的方法4、5也要实例化(红色部分)。类C只需要方法1、4、5,但是类D在实例化接口的时候要把不需要用到的方法2、3实例化(红色部分)。图1-2中我们根据接口隔离原则将这个庞大的接口分成三个小接口,这样每个类实例化接口的时候就不需要去实例化跟自己没关系的方法了
在这里插入图片描述
图 1-1
在这里插入图片描述
图1-2

依赖反转

这个原则乍一看有点绕,我把它的概念总结为4点:

  1. 代码中要多使用抽象的接口,避免直接使用那些多变的具体实现类,在上层应用代码中避免写入任何与具体实现类相关的名字,或者其他容易变动事物的名字
  2. 每次修改抽象接口后都必须修改具体的实现类。但反过来每次修改具体的实现类以后,不需要修改抽象接口,所以抽象接口比具体的实现类要稳定
  3. 抽象不应该依赖于实现细节,实现细节应该依赖抽象
  4. 高层模块不应该依赖于低层模块,二者都应该依赖抽象接口

说明:如图2-1,APP类中需要使用Message1 Message2 Message3三个类,APP类依赖于这三个类(因为APP知道这三个类的存在,而这三个类不知道APP的存在)。如图2-2所示,加了一个Message抽象接口,三个类都去实例化这个接口,APP依赖这个接口(APP知道这个接口的存在,而这个接口不知道APP的存在),三个类也依赖这个接口(Message接口知道三个类的存在,而三个类不知道Message接口的存在),APP只知道有这个接口的存在,而不知道这个接口下面三个类的存在。这样就通过Message抽象接口直接反转了APP对三个类的依赖关系,隔离了上层应用与底层技术实现细节,源代码的依赖方向永远是控制流方向的反转,这就是依赖反转的由来,大家可以看到APP指向Message接口的箭头正好和三个实现类指向Message接口的箭头相反
在这里插入图片描述
图 2-1

在这里插入图片描述
图2-2

设计实战-划分边界

说明:软件设计是一门划分边界的艺术,边界的作用是将软件分割成各种元素,以便约束边界两边的关系。有些边界在项目初期就已经划分好,有些边界则是在项目后期才划。项目初期划分好这些边界的目的是为了方便我们尽可能地将一些决策延后,并且确保这些决策未来不会对系统的核心业务逻辑产生干扰。很多技术实现细节的过早决定将导致不必要的耦合,团队将来很有可能会为这些不必要的耦合买单。

下面我们拿一个项目来模拟一遍设计开发的全流程,会用到上面讲到的五大原则和划分边界

项目初期

不应该过早地确定技术实现细节,业务逻辑与数据存储、GUI显示、通信模块没有任何的关系,所以业务逻辑模块和数据存储模块、GUI显示模块、通信模块之间是有一条边界的。我们首先开发的是业务逻辑模块的代码,这个模块是整个系统中最高级的模块,我们只需要确定好数据存储系统和GUI显示模块、通信模块的接口即可,业务逻辑模块去调用这些接口,而不去关心这些模块的实现细节。正确的实现方式:

在这里插入图片描述

反面教材

在刚开始写代码的时候,项目组直接确定了用MQTT作为通信方式,直接在业务逻辑代码中写入了MQTT的很多技术实现细节。当业务逻辑模块的代码写完三分之一的时候,项目组感觉MQTT太浪费了,也不太符合一些业务场景的要求,其实用UDP通信就够了,于是乎马上大动干戈加班好几天把原来MQTT的代码全部替换成了UDP的实现代码。当整个项目的代码完成了三分之二,随着开发人员对业务场景和产品的进一步深入了解,项目组又发现UDP也不太合适,其实用普通的串口速传模块通信就够了,但是现在业务逻辑模块的代码又和UDP的技术实现细节强耦合,于是乎项目组又大动干戈把UDP的代码替换成了串口速传的代码。来回折腾,代价和风险太大了

项目中期

当业务逻辑模块的代码开发完成后,项目组会对产品和业务场景有更深一步的认识,这个时候项目组可以根据自己对产品和业务场景的理解去确定数据存储系统、GUI、通信模块的技术实现细节了。可能项目初期你觉得应该用MySQL来存储数据,可能现在发现只需要引入小型的Fatfs文件系统就能搞定,没必要用数据库,不过没关系,你是面向接口编程的,只要让数据存储系统适配你之前定义好的接口就行了。最开始可能你觉得GUI要用很炫酷的网页展示,但现在看来只需要终端打印输出就足够了,还好没有过早的确定GUI实现的细节呀。注意:边界线是穿过继承关系的
在这里插入图片描述

项目后期

随着客户需求的演变,核心的业务逻辑模块还是比较稳定的,不需要做任何变化,但是项目中期确定的串口速传、Fatfs、终端打印已经不能满足客户的需求了,这时需要升级为TCP、MySQL、网页显示。不过没关系,因为我们在项目初期已经为整个软件架构换好了边界线,我们是面向接口编程的,各个模块之间互不干扰,直接用里氏替换原则就好了。这样也符合了开闭原则-只增加新代码,不去冒风险修改已经稳定运行的老代码。这张图也印证了依赖反转原则。各个软件模块的职责也单一的,比如数据存储模块不会影响GUI模块,这两个模块有各自的变更理由和变更速度,这又符合了单一原则。我们刚才讲过的五大设计原则,这里全部得到验证,大家可以好好研究一下这张图,体会五大设计原则和边界划分
在这里插入图片描述

从组件的角度去看上面的软件架构

箭头指向说明业务逻辑组件不知道其他组件的存在,但是其他组件知道业务逻辑组件的存在。因为其他三个组件所实现的接口是定义在业务逻辑组件中的,而业务逻辑组件中的接口肯定不知道谁实现了自己。所有组件之间都是单向依赖关系的,箭头都指向最稳定的组件。所有箭头都指向了业务逻辑组件,所以整个系统中业务逻辑组件是最稳定、最核心的组件,而其他低层次的组件依赖于高层次的组件

抽象接口是组件之间交互的契约,jar包相当于Java的组件,动态库相当于C++的组件,一个低层次的组件在系统中可以随意被替换,而其他组件则不需要被重新编译部署。但是如果高层次组件代码中所使用的的抽象接口发生了变化,那么依赖这个接口的低层次组件肯定也要被重新编译部署

计算机软件本质上就是如何将输入转化为输出的策略语句,离输入/输出越远的组件层次越高,越近层次越低

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值