设计模式

设计模式和软件工程作用是差不多的,都是为了更高效的软件开发

设计模式是软件开发中一个些典型问题的解决方法。设计模式主要关心当产品的 业务需求 发生变更时,要怎么做,让软件开发更加安全高效

软件的开发需要依据一些设计原则,违反了这些设计原则后,开发就会因为业务需求的变化而变的十分困难。这时候,就需要用到设计模式了

设计模式有三大类

类别 特点
创造型 关注类的创建,提供创造类的对象的机制
行为型 关注类的通讯和职责委派
结构型 介绍如何将对象和类组装成较大的结构

UML类图

classDiagram
Delivered --|> Base : 继承
Realization ..|> Interface : 实现
Penguin --> Climate : 关联
LiHua --o Chinese : 聚合
Wing --* Bird : 组合
Animal ..> Water : 依赖
  • 关联:一个类需要知道另外一个类的一些消息
  • 聚合:一种弱的拥有关系,体现的是A对象可以包含B对象,但是B对象不是A对象的一部分
  • 组合:一种强的拥有关系,体现部分和整体的关系,部分和整体的声明周期是相同的

设计原则 | Design Principles

  • 依赖倒置原则

    • 高层模块不应该依赖于底层模块,两者都应该依赖与抽象
    • 抽象不应该依赖于细节。细节应该依赖抽象

    针对接口(抽象)编程,不要针对实现(细节)编程

  • 单一职责原则

    • 一个类只应该有一个影响它变化的因素

    类的职责过多的话,当每一处的需求发生变化的话就会牵一发而动全身,把指着分开则可以实现代码的高效复用

  • 开放封闭原则

    • 类应该是可拓展的,但是不可修改的

    可拓展是为了应对变化,不可修改是为了防止错误

  • 里氏替换原则

    • 子类应该可以替换父类(IS-A)
  • 接口隔离原则

    • 接口应该小而完整
  • 优先使用对象组合,而不是类继承

    • 类的继承是白箱复用,对象组合是黑箱组合
    • 类继承破坏了封装性,子类父类耦合性高
    • 而对象组合则只要求被组合的对象具有良好定义的接口
  • 封装变化点

    • 应该将变化的部分和稳定的部分分开,设计者可以在分界的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合
  • 针对接口编程,而不是针对实现编程

    • 不将变量类型声明为某个特定的具体类,而是声明为某个接口
    • 客户程序无需获知对象的具体类型,只需要知道对象所具有的接口
    • 减少系统中各部分的依赖关系,从而实现“高内聚、松耦合”的类型设计方案

创造型模式 | Creational Patterns

提供创建对象的机制, 增加已有代码的灵活性和可复用性

主要的措施是绕开 new 然后避免紧耦合(编译时依赖)

简单工厂

针对的问题

针对由于需求的需要,创建的对象经常改变的情况。

例如在界面中之前使用 Lable 显示进度,现在要改为进度条。如果 Lable 在原先的代码中

实现的方法

classDiagram
	Product <.. Client
	Creator <.. Client
	ProductA <.. Creator
	class Product {
		<<Interface>>
		+doStuff()*
	}
	class Creator{
		+creatorProduct(flag)
	}
	Product <|.. ProductA
  • Creator::creatorProduct(flag) 中对传入的flag进行判断,通过条件分支创建不同的对象
  • Client通过Product *pd = Creator::creatorProduct(flag) 创建对象

带来的好处

简单工厂的出现能够让代码更加方便的管理。当使用 new 创建对象的时候就形成了稳定的模块依赖于变化的模块了,违反了依赖倒置原则。

如果不采用简单工厂方法,为了适应对象经常改变的这种情况,就需要在代码中不断的出现if-else这种条件语句,用来更改对应的对象,如果现在新添加一种类型的对象,就需要在每个条件语句进行更改,同时,所有更改的文件都需要重新编译,效率低下

如果采用简答工厂方法,添加新类型的时候就只需要在对应的工厂类中的Creator::creatorProduct(flag)进行更改

存在的问题

简单工厂每次修改都需要更改Creator::creatorProduct(flag),违反了开放封闭原则。当这些产品种类变的很多的时候,工厂的条件判断就会变的十分庞杂。同时很多人进行协作时,会对这个工厂进行频繁的修改,管理起来就会比较麻烦。

