设计模式与开发原则概述

8/31/2021 设计模式开发原则

# 1. 设计模式概述

# 1.1 什么是设计模式

设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 设计模式于己于他人于系统都是多赢的,设计模式使代码编写真正工程化,是软件工程的基石,如同大厦的一块块砖石一样。项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。

简而言之:

  • 模式:在某些场景下,针对某类问题的某种通用的解决方案。
  • 场景:项目所在的环境
  • 问题:约束条件,项目目标等
  • 解决方案:通用、可复用的设计,解决约束达到目标。

# 1.2 设计模式分类

创建型模式:对象实例化的模式,创建型模式用于解耦对象的实例化过程。

结构型模式:把类或对象结合在一起形成一个更大的结构。

行为型模式:类和对象如何交互,及划分责任和算法。

设计模式分类

# 1.3 各设计模式的关键点

创建型模式

  • 单例模式(Singleton):某个类只能有一个实例,提供一个全局的访问点。
  • 工厂方法模式(Factory Method):定义一个创建对象的接口,让子类决定实例化那个类。
  • 抽象工厂模式(Abstract Factory):创建相关或依赖对象的家族,而无需明确指定具体类。
  • 建造者模式(BuilderPattern):封装一个复杂对象的构建过程,并可以按步骤构造。
  • 原型模式(Prototype):通过复制现有的实例来创建新的实例。

结构型模式

  • 适配器模式(Adapter):将一个类的方法接口转换成客户希望的另外一个接口。
  • 桥接模式(Bridge):将抽象部分和它的实现部分分离,使它们都可以独立的变化。
  • 组合模式(Composite):将对象组合成树形结构以表示“部分-整体”的层次结构。
  • 装饰模式(Decorator):动态的给对象添加新的功能。
  • 外观模式(Facade):对外提供一个统一的方法,来访问子系统中的一组接口。
  • 亨元模式(Flyweight):通过共享技术来有效的支持大量细粒度的对象。
  • 代理模式(Proxy):为其他对象提供一个代理以便控制这个对象的访问。

行为型模式

  • 访问者模式(Visitor):在不改变数据结构的前提下,增加作用于一组对象元素的新功能。
  • 模板模式(Template):定义一个算法结构,而将一些步骤延迟到子类实现。
  • 策略模式(Strategy):定义一系列算法,把他们封装起来,并且使它们可以相互替换。
  • 状态模式(State):允许一个对象在其对象内部状态改变时改变它的行为。
  • 观察者模式(Observer):对象间的一对多的依赖关系。
  • 备忘录模式(Memento):在不破坏封装的前提下,保持对象的内部状态。
  • 中介者模式(Mediator):用一个中介对象来封装一系列的对象交互。
  • 迭代器模式(Iterator):一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。
  • 解释器模式(Interpreter):给定一个语言,定义它的文法的一种表示,并定义一个解释器。
  • 命令模式(Command):将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。
  • 责任链模式(Chain Of Responsibilities):将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会。

# 2. 设计模式详细介绍

# 2.1 创建型设计模式

通俗解释:创建型设计模式专注于如何实例化对象或相关对象组。

维基百科:在软件工程中,创建设计模式是处理对象创建机制的设计模式,试图以适合于该情况的方式创建对象。对象创建的基本形式可能导致设计问题或增加设计的复杂性。创建设计模式通过某种方式控制此对象创建来解决此问题。

# 2.1.1 单例模式

单例模式,它的定义就是确保某一个类只有一个实例,并且提供一个全局访问点。

单例模式具备典型的3个特点:1)只有一个实例。 2)自我实例化。 3)提供全局访问点。

因此当系统中只需要一个实例对象或者系统中只允许一个公共访问点,除了这个公共访问点外,不能通过其他访问点访问该实例时,可以使用单例模式。

