设计模式的7大原则(设计模式6大设计原则增强版)

设计模式(Design Pattern)是经过高度抽象化的在编程中可以被反复使用的代码设计经验的总结。

正确使用设计模式能有效提高代码的可读性、可重用性和可靠性,编写符合设计模式规范的代码不但有利于自身系统的稳定、可靠,还有利于外部系统的对接。在使用了良好的设计模式的系统工程中,无论是对满足当前的需求,还是对适应未来的需求,无论是对自身系统间模块的对接,还是对外部系统的对接,都有很大的帮助。

设计模式被GOF(Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides四人)首次提出是在1994年《设计模式 - 可复用的面向对象软件元素》(英文名: Design Patterns - Elements of Reusable Object-Oriented Software)一书中,最初的思想主要是对接口编程而不是对实现编程与优先使用对象组合而不是继承。

在程序设计领域起初有5大设计原则SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)是由罗伯特·C·马丁在21世纪早期[1] 引入的记忆术首字母缩略字,指代了面向对象编程和面向对象设计的五个基本原则。当这些原则被一起应用时,它们使得一个程序员开发一个容易进行软件维护和扩展的系统变得更加可能。SOLID所包含的原则是通过引发编程者进行软件源代码的代码重构进行软件的代码异味清扫,从而使得软件清晰可读以及可扩展时可以应用的指南。SOLID被典型的应用在测试驱动开发上,并且是敏捷开发以及自适应软件开发的基本原则的重要组成部分。

到现在最熟知的设计模式主要有23种,而这23种设计模式遵循了7大原则(有的书中只提到了6大原则,在此综合所有进行讲解)。

设计模式的7大原则介绍

设计模式的7大原则:单一职责原则(SRP)、里氏代换原则(LSP)、依赖倒置原则(DIP)、接口隔离原则、合成/聚合复用原则、迪米特法则、开闭原则。

1、单一职责原则

单一职责原则的英文名称是Single Responsibility Principle,简称是SRP

单一职责原则又称单一功能原则,它规定一个类只有一个职责。如果有多个职责(功能)被设计在一个类中,这个类就违反了单一职责原则。

SRP的原话:There should never be more than one reason for a class to change.(不能有更多的原因导致类的改变)。

在写单一职责原则的的这段博文的时候,我在回想自己之前写过的代码,发现很多情况是违背了单一职责原则。比如用户性别修改接口与年龄修改接口,按照单一职责来划分应该写成两个接口。在很多技术峰会或论坛中也会有很多类似的声音,单一职责这个原则有时候很难说。

单一职责从理论上来说是非常优秀的原则,但是实现还是有非常大的难度的,你必须考虑工期、成本、技术员的技术水平等等一些因素。

我个人认为单一职责在仅从业务层面来分析应该实现的难道还是很大,但是仅从技术或框架设计来说还是需要值得重视的。

对于单一职责原则,建议是接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。

2、里氏替换原则

在面向对象的语言中,继承是必不可少的、非常优秀的语言机制,它有如下优点:

  • 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
  • 提高代码的重用性;
  • 子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同;
  • 提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的扩展接口都是通过继承父类来完成的;
  • 提高产品或项目的开放性。

自然界的所有事物都是优点和缺点并存的,即使是鸡蛋,有时候也能挑出骨头来,继承的缺点如下:

  • 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;
  • 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;
  • 增强了耦合性。当父类的常量、变量和方法被修改时,必需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大片的代码需要重构。

Java使用extends关键字来实现继承,它采用了单一继承的规则,C++则采用了多重继承的规则,一个子类可以继承多个父类。从整体上来看,利大于弊,怎么才能让“利”的因素发挥最大的作用,同时减少“弊”带来的麻烦呢?解决方案是引入里氏替换原则(Liskov Substitution Principle,LSP),什么是里氏替换原则呢?它有两种定义:

  • 第一种定义,也是最正宗的定义:If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.(如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。)
  • 第二种定义:Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.(所有引用基类的地方必须能透明地使用其子类的对象。)

第二个定义是最清晰明确的,通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。

里氏替换原则为良好的继承定义了一个规范,一句简单的定义包含了4层含义。
(1).子类必须完全实现父类的方法

(2).子类可以有自己的个性

(3).覆盖或实现父类的方法时输入参数可以被放大

(4).覆写或实现父类的方法时输出结果可以被缩小

3、依赖倒置原则

依赖倒置原则(Dependence Inversion Principle,DIP)这个名字看着有点别扭,“依赖”还“倒置”,这到底是什么意思?依赖倒置原则的原始定义是:
High level modules should not depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.
翻译过来,包含三层含义:

  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象;
  • 抽象不应该依赖细节;
  • 细节应该依赖抽象。