参考资料

https://blog.csdn.net/qq_22238021/article/details/79832092

工厂方法

针对的问题

解决和简单工厂一样的问题,但是针对简单工厂违反开放封闭原则的问题,将条件分支转化为了类接口的实现

工厂方法 = 简单工厂 + 策略模式

实现的方法

解决方法是为每个产品定义一个工厂,工厂负责创造产品的实例。所有的产品都继承自一个抽象产品类,所有的工厂都继承自一个抽象工厂类。使用的时候通过不同的工厂获得不同的产品

classDiagram
	Creator <.. Client
	Product <.. Client
	class Creator{
		<<Interface>>
		+creatProduct()*
	}
	class Product{
		<<Interface>>
		+doStuff()*
	}
	class CreatorA{
		+createProduct()
	}
	class CreatorB{
		+createProduct()
	}
	Creator <|.. CreatorA
	Creator <|.. CreatorB
	Product <|.. ProductA
	Product <|.. ProductB
	ProductA <.. CreatorA
	ProductB <.. CreatorB
  • createProduct() 返回具体 Createor 对应的 Product 对象(CreatorA::createProduct() { return new ProductA; })。
  • 构造 Product 对象时,通过调用 product *p = creator->creatProduct() 实现对象的创建,当业务需求时更改创建的对象时,只需更改 creator 指向的对象即可

特点

优点: 1、一个调用者想创建一个对象,只要知道其名称就可以了。 2、扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。 3、屏蔽产品的具体实现,调用者只关心产品的接口。

缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。

抽象工厂

主要是针对一系列相互依赖的对象的创建工作

具体的解决方法时提供一个接口,让这个接口负责创建一系列相互依赖的对象

classDiagram
	AbstractCreator <.. Client
	AbstractProductA <.. Client
	AbstractProductB <.. Client
	class AbstractCreator {
		<<Interface>>
		+createProductA()*
		+createProductB()*
	}
	class AbstractProductA {
		<<Interface>>
	}
	class AbstractProductB {
		<<Interface>>
	}
	class Creator1 {
		+createProductA()
		+createProductB()
	}
	class Creator2 {
		+createProductA()
		+createProductB()
	}
	AbstractCreator <|.. Creator1
	AbstractCreator <|.. Creator2
	AbstractProductA <|.. ProductA1
	AbstractProductA <|.. ProductA2
	AbstractProductB <|.. ProductB1
	AbstractProductB <|.. ProductB2
	ProductA1 <.. Creator1
	ProductB1 <.. Creator1
	ProductA2 <.. Creator2
	ProductB2 <.. Creator2

特点

优点:当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。

缺点:产品族扩展非常困难,要增加一个系列的某一产品,既要在抽象的 Creator 里加代码,又要在具体的里面加代码。

单例

解决只生成一个实例的问题