单例模式的主要优点就是节约系统资源、提高了系统效率,同时也能够严格控制客户对它的访问。也许就是因为系统中只有一个实例,这样就导致了单例类的职责过重,违背了“单一职责原则”,同时也没有抽象类,所以扩展起来有一定的困难。其UML结构图如下:

单例模式

# 2.1.2 工厂方法模式

作为抽象工厂模式的孪生兄弟,工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。

工厂方法模式非常符合“开闭原则”,当需要增加一个新的产品时,我们只需要增加一个具体的产品类和与之对应的具体工厂即可,无须修改原有系统。同时在工厂方法模式中用户只需要知道生产产品的具体工厂即可,无须关系产品的创建过程,甚至连具体的产品类名称都不需要知道。虽然他很好的符合了“开闭原则”,但是由于每新增一个新产品时就需要增加两个类,这样势必会导致系统的复杂度增加。其UML结构图如下:

工厂方法模式

# 2.1.3 抽象工厂模式

所谓抽象工厂模式就是提供一个接口,用于创建相关或者依赖对象的家族,而不需要明确指定具体类。他允许客户端使用抽象的接口来创建一组相关的产品,而不需要关系实际产出的具体产品是什么。这样一来,客户就可以从具体的产品中被解耦。它的优点是隔离了具体类的生成,使得客户端不需要知道什么被创建了,而缺点就在于新增新的行为会比较麻烦,因为当添加一个新的产品对象时,需要更加需要更改接口及其下所有子类。其UML结构图如下:

抽象工厂模式

# 2.1.4 建造者模式

对于建造者模式而言,它主要是将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示。适用于那些产品对象的内部结构比较复杂的情况。

建造者模式将复杂产品的构建过程封装分解在不同的方法中,使得创建过程非常清晰,能够让我们更加精确的控制复杂产品对象的创建过程,同时它隔离了复杂产品对象的创建和使用,使得相同的创建过程能够创建不同的产品。但是如果某个产品的内部结构过于复杂,将会导致整个系统变得非常庞大,不利于控制,同时若几个产品之间存在较大的差异,则不适用建造者模式,毕竟这个世界上存在相同点大的两个产品并不是很多,所以它的使用范围有限。其UML结构图如下:

建造者模式

# 2.1.5 原型模式

我们应用程序可能有某些对象的结构比较复杂,但我们又需要频繁的使用它们,如果这个时候我们来不断的新建这个对象势必会大大损耗系统内存,这个时候我们需要使用原型模式来对这个结构复杂又要频繁使用的对象进行克隆。所以原型模式就是用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。

它主要应用于那些创建新对象成本过大的情况。主要优点是简化了新对象的创建过程,提高了效率,同时原型模式提供了简化的创建结构。其UML结构图如下:

原型模式

原型模式包含如下角色:

  • Prototype:抽象原型类
  • ConcretePrototype:具体原型类
  • Client:客户类

# 2.2 结构型设计模式

通俗解释:结构型设计模式大多关注对象组成,或者换句话说,实体如何相互使用。或者另一种解释是,它们有助于回答“如何构建软件组件”。

维基百科:在软件工程中,结构设计模式是通过识别实现实体之间关系的简单方法来简化设计的设计模式。

# 2.2.1 适配器模式

在应用程序中我们可能需要将两个不同接口的类进行通信,在不修改这两个类的前提下我们可能会需要某个中间件来完成这个衔接的过程,这个中间件就是适配器。所谓适配器模式就是将一个类的接口,转换成客户期望的另一个接口,它可以让原本两个不兼容的接口能够无缝完成对接。

作为中间件的适配器将目标类和适配者解耦,增加了类的透明性和可复用性。其UML结构图如下:

适配器模式

适配器模式包含如下角色:

  • Target:目标抽象类
  • Adapter:适配器类
  • Adaptee:适配者类
  • Client:客户类

# 2.2.2 桥接模式

如果说某个系统能够从多个角度来进行分类,且每一种分类都可能会变化,那么我们需要做的就是将这多个角度分离出来,使得他们能独立变化,减少他们之间的耦合,这个分离过程就使用了桥接模式。所谓桥接模式就是将抽象部分和实现部分隔离开,使得他们能够独立变化。

