软件设计七大原则

软件设计原则是设计模式的基石。目的只有一个,降低对象之间的耦合,增加程序的可复用性、可扩展性、可维护性。

一.开闭原则 OCP

定义:软件实体对扩展开放,对修改关闭。
对扩展开发,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
对修改关闭,意味着类一旦设计完成,就可以独立的工作,而不要对其进行任何的修改。
在面向对象设计中,我们通常通过继承和多态来实现OCP,即封装不变部分。
比如需求要实现2种状态的业务。
☆如果用if else来判断,那么后面加第三种状态,就还需要在此接口上增加else逻辑,不符合开闭原则。
☆用策略类实现,则定义策略接口,策略A和策略B为具体实现类,分别对应两种状态。假如下一次需求要实现第三种状态,那么直接定义一个策略C实现类就可满足。原有代码不变,符合开闭原则。
关于策略模式详见:C++设计模式——策略模式  

二.里氏替换原则 LSP

定义:程序中的父类型都可以正确的被子类型替换。
程序中的对象可以在不改变程序正确性的前提下被它的子类所替换,即子类可以替换任何基类能够出现的地方,并且经过替换后,代码还能正确工作。
里氏替换原则通俗的讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说,子类继承父类的时候,除了添加新的方法扩展功能之外,尽量不要重写父类的方法。
根据上述理解,对里氏替换原则的实现可以总结如下:
☆子类可以实现父类的抽象方法,但是不能覆盖父类非抽象的方法。
☆子类中可以增加自己特有的方法。
☆当子类重写父类的方法时,方法的入参应该比父类的方法更加宽松。
☆当子类的方法实现父类的方法时(重写/重载/或实现抽象方法),方法的返回值应该比父类更加严格或者相等。
假设定义一个抽象禽类,有一个飞翔方法fly(), 我们就可以自由的继承禽类衍生出各种鸟儿,并调用其飞翔方法。如果鸵鸟加入禽类行列,继承禽类,但不会飞,那么飞翔方法fly()就显得多余。而且在所有禽类出现的地方,无法用鸵鸟替换(此时不满足正确业务逻辑)。违反了里氏替换原则。

经过反思,是设计问题,禽类和飞翔无必然联系,所以禽类不应该定义飞翔方法fly(),把禽类飞翔方法fly()抽离出去单独定义飞翔接口Flyable。
对于有飞翔能力的鸟儿继承禽类并实现飞翔接口。鸵鸟继承禽类,但不实现飞翔接口,是否是鸟儿取决于是否继承自禽类,能不能飞取决于是否实现飞翔接口。所有禽类出现的地方都可以用子类进行替换,所有飞翔接口出现的地方都可以被其替换为实现。

三.依赖倒置原则 DIP

定义:模块之间交互应该依赖抽象,而非实现。
DIP要求高层模块不应该依赖于底层模块,二者都应该依赖于抽象。抽象不应该依赖细节,细节应该依赖抽象。
比如某个人喂养小动物,如果依赖了具体的实现,则每新增一个动物,需要在Person内加一个对应的方法。违背了开闭原则,也不符合依赖倒置原则。

重新修改后,如下。新增一个Birds抽象类,具体的动物继承自父类Birds,Person中的方法参数依赖于抽象,而不是具体的实现。符合依赖倒置原则。

注:上图中的fly()应该是feed()

桥接模式是依赖倒置原则的极好范例。

关于桥接模式详见:C++设计模式——桥接模式

四.单一职责原则 SRP

定义:对任何类的修改只能有一个原因。换句话说,一个类只应该负责一项职责。
SRP要求每个软件模块职责要单一,衡量标准是模块是否只有一个被修改的原因。职责越单一,被修改的原因就越少,模块的内聚性就越高,被复用的可能性就越大,也更容易被理解。
举例员工类 Employee,开发工作变了,需要修改Employee类,测试工作变了,也需要修改Employee类,不符合单一职责原则,类的复杂性也高。
☆职责多,引起此类变化的原因也多。后续变更的风险就大。
☆后续需求变更,会造成职责的混乱,类结构的不稳定。

改造后,类的职责单一。开发者的职责就是“写代码”,那么对其进行的修改只有与“写代码”相关的一个原因(画类图也是为了指导代码落地),这样才能确保类职责的单一性原则。
同时,类与类之间虽有着明确的职责划分,但又一起合作完成任务,它们保持着一种“对立且统一”的辩证关系。
☆以职责链模式为例,每个处理者类职责清晰,只处理与自己职责相关的业务。
☆以员工类为例,拆分后,各个员工完成相应的职责,共同保障项目上线。
这种清晰的职责范围划分就是单一职责原则的最佳实践。符合单一职责原则的设计能使类具备高内聚性,让单个模块变得简单易懂,如此才能增强代码的可读性和可复用性。并提高系统的易维护性和易测试性。