高层模块和低层模块容易理解,每一个逻辑的实现都是由原子逻辑组成的,不可分割的原子逻辑就是低层模块,原子逻辑的再组装就是高层模块。那什么是抽象?什么又是细节呢?在Java语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是可以直接被实例化,也就是可以加上一个关键字new产生一个对象。依赖倒置原则在Java语言中的表现就是:

  • 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;
  • 接口或抽象类不依赖于实现类;
  • 实现类依赖接口或抽象类。

更加精简的定义就是“面向接口编程”——OOD(Object-Oriented Design,面向对象设计)的精髓之一。

依赖的三种写法
依赖是可以传递的,A对象依赖B对象,B又依赖C,C又依赖D……生生不息,依赖不止,记住一点:只要做到抽象依赖,即使是多层的依赖传递也无所畏惧!
对象的依赖关系有三种方式来传递,如下所示。

(1)构造函数传递依赖对象

(2)Setter方法传递依赖对象

(3)接口声明依赖对象

 

4、接口隔离原则

接口隔离原则(英语:interface-segregation principles, 缩写:ISP),以下有两种定义:

  • Clients should not be forced to depend upon interfaces that they don't use.(客户端不应该依赖它不需要的接口。)
  • The dependency of one class to another one should depend on the smallest possible interface.(类间的依赖关系应该建立在最小的接口上。)

我们可以把这两个定义概括为一句话:建立单一接口,不要建立臃肿庞大的接口。再通俗一点讲:接口尽量细化,同时接口中的方法尽量少。看到这里大家有可能要疑惑了,这与单一职责原则不是相同的吗?错,接口隔离原则与单一职责的审视角度是不相同的,单一职责要求的是类和接口职责单一,注重的是职责,这是业务逻辑上的划分,而接口隔离原则要求接口的方法尽量少。例如一个接口的职责可能包含10个方法,这10个方法都放在一个接口中,并且提供给多个模块访问,各个模块按照规定的权限来访问,在系统外通过文档约束“不使用的方法不要访问”,按照单一职责原则是允许的,按照接口隔离原则是不允许的,因为它要求“尽量使用多个专门的接口”。专门的接口指什么?就是指提供给每个模块的都应该是单一接口,提供给几个模块就应该有几个接口,而不是建立一个庞大的臃肿的接口,容纳所有的客户端访问。

采用接口隔离原则对接口进行约束时,要注意以下几点:

  • 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
  • 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
  • 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。

 

5、合成/聚合复用原则

合成/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)也称合成复用原则(Composite Reuse Principle,CRP)指通过在一个新的对象中引入(注入)已有的对象以达到类的功能复用和扩展的目的。它的设计原则是要尽量使用合成或聚合而不要使用继承来扩展类的功能。

为什么“要尽量使用合成和聚合,尽量不要使用继承”呢?

这是因为:

  •      第一,继承复用破坏包装,它把父类的实现细节直接暴露给了子类,这违背了信息隐藏的原则;
  •      第二:如果父类发生了改变,那么子类也要发生相应的改变,这就直接导致了类与类之间的高耦合,不利于类的扩展、复用、维护等,也带来了系统僵硬和脆弱的设计。而用合成和聚合的时候新对象和已有对象的交互往往是通过接口或者抽象类进行的,就可以很好的避免上面的不足,而且这也可以让每一个新的类专注于实现自己的任务,符合单一职责原则。
     

6、迪米特法则

迪米特法则(Law of Demeter,LoD)也称为最少知识原则(Least Knowledge Principle,LKP),虽然名字不同,但描述的是同一个规则:一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的这么多public方法,我就调用这么多,其他的我一概不关心。

 

7、开闭原则

开闭原则,英文缩写OCP,全称Open Closed Principle。

原始定义:Software entities (classes, modules, functions) should be open for extension but closed for modification。

字面翻译:软件实体(包括类、模块、功能等)应该对扩展开放,但是对修改关闭。

开闭原则规定软件中的对象(类、模块、函数等)对扩展开放,对修改封闭,这意味着一个实体允许在不改变其源代码的前提下改变其行为,该特性在产品化的环境下是特别有价值的,在这种环境下,改变源代码需要经过代码审查、单元测试等过程,以确保产品的使用质量。遵循这个原则的代码在扩展时并不发生改变,因此不需要经历上述过程。

 

笔者的微信公众号,每天一篇好文章:

posted @ 2020-03-10 23:27  两粒  阅读(469)  评论(0编辑  收藏  举报