桥接模式将继承关系转化成关联关系,封装了变化,完成了解耦,减少了系统中类的数量,也减少了代码量。其UML结构图如下:

桥接模式

桥接模式包含如下角色:

  • Abstraction:抽象类
  • RefinedAbstraction:扩充抽象类
  • Implementor:实现类接口
  • ConcreteImplementor:具体实现类

# 2.2.3 组合模式

组合模式组合多个对象形成树形结构以表示“整体-部分”的结构层次。它定义了如何将容器对象和叶子对象进行递归组合,使得客户在使用的过程中无须进行区分,可以对他们进行一致的处理。在使用组合模式中需要注意一点也是组合模式最关键的地方:叶子对象和组合对象实现相同的接口,这就是组合模式能够将叶子节点和对象节点进行一致处理的原因。

虽然组合模式能够清晰地定义分层次的复杂对象,也使得增加新构件也更容易,但是这样就导致了系统的设计变得更加抽象,如果系统的业务规则比较复杂的话,使用组合模式就有一定的挑战了。其UML结构图如下:

组合模式

组合模式包含如下角色:

  • Component: 抽象构件
  • Leaf: 叶子构件
  • Composite: 容器构件
  • Client: 客户类

# 2.2.4 装饰模式

我们可以通过继承和组合的方式来给一个对象添加行为,虽然使用继承能够很好拥有父类的行为,但是它存在几个缺陷:1)对象之间的关系复杂的话,系统变得复杂不利于维护。2)容易产生“类爆炸”现象。3)是静态的。在这里我们可以通过使用装饰者模式来解决这个问题。

装饰者模式,动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更加有弹性的替代方案。虽然装饰者模式能够动态将责任附加到对象上,但是他会产生许多的细小对象,增加了系统的复杂度。其UML结构图如下:

装饰模式

装饰模式包含如下角色:

  • Component: 抽象构件
  • ConcreteComponent: 具体构件
  • Decorator: 抽象装饰类
  • ConcreteDecorator: 具体装饰类

# 2.2.5 外观模式

类与类之间的耦合越低,那么可复用性就越好,如果两个类不必彼此通信,那么就不要让这两个类发生直接的相互关系,如果需要调用里面的方法,可以通过第三者来转发调用。外观模式提供了一个统一的接口,用来访问子系统中的一群接口。它让一个应用程序中子系统间的相互依赖关系减少到了最少,它给子系统提供了一个简单、单一的屏障,客户通过这个屏障来与子系统进行通信。通过使用外观模式,实现了客户与子系统之间的松耦合。但是它违背了“开闭原则”,因为增加新的子系统可能需要修改外观类或客户端的源代码。其UML结构图如下:

外观模式

外观模式包含如下角色:

  • Facade: 外观角色
  • SubSystem:子系统角色

# 2.2.6 亨元模式

在一个系统中对象会使得内存占用过多,特别是那些大量重复的对象,这就是对系统资源的极大浪费。享元模式对对象的重用提供了一种解决方案,它使用共享技术对相同或者相似对象实现重用。享元模式就是运行共享技术有效地支持大量细粒度对象的复用。系统使用少量对象,而且这些都比较相似,状态变化小,可以实现对象的多次复用。

这里有一点要注意:享元模式要求能够共享的对象必须是细粒度对象。享元模式通过共享技术使得系统中的对象个数大大减少了,同时享元模式使用了内部状态和外部状态,同时外部状态相对独立,不会影响到内部状态,所以享元模式能够使得享元对象在不同的环境下被共享。同时正是分为了内部状态和外部状态,享元模式会使得系统变得更加复杂,同时也会导致读取外部状态所消耗的时间过长。其UML结构图如下:

亨元模式