上面的例子是类职责单一,那么微服务划分也同理,采用单一职责原则,每个服务负责一块业务。同一类业务的变更落在单个服务内变更。

五.接口隔离原则 ISP

定义:客户端对类的依赖基于最小接口,而不依赖不需要的接口。
接口隔离原则认为不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口要好。做接口拆分时,也要尽量满足单一职责原则。将外部依赖减到最少,降低模块间的耦合。
比如类A只需要使用方法1、方法2,类B只需要使用方法3、方法4,但在源代码层次上与所有方法形成依赖关系(这四个方法在同一个接口文件中)。这种依赖意味着我们对方法3修改,即使不会影响A所依赖的方法1、方法2的功能,也会导致它需要重新部署和编译。
此时,我们可以把方法1和方法2放到一个接口文件,把方法3和方法4放到另一个接口文件,这样的话,类A既然用不到方法3和方法4,只需要依赖接口1即可。
现有一个接口CustomerDataDisplay,包含了createChart()、displayChart()、createReport()、displayReport()方法,方法说明如下:
☆createChart():创建图表
☆displayChart():显示图表
☆createReport():创建报表
☆displayReport():显示报表
现有一个图表类ChartClass实现了CustomerDataDisplay接口,该类就要实现接口的全部方法,正常来说ChartClass只需要createChart()、displayChart()方法即可,因为CustomerDataDisplay接口方法太多了,承担了太多职责,违背了接口隔离原则。
此时我们可以根据业务职责将CustomerDataDisplay接口分为ChartHandler、ReportHandler这两个接口,其中ChartHandler接口包含createChart()和displayChart()方法;ReportHandler接口包括createReport()、displayReport()方法。
在使用接口隔离原则时我们要控制接口的颗粒度,颗粒度不能太大,也不能太小。如果太小就会造成接口泛滥,不利于维护;如果太大就会违背接口隔离原则,灵活性较差,使用起来不方便。一般来说接口中仅包含某业务模块的方法即可,不应该有其他业务模块的方法。

六.迪米特法则 LOD

定义:一个类对于其它类知道的越少越好。
迪米特法则也被称为最少知识原则,它提出一个模块对其他模块应该知之甚少,或者说模块之间应该彼此保持陌生,甚至意识不到对方的存在,以此最小化、简单化模块间的通信,并达到松耦合的目的。
反之,模块之间若存在过多的关联,那么一个很小的变动则可能会引发蝴蝶效应般的连锁反应,最终会波及大范围的系统变动。我们说,缺乏良好封装性的系统模块是违反迪米特法则的,牵一发动全身的设计使系统的扩展与维护变的举步维艰。
外观模式和中介者模式是迪米特法则极好的范例。 
关于外观模式详见:C++设计模式——外观模式
关于中介者模式详见:C++设计模式——中介者模式
迪米特法则还有几种定义形式,包括:不要和"陌生人"说话、只与你的直接朋友通信等,在迪米特法则中,对于一个对象,其朋友包括以下几类:
☆当前对象本身(this)
☆以参数形式传入到当前对象方法中的对象
☆当前对象的成员对象
☆如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友。
☆当前对象创建的对象
任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”。在应用迪米特法则时,一个对象只能与直接朋友发生交互,不能与“陌生人”发生直接交互,这样子可以降低系统的耦合度,一个对象的改变不会给太多其他对象带来影响。
迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象不必直接通信,那么这两个对象就不应该发生任何直接的相互作用,如果其中一个对象需要调用另外一个对象的某个方法时,可以通过第三者触发这个调用。就是通过引入一个合理的第三者来降低先有对象之间的耦合度。
明星与经纪人的例子,像我们普通的明星工作繁忙,他的日常工作、日程安排都是由那个经纪人带做,明星是不可能自己亲力亲为的,不然的话会很累。
假设明星是一个类,行程是一个类,那么明星不会直接安排自己的行程,他会找来经纪人这个类,通过经纪人去安排行程。

七.合成复用原则 CRP

定义:优先使用合成/聚合,而不是类继承。
比如对象的继承关系是在编译时就定义好了,所以无法在运行时改变从父类继承的实现。子类的实现与它的父类有非常紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。当你需要复用子类时,如果继承下来的实现不适合解决新的问题,则父类必须重写或被其它更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。
合成(组合)和聚合都是关联的特殊种类。
聚合表示一种弱拥有关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分;
合成则是一种强拥有关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样。
合成复用原则好处:优先使用对象的合成/聚合将有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模。
举例:手机软件划分可分为QQ、微信等,按品牌划分可分为华为、小米等。如果同时考虑这两种分类,其组合就很多。往下继续扩展软件、手机品牌,都会新增许多子类。

用聚合关系实现的类图:后面新增软件,手机品牌类不用变更代码。继承的层次也少了

本文主要来自:深度总结:软件设计七大原则 ,有增删

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

草上爬

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

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

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

打赏作者

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

抵扣说明:

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

余额充值