> 文档中心 > 详解面向对象设计模式六/七大原则

详解面向对象设计模式六/七大原则


面向对象设计模式六/七大原则

前言

小孩儿没娘,说来话长。我们提到设计模式总会说23中设计模式,六大设计原则什么的。但是他们关系是什么呢?

个人的一点愚见是这样的,23种设计模式是根据实际生产经验提出来的可行性的模式方法,面对特定的问题,我们可以用特定的设计模式来解决这个问题,或者将各种设计模式组合起来解决更复杂一点的问题。而设计原则呢?则是在设计模式基础上提出来的更抽象的概念。各种各样的设计模式都会符合部分设计原则,同时如果不遵守设计原则很难在合适的地方合理的应用到合理的设计模式。

所以这篇文章的目的是直接讨论一下最抽象的设计原则,以达到张无忌忘记剑谱的目的。毕竟23中设计模式再加上各种变种,不是谁都能记得住。但是我们如果尽量按照设计原则来要求自己的代码,再以设计模式做校验。可能无形中我们已经会很灵活的的应用设计模式了。

设计模式的提出

在考虑设计模式之前我们首先要根据自己的业务模型和需求来考虑三个问题:

  1. **对象应该如何创建?**因为对象的创建会消耗掉系统的很多资源,复杂的业务也可能需要各种各样的对象。
  2. **对象之间的依赖和关系是什么样子的?**因为如何设计对象的结构、继承和依赖关系会影响到后续程序的可维护性、健壮性、耦合性等
  3. **对象之间的行为应该是什么?**如果对象的行为设计的好,那么对象的行为就会更清晰,它们之间的协作效率就会提高

带着这三个问题我们再看下文。

面向对象语言推出以后,由下图的四位大牛(又被称为 GoF gang of four)根据C++的经验于94年在《Design Patterns - Elements of Reusable Object-Oriented Software》一书中整合了23种设计模式。分别为

创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

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

设计原则的提出

96年左右另一批大牛也是博采众家之长,包括一些建筑行业的思想陆续总结出来6大设计原则,分别为

单一职责原则——SRP(Single Responsibility Principle)
开闭原则——OCP(Open Closed Principle)
里式替换原则——LSP (Liskov Substitution Principle)
接口隔离原则——ISP(Interface Segregation Principle)
依赖倒置原则——DIP(DependencyInversionPrinciple)
迪米特原则——LOD(Law Of Demeter)

部分文献资料如下
在这里插入图片描述
但是有一些文献资料中也经常提到另外一个设计原则就是

组合/聚合复用原则(Composition/Aggregation Principle)

那我们就将7个原则都讨论一下,汇总如下

开闭原则——OCP(Open Closed Principle)

  • 一个软件实体应当对扩展开放,对修改关闭

单一职责原则——SRP(Single Responsibility Principle)

  • 不要存在多于一个导致类变更的原因,也就是说每个类应该实现单一的职责,否则就应该把类拆分。

里式替换原则——LSP (Liskov Substitution Principle)

  • 一个实体如果使用的是一个基类的话,那么一定适用于其子类,而且它根本不能察觉出来基类对象和子类对象的区别。

接口隔离原则——ISP(Interface Segregation Principle)

  • 面向接口编程,依赖于抽象而不依赖于具体。

依赖倒置原则——DIP(DependencyInversionPrinciple)

  • 使用多个专门的接口比使用单一的总接口好

迪米特原则——LOD(Law Of Demeter)

  • 又叫最少知识原则,就是说一个对象应当对其他对象有尽可能少的了解。

组合/聚合复用原则(Composition/Aggregation Principle)

-尽量首先使用合成/聚合的方式,而不是使用继承。

实际生产中我们不可能会满足所有的原则,所以要做合适的取舍。但是要尽量拿原则来约束自己的代码。

以下是自己根据一些资料和实际生产中的经验对设计原则的一些粗浅的理解。
所有的理解都是围绕开闭原则这一条来展开讨论的,而这一条我也认为是最重要的一条。几乎所有的设计模式都围绕着这一条原则做文章

开闭原则

介绍

  • 在设计一个模块的时候,应当使这个模块可以在不修改的前提下被扩展。换言之,应当可以在不修改源码的情况下改变这个模块的行为。
  • In other words, (in an ideal world…) you should never need to change existing code or classes: All new functionality can be added by adding new subclasses or methods, or by reusing existing code through delegation.
  • This prevents you from introducing new bugs in existing code. If you never change it, you can’t break it. It also prevents you from fixing existing bugs in existing code, if taken to the extreme.

理解

  • 开闭原则是面向对象的可复用设计的基石
  • 优越性体现在通过扩展已有的软件系统,可以提供新的行为,以满足软件的新需求,使变化中的软件系统有一定的适应性和灵活性
  • 已有软件模块,特别是最重要的抽象层模块不再修改,这就使得变化中的软件有一定的稳定性和延续性
  • 抽象化是实现开闭原则的重要手段,而抽象化在java中的体现主要就是接口,抽象类及泛型约束等。
  • 通过对业务中可变性的封装可以使得流程是稳定和延续的,也就是对修改关闭的;同时使得能力是可以适应性和灵活性的,也就是对扩展的开放,而这种开放也不会对原有流程和能力造成破坏。

单一职责原则