享元模式包含如下角色:

  • Flyweight: 抽象享元类
  • ConcreteFlyweight: 具体享元类
  • UnsharedConcreteFlyweight: 非共享具体享元类
  • FlyweightFactory: 享元工厂类

# 2.2.7 代理模式

代理模式就是给一个对象提供一个代理,并由代理对象控制对原对象的引用。它使得客户不能直接与真正的目标对象通信。代理对象是目标对象的代表,其他需要与这个目标对象打交道的操作都是和这个代理对象在交涉。

代理对象可以在客户端和目标对象之间起到中介的作用,这样起到了保护了目标对象的作用,同时也在一定程度上面减少了系统的耦合度。其UML结构图如下:

代理模式

代理模式包含如下角色:

  • Subject: 抽象主题角色
  • Proxy: 代理主题角色
  • RealSubject: 真实主题角色

# 2.3 行为型设计模式

通俗解释:行为型设计模式关注对象之间的职责分配。它们与结构型设计模式的不同之处在于,它们不仅指定了结构,还概述了它们之间的消息传递/通信模式。或者换句话说,他们协助回答“如何在软件组件中运行行为”。

维基百科:在软件工程中,行为设计模式是识别对象之间的共同通信模式并实现这些模式的设计模式。通过这样做,这些模式增加了执行该通信的灵活性。

# 2.3.1 访问者模式

在软件开发中我们可能会对同一个对象有不同的处理,如果我们都做分别的处理,将会产生灾难性的错误。对于这种问题,访问者模式提供了比较好的解决方案。访问者模式即表示一个作用于某对象结构中的各元素的操作,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

访问者模式的目的是封装一些施加于某种数据结构元素之上的操作,一旦这些操作需要修改的话,接受这个操作的数据结构可以保持不变。为不同类型的元素提供多种访问操作方式,且可以在不修改原有系统的情况下增加新的操作方式。同时我们还需要明确一点那就是访问者模式是适用于那些数据结构比较稳定的,因为它是将数据的操作与数据结构进行分离了,如果某个系统的数据结构相对稳定,但是操作算法易于变化的话,就比较适用适用访问者模式,因为访问者模式使得算法操作的增加变得比较简单了。其UML结构图如下:

访问者模式

访问者模式包含如下角色:

  • Vistor: 抽象访问者
  • ConcreteVisitor: 具体访问者
  • Element: 抽象元素
  • ConcreteElement: 具体元素
  • ObjectStructure: 对象结构

# 2.3.2 模板模式

有些时候我们做某几件事情的步骤都差不多,仅有那么一小点的不同,如果我们都将这些步骤都逐一去做的话,费时费力不讨好。所以我们可以将这些步骤分解、封装起来,然后利用继承的方式来继承即可,不同的可以自己重写实现,这就是模板方法模式提供的解决方案。

所谓模板方法模式就是在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

在模板方法模式中,我们可以将相同部分的代码放在父类中,而将不同的代码放入不同的子类中。也就是说我们需要声明一个抽象的父类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法让子类来实现剩余的逻辑,不同的子类可以以不同的方式来实现这些逻辑。所以模板模式的模板其实就是一个普通的方法,只不过这个方法是将算法实现的步骤封装起来的。其UML结构图如下:

模板模式

模板方法模式包含如下角色:

  • AbstractClass: 抽象类
  • ConcreteClass: 具体子类

# 2.3.3 策略模式

我们有很多中方法来实现一个功能,但是我们需要一种简单、高效的方式来实现它,使得系统能够非常灵活,这就是策略模式。所谓策略模式就是定义了算法族,分别封装起来,让他们之前可以互相转换。

在策略模式中,它将这些解决问题的方法定义成一个算法群,每一个方法都对应着一个具体的算法,这里的一个算法我就称之为一个策略。虽然策略模式定义了算法,但是它并不提供算法的选择,即什么算法对于什么问题最合适这是策略模式所不关心的,所以对于策略的选择还是要客户端来做。客户必须要清楚的知道每个算法之间的区别和在什么时候什么地方使用什么策略是最合适的,这样就增加客户端的负担。