classDiagram
	class Singleton{
		-Singleton()
		+getInstance()$
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Singleton {
private:
Singleton() {}
~Singleton() {}
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;
Singleton(Singleton &&) = delete;
Singleton &operator=(Singleton&&) = delete;
public:
static Singleton &getInstance() {
static Singleton s;
return s;
}
}

行为模式 | Behavioral Patterns

负责对象间的高效沟通和职责委派

观察者

实现订阅-通知这种关系,一个对象的状态发生改变,就立即通知其他的对象,是一种一对多或者多对多的通知关系

classDiagram
	Publisher <.. Client
	Subscriber <.. Client
	class Publisher {
		<<Interface>>
		+attach(Subscriber)*
		+detach(Subscriber)*
		-notify()*
	}
	class ConcretePublisher {
		+getDate()
		-attach(Subscriber)
		-detach(Subscriber)
		-notify()
		-mainProgess()
	}
	class Subscriber {
		<<Interface>>
		update()*
	}
	class ConcreteSubscriber {
		update()
	}
	Publisher <|.. ConcretePublisher
	Subscriber <|.. ConcreteSubscriber
	Publisher <.. Subscriber
	ConcretePublisher <.. ConcreteSubscriber

策略

针对问题

某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使对象变得异常复杂;而且有时候支持不使用的算法也是一个性能负担

虽然可以将算法类抽象成一个对象,然后使用简单工厂创建。但是遇到算法更改十分频繁的时候,工厂类会同时部署起来也十分的麻烦,因此不是最好的解决方法。

实现的方法

定义一系列算法,把它们一个个封装起来,并且使它们可互相替换(变化)。该模式使得算法可独立于使用它的客户程 序(稳定)而变化(扩展,子类化)

classDiagram
	Context <.. Client
	StrategyA <.. Client
	StrategyB <.. Client
	StrategyC <.. Client
	Strategy --o Context
	class Strategy {
		<<Interface>>
		+Algorithm()*
	}
	class StrategyA {
		+Algorithm()
	}
	class StrategyB {
		+Algorithm()
	}
	class StrategyC {
		+Algorithm()
	}
	class Context {
		+Context(Strategy)
		+contextInterface()
	}
	Strategy <|.. StrategyA
	Strategy <|.. StrategyB
	Strategy <|.. StrategyC
  • Context::contextInterface() { strategy->Algorithm(); } 这个函数执行算法
  • 使用算法时,调用Context(new StrategyA())创建上下文后调用Context::Contextinterface()即可

特点

优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。

缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。

Strategy模式提供了用条件判断语句以外的另一种选择,消除条件判断语句,就是在解耦合。含有许多条件判断语句的代码通常都需 要Strategy模式。

模板方法

针对的问题

对于某一项任务,它常常有稳定的整体操作结构,但各个子步骤却有很多改变的需求,或者由于固有的原因 (比如框架与应用之间的关系)而无法和任务的整体结构同时实现

实现的方法

定义一个操作中的算法的骨架 (稳定),而将一些步骤延迟 (变化)到子类中。Template Method使得子类可以不改变 (复用)一个算法的结构即可重定义(override 重写)该算法的 某些特定步骤。

classDiagram
	class AbstractClass {
		+templateMethod()
		-step1()*
		-step2()*
		-step3()*
	}
	class Class1 {
		-step1()
	}
	class Class2 {
		-step2()
		-step3()
	}
	AbstractClass <|.. Class1
	AbstractClass <|.. Class2

特点

优点: 1、封装不变部分,扩展可变部分。 2、提取公共代码,便于维护。 3、行为由父类控制,子类实现。

缺点:每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。

结构型模式 | Structural Patterns

介绍如何将对象和类组装成较大的结构, 并同时保持结构的灵活和高效

装饰模式

针对的问题

在通过类的继承实现拓展的时候,有时候一些组合会使得拓展出来的子类数量急剧的膨胀

例如咖啡类。如果要在咖啡上添加牛奶、巧克力和糖,就会产生8个子类

装饰模式能够实现类的动态拓展,避免子类数量的膨胀

实现

classDiagram
	ConcreteComponent <.. Client
	DecoratorComponentA <.. Client
	DecoratorComponentB <.. Client
	class Component{
		<<Interface>>
		+operation()*
	}
	Component <|.. ConcreteComponent
	class ConcreteComponent{
		+operation()
	}
	Component <|-- Decorator
	class Decorator{
		<<Interface>>
		+Decorator(Component)
		+operation()
	}
	class DecoratorComponentA{
		+Decorator(Component)
		+operation()
		-addItem()
	}
	class DecoratorComponentB{
		+Decorator(Component)
		+operation()
		-addString
	}
	Decorator <|.. DecoratorComponentA
	Decorator <|.. DecoratorComponentB
  • 装饰类DecoratorComponentA/Boperation()中包括传入的componentopration()和自己添加的操作

    1
    2
    3
    4
    void DecoratorComponentA::operation() {
    component.operation(); // component是这个类构造函数中传入的对象
    ... // DecoratorComponentA自己的操作
    }
  • Client使用时,进行如下操作即可

    1
    2
    3
    4
    5
    6
    void doStuff() {
    Component *cc = new ConcreteComponent();
    DecorationComponentA *dca = new DecorationComponentA(cc);
    DecorationComponentB *dcb = new DecorationComponentB(dca);
    dcb->operation();
    }

特点

优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。

缺点:多层装饰比较复杂。

参考资料

  1. https://refactoringguru.cn/design-patterns/
  2. 程杰.《大话数据结构》

设计模式
https://fu-qingchen.github.io/2021/08/22/HUST/DesignPattern/
作者
FU Qingchen
发布于
2021年8月22日
许可协议