介绍

  • 不要存在多于一个导致类变更的原因,也就是说每个类应该实现单一的职责,否则就应该把类拆分。
  • Each responsibility should be a separate class, because each responsibility is an axis of change.
    A class should have one, and only one, reason to change.

理解

  • 单一职责原则是开闭原则的前提
  • 默认大家都会,比如新开一个功能我们总会新增加一些XXXService,XXXBean之类
  • 但是因为某种原因,可能是需求的变更,可能是自己编码抽象能力的提高,职责P被分化为粒度更细的职责P1和P2。这时候就会有职责扩散的问题,逐渐的违背单一职责原则
  • 在职责扩散到我们无法控制的程度之前,立刻对代码进行重构。

里式替换原则

介绍

  • 一个实体如果使用的是一个基类的话,那么一定适用于其子类,而且它根本不能察觉出来基类对象和子类对象的区别。
  • 任何基类可以出现的地方,子类一定可以出现。LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为
  • 上面说的是可以出现和可以替换,而不是让大家在代码中用子类去替换基类。

理解

  • 里式替换原则是对实现开闭原则的关键步骤(抽象化)的规范
  • 主要是约束子类的行为,要求其行为和基类保持一致,如果替换为子类后,程序运行不正常,则说明子类没有按基类的预期实现业务,或者说子类不适合继承这个基类,对不起,请找适合的基类继承。

依赖倒置原则

介绍

  • 要面向接口编程,依赖于抽象而不依赖于具体。
  • 抽象不应当依赖于细节,细节应当依赖于抽象
  • 要针对接口编程,不要针对实现编程

理解

  • 依赖倒转原则是实现开闭原则的手段
  • 代码间的三种耦合关系分别为零耦合、具体耦合、抽象耦合
  • 要求调用方依赖于抽象耦合,也就是所谓的面向接口编程
  • 多考虑扩展性,如果这段代码以后扩展或变化的可能性很高,请用接口或抽象类封装下,抽象出不变的接口,将变化的部分留给不同的实现类。

迪米特原则

介绍

  • 又叫最少知识原则,就是说一个对象应当对其他对象有尽可能少的了解。
  • 另一种说法是只与你直接的朋友们通信,不要和陌生人说话
  • 每一个软件单位对其他单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位
  • “朋友圈”的定义
    • 当前对象本身
    • 以参量形式传入到当前对象方法中的对象
    • 当前对象的实例变量直接应用的对象
    • 当前对象的聚集变量中的元素
    • 当前对象所创建的对象

理解

  • 迪米特原则保证开闭原则能开闭多远
  • 如果我们将一个系统想象成一个亚马逊大森林,里面树根缠着树根,树枝交叉着树枝,一根藤蔓可能不知道上下左右前后缠着多少棵数。那这样的一个系统我们是不敢碰的。原地抽根烟可能几十里外的树都会遭殃。
  • 迪米特原则什么就相当于我们尽量做各种各样的隔离带。隔离带之间需要联系的可以用类似于门面模式,适配器模式等来做链接。那这样对我们来说就是可控的,在一个隔离区内你可以尽情的折腾。所以说我对迪米特原则的理解就是保证开闭原则能开闭多远
    在这里插入图片描述

接口隔离原则

介绍

  • 使用多个专门的接口比使用单一的总接口好
  • The dependency of one class to another one should depend on the smallest possible interface.

理解

  • 接口隔离原则保证开闭原则能开闭多广
  • 还拿上面的亚马逊大森林举例。我们在建立了隔离带的基础上,如果再分区来规划草皮,灌木,乔木,花圃,农田和河流。那是不是就更好管理了?花匠不需要管理河流是怎么治理的,只要负责四季花开就行。农夫也不需要管草皮上有没有国足在踢球。
  • 这样的话就很容易行程规模效应,所以说我对接口隔离原则的理解就是保证了开闭原则能开闭多广
    在这里插入图片描述

组合/聚合复用原则

介绍

  • 尽量首先使用合成/聚合的方式,而不是使用继承。
  • 在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新对象通过向这些对象的委派达到复用已有功能的目的

理解

  • 组合/聚合复用原则保证开闭原则能开闭多深
  • 通过组合和聚合的手段,达到复用的目的
  • 继承复用的缺点
  • 继承复用破坏包装,因为继承将超类的实现细节暴露给子类,由于超类的内部细节常常是对子类透明的,因此这种复用是透明的复用,又称“白箱”复用
  • 如果超类的实现发生改变,那么子类的实现也不得不发生改变。
  • 从超类继承而来的实现是静态的,不可能在运行时间内发生改变,因此没有足够的灵活性
  • 我们这里不用亚马逊大森林还举例了,我们拿雷峰塔举例,塔基就是基类,塔身和塔尖就是各个级别的子类。那塔基或塔身稍微有点动静,对塔尖来说可能就是地动山摇。如果塔身上再多来几个分支,就更不稳定了。
  • 而组合聚合复用原则就是拿上面的单一原则,接口隔离原则等原则分割出来的功能块来拼乐高。这样独立模块的复用性就提高了。
  • 还能减少bug的产生,因为我们主要在用已有的模块,有bug的话早发现了。

总结

如果记不住那么多的设计模式,至少要牢牢记住这六/七个设计原则,并拿他们来规范自己的代码。可能无形中已经应用了好几种设计模式。如果能时不时再拿出来设计模式看看,再对比一下自己的代码,再理解一下到底保证了哪些原则,一定会潜移默化的写出来漂亮的代码。