同时策略模式也非常完美的符合了“开闭原则”,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。但是一个策略对应一个类将会是系统产生很多的策略类。其UML结构图如下:

策略模式

策略模式包含如下角色:

  • Context: 环境类
  • Strategy: 抽象策略类
  • ConcreteStrategy: 具体策略类

# 2.3.4 状态模式

在很多情况下我们对象的行为依赖于它的一个或者多个变化的属性,这些可变的属性我们称之为状态,也就是说行为依赖状态,即当该对象因为在外部的互动而导致它的状态发生变化,从而它的行为也会做出相应的变化。对于这种情况,我们是不能用行为来控制状态的变化,而应该站在状态的角度来思考行为,即是什么状态就要做出什么样的行为,这个就是状态模式。

所谓状态模式就是允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。在状态模式中我们可以减少大块的if…else语句,它是允许态转换逻辑与状态对象合成一体,但是减少if…else语句的代价就是会换来大量的类,所以状态模式势必会增加系统中类或者对象的个数。

同时状态模式是将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。但是这样就会导致系统的结构和实现都会比较复杂,如果使用不当就会导致程序的结构和代码混乱,不利于维护。其UML结构图如下:

状态模式

状态模式包含如下角色:

  • Context: 环境类
  • State: 抽象状态类
  • ConcreteState: 具体状态类

# 2.3.5 观察者模式

观察者模式定义了对象之间的一对多依赖关系,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并且自动更新。

在这里,发生改变的对象称之为观察目标,而被通知的对象称之为观察者。一个观察目标可以对应多个观察者,而且这些观察者之间没有相互联系,所以么可以根据需要增加和删除观察者,使得系统更易于扩展。所以观察者提供了一种对象设计,让主题和观察者之间以松耦合的方式结合。其UML结构图如下:

观察者模式

观察者模式包含如下角色:

  • Subject: 目标
  • ConcreteSubject: 具体目标
  • Observer: 观察者
  • ConcreteObserver: 具体观察者

# 2.3.6 备忘录模式

备忘录模式给我们的软件提供后悔药的机制,通过它可以使系统恢复到某一特定的历史状态。

所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。它实现了对信息的封装,使得客户不需要关心状态保存的细节。保存就要消耗资源,所以备忘录模式的缺点就在于消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。其UML结构图如下:

备忘录模式

备忘录模式包含如下角色:

  • Originator: 原发器
  • Memento: 备忘录
  • Caretaker: 负责人

# 2.3.7 中介者模式

在我们的系统中有时候会存在着对象与对象之间存在着很强、复杂的关联关系,如果让他们之间有直接的联系的话,必定会导致整个系统变得非常复杂,而且可扩展性很差。在前面我们就知道如果两个类之间没有不必彼此通信,我们就不应该让他们有直接的关联关系,如果实在是需要通信的话,我们可以通过第三者来转发他们的请求。同样,这里我们利用中介者来解决这个问题。

所谓中介者模式就是用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。在中介者模式中,中介对象用来封装对象之间的关系,各个对象可以不需要知道具体的信息通过中介者对象就可以实现相互通信。它减少了对象之间的互相关系,提供了系统可复用性,简化了系统的结构。

在中介者模式中,各个对象不需要互相知道了解,他们只需要知道中介者对象即可,但是中介者对象就必须要知道所有的对象和他们之间的关联关系,正是因为这样就导致了中介者对象的结构过于复杂,承担了过多的职责,同时它也是整个系统的核心所在,它有问题将会导致整个系统的问题。所以如果在系统的设计过程中如果出现“多对多”的复杂关系群时,千万别急着使用中介者模式,而是要仔细思考是不是您设计的系统存在问题。其UML结构图如下:

中介者模式

中介者模式包含如下角色:

  • Mediator: 抽象中介者
  • ConcreteMediator: 具体中介者
  • Colleague: 抽象同事类
  • ConcreteColleague: 具体同事类

# 2.3.8 迭代器模式

能够游走于集合内的每一个元素,同时还可以提供多种不同的遍历方式,这就是迭代器模式的设计动机。在实际的开发过程中,我们可能会需要根据不同的需求以不同的方式来遍历整个对象,但是我们又不希望在聚合对象的抽象接口中充斥着各种不同的遍历操作,于是就希望有某个东西能够以多种不同的方式来遍历一个聚合对象,这时迭代器模式出现了。

所谓迭代器模式就是提供一种方法顺序访问一个聚合对象中的各个元素,而不是暴露其内部的表示。迭代器模式是将迭代元素的责任交给迭代器,而不是聚合对象,我们甚至在不需要知道该聚合对象的内部结构就可以实现该聚合对象的迭代。通过迭代器模式,使得聚合对象的结构更加简单,它不需要关注它元素的遍历,只需要专注它应该专注的事情,这样就更加符合单一职责原则了。其UML结构图如下:

迭代器模式

迭代器模式包含如下角色:

  • Iterator: 抽象迭代器
  • ConcreteIterator: 具体迭代器
  • Aggregate: 抽象聚合类
  • ConcreteAggregate: 具体聚合类

# 2.3.9 解释器模式

所谓解释器模式就是定义语言的文法,并且建立一个解释器来解释该语言中的句子。解释器模式描述了如何构成一个简单的语言解释器,主要应用在使用面向对象语言开发的编译器中。它描述了如何为简单的语言定义一个文法,如何在该语言中表示一个句子,以及如何解释这些句子。其UML结构图如下:

解释器模式

解释器模式包含如下角色:

  • AbstractExpression: 抽象表达式
  • TerminalExpression: 终结符表达式
  • NonterminalExpression: 非终结符表达式
  • Context: 环境类
  • Client: 客户类

# 2.3.10 命令模式

有些时候我们想对某个对象发送一个请求,我们并不需要知道该请求的具体接收者是谁、具体的处理过程是如何的,而我们只知道在程序运行中指定具体的请求接收者即可,对于这样将请求封装成对象的模式我们称之为命令模式。所谓命令模式将请求封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象,同时命令模式支持可撤销的操作。

命令模式可以将请求的发送者和接收者之间实现完全的解耦,发送者和接收者之间没有直接的联系,发送者只需要知道如何发送请求命令即可,其余的可以一概不管,甚至命令是否成功都无需关心。同时我们可以非常方便的增加新的命令,但是可能会因此导致系统中会存在过多的具体命令类。其UML结构图如下:

命令模式

命令模式包含如下角色:

  • Command: 抽象命令类
  • ConcreteCommand: 具体命令类
  • Invoker: 调用者
  • Receiver: 接收者
  • Client:客户类

# 2.3.11 责任链模式

责任链模式描述的是请求如何沿着对象所组成的链进行传递的。它将对象组成一条链,发送者将请求发给链的第一个接收者,并且沿着这条链传递,直到有一个对象来处理它或者直到最后也没有对象处理而留在链末尾端。

避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止,这就是责任链模式。在责任链模式中,使得每一个对象都有可能来处理请求,从而实现了请求的发送者和接收者之间的解耦。同时责任链模式简化了对象的结构,它使得每个对象都只需要引用它的后继者即可,而不必了解整条链,这样既提高了系统的灵活性也使得增加新的请求处理类也比较方便。但是在责任链中我们不能保证所有的请求都能够被处理,而且不利于观察运行时特征。其UML结构图如下:

责任链模式

责任链模式包含如下角色:

  • Handler: 抽象处理者
  • ConcreteHandler: 具体处理者
  • Client: 客户类

# 3. 开发原则概述

在程序设计时,我们应该将程序功能最小化,每个类只干一件事。若有类似功能基础之上添加新功能,则要合理使用继承。对于多方法的调用,要会运用接口,同时合理设置接口功能与数量。最后类与类之间做到低耦合高内聚。

以下是七大开发原则:

设计原则 一句话归纳 目的
开闭原则 对扩展开放,对修改关闭 降低维护带来的新风险
依赖倒置原则 高层不应该依赖低层,要面向接口编程 更利于代码结构的升级扩展
单一职责原则 一个类只干一件事,实现类要单一 便于理解,提高代码的可读性
接口隔离原则 一个接口只干一件事,接口要精简单一 功能解耦,高聚合、低耦合
迪米特法则 不该知道的不要知道,一个类应该保持对其它对象最少的了解,降低耦合度 只和朋友交流,不和陌生人说话,减少代码臃肿
里氏替换原则 不要破坏继承体系,子类重写方法功能发生改变,不应该影响父类方法的含义 防止继承泛滥
合成复用原则 尽量使用组合或者聚合关系实现代码复用,少使用继承 降低代码耦合

实际上,这些原则的目的只有一个:降低对象之间的耦合,增加程序的可复用性、可扩展性和可维护性。

# 4. 开发原则详细介绍

# 4.1 开闭原则

Open Closed Principle,OCP:软件实体应当对扩展开放,对修改关闭。即, 当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。作用如下:

  • 对软件测试的影响:测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运行。
  • 提高代码的可复用性:粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性。
  • 提高软件的可维护性:稳定性高和延续性强,从而易于扩展和维护。

# 4.2 里氏替换原则

Liskov Substitution Principle,LSP:继承必须确保基类所拥有的性质在子类中仍然成立。即, 子类可以扩展父类的功能,但不能改变父类原有的功能。作用如下:

  • 是实现开闭原则的重要方式之一。
  • 克服了继承中重写父类造成的可复用性变差的缺点。
  • 类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。

# 4.3 依赖倒置原则

Dependence Inversion Principle,DIP:要面向接口编程,不要面向实现编程。即高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。作用如下:

  • 可以降低类间的耦合性。
  • 可以减少并行开发引起的风险。
  • 可以提高代码的可读性和可维护性。

# 4.4 单一职责原则

Single Responsibility Principle,SRP:单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分。如果一个对象承担了太多的职责,至少存在以下两个缺点:

  • 一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
  • 当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码的浪费。

遵循该原则有以下作用:

  • 降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多。
  • 提高类的可读性。复杂性降低,自然其可读性会提高。
  • 提高系统的可维护性。可读性提高,那自然更容易维护了。
  • 变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响。

# 4.5 接口隔离原则

Interface Segregation Principle,ISP:客户端不应该被迫依赖于它不使用的方法,一个类对另一个类的依赖应该建立在最小的接口上。

与单一职责原则的区别:

  • 单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
  • 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。

遵循该原则有以下作用:

  • 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
  • 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
  • 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
  • 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。

# 4.6 迪米特法则

Law of Demeter,LoD, 又叫作最少知识原则,如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。作用如下:

  • 降低了类之间的耦合度,提高了模块的相对独立性。
  • 由于亲合度降低,从而提高了类的可复用率和系统的扩展性。

# 4.7 合成复用原则

Composite Reuse Principle,CRP, 又叫组合/聚合复用原则,在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。作用如下:

  • 通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点:
    • 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
    • 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与
    • 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
  • 采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:
    • 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
    • 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
    • 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。

# 5. 参考资料

[1] JAVA设计模式总结之23种设计模式 from 博客园 (opens new window)

[2] Java 23种设计模式全归纳 from Github (opens new window)

[3] Java常用设计模式 from refactoringguru (opens new window)

[4] 设计模式超简单的解释 from Github (opens new window)

[5] 如何学习设计模式 from CSDN (opens new window)

[6] 用 Java 实现的设计模式 from Github (opens new window)

[7] 软件设计模式——七大设计原则 from 普通人 (opens new window)

Last Updated: 5/4/2023, 4:23:26 PM