23个小案例带你吃透23种设计模式 | C++实现
前言:模式就是解决问题的固定套路,设计模式(Design pattern)就是一套经过前人反复使用,总结出来的程序设计经验。设计模式总共分为三大类:第一类是创建型模式 、第二类是结构型模式、第三种是行为型模式,共计23种标准设计模式。本文将通过5万字、23个经典小案例带你吃透23种设计模式。
🌏文章目录
✈UML基础
在软件开发流程中,一般应先对软件开发的过程进行建模,把要做什么功能、如何去实现、达到什么样的程度这些基本问题分析清楚了,才去写代码实现。建模是对现实按照一定规则进行简化,但应该体现出现实事物的特点。通过软件建模可以把现实世界中的问题转化到计算机世界进行分析和实现,软件建模的实现过程就是需求-建模-编码的一个过程。
UML统一建模语言,United Modeling Language,是一种面向对象的可视化建模语言,通过图形符号描述现实系统的组成,通过建立图形之间的关系来描述整个系统模型。
1. 类图
类图是面向对象系统建模中最常用的一种UML图,主要用来表示类与类之间的关系,包括泛化关系、关联关系、依赖关系和实现关系。
类图由三部分组成:类名、属性和方法。
- 表示private
+ 表示public
# 表示protected
点击选择类组件就可以进行设置,可以直接在组件上修改,也可以在右侧Editors修改。
- 属性表示为 属性名:类型
- 方法表示为 方法名(参数类型):返回值类型
2. 类与类之间的关系
😀泛化关系
Generalization,用来表示类与类之间的继承关系,也叫做is a kind of关系,用三角形的箭头从子类指向父类。
😀实现关系
Realization,实现关系就是类或接口的实现,由三角箭头虚线从实现类指向接口类,(比如C++中纯虚函数的实现)。可以在右侧查看类之间的关系。
😀依赖关系
Dependency,依赖关系是指在一个类中要用到另一个类的实例的一种关系,主要表现为一个类是另一个类的函数参数,或者一个类是另一个类的函数返回值的情况。在类图中的表现形式为一个虚线箭头从一个类指向被依赖的类。
具体代码表现形式为
😀关联关系
Association,关联关系是类和类之间对应的一种连结,是指两个独立的对象,一个对象的实例与另一个对象的一些特定实例存在固定的对应关系。关联关系可以是单向的也可以是双向的,通过关联使得一个类可以使用另一个类的属性和方法,在C++中具体的表现形式为,一个类的对象是另一个类的成员属性。在类图中,单向的是带箭头的实线,双向的是不带箭头的实线。
代码形式为
😀聚合关系
Aggregation,聚合关系表示整体和部分的关系,它是一种has-a的包含关系,而且整体和部分是可以分离的,即离开整体,部分也能存在。聚合关系是关联关系的一种,它是一种更强的关联关系。聚合关系在C++中的表现也是成员变量的方式,但是和关联关系不同的是,关联关系中的两个类是相互独立的,而聚合关系中的两个类是整体与部分的关系,一个类是另一个类的一部分。在类图中,空心菱形在代表整体的类那侧,另一侧为代表部分的类。
聚合关系的简单理解,比如手机和充电线,充电线是手机的一部分,手机就是整体,充电线是部分,但是没有手机了,充电线也可以单独存在。代码表现形式为:
在聚合关系中,我们在创建一个对象phone的时候可以不去管charger,因为在phone类中定义了set_charger方法用于构造charger,我们可以通过该方法在其它时机设置charger。
😀组合关系
Composite,也是关联关系的一种,是一种比聚合关系更强的关联关系。如果两个类是聚合关系(A是B的一部分),那么离开B之后,A就失去了存在的意义。组合关系是一种不可分离的关系。在类图中用实心菱形指在代表整体的类,另一侧为代表部分的类。
聚合关系的简单理解,屏幕screen是手机phone的一部分,并且屏幕screen离开手机phone之后就失去了存在的意义,这就是组合关系。代码表现形式为:
在组合关系中,创建phone对象的时候就已经构造了属性m_s,也就是整体phone的生命周期也决定了部分screen的生命周期,一旦phone生命周期结束了,screen的生命周期也结束了。在聚合关系中,没有这种强的生命周期的关联。
😀小结
对6种类与类之间的关系进行总结对比:
- 泛化关系和实现关系的区别:泛化关系是指C++中的继承关系;而实现关系是指虚基类的继承,在子类中实现虚基类的纯虚函数。
- 泛化关系和实现关系可以看成依赖关系的一种,因为它们离开依赖的类都无法编译通过。
- 聚合关系和组合关系是关联强度逐渐增强的关联关系;关联关系双方是平等的,聚合关系和组合关系的双方是整体和部分的关系。
- 聚合关系的双方整体和部分可以分离单独存在,没有生命周期的强相关;组合关系双方,部分离开整体将失去意义,整体的生命周期代表了部分的生命周期。
3. 设计模式七大原则
😁开放封闭原则
OCP,Open For Extension Closed For Modification Principle,简称开闭原则。开闭原则是指软件实体是可以扩展的,但是不可修改。也就是说,模块和函数是对扩展(提供方)开放的,对修改(使用方)关闭的,对于一个新的需求,对软件的改动应该是通过增加代码来实现的,而不是通过改动代码实现的。开闭原则是面向对象的核心,是最基础、最重要的设计原则,开发过程中,应该把可能会频繁变动的部分抽象出来,当需要变动时,只需要去实现抽象即可,也就是面向抽象编程。对于C++类来说,对类的改动是通过增加代码实现的,而不是修改代码实现的,通过虚基类的继承和虚函数的实现来完成一个类功能的扩充,这也是多态在设计模式中重要地位的体现。
举例来说,假如我们要创建一个迪迦奥特曼类,迪迦奥特曼有三种形态,最简单的方式就是在一个类中实现,每次都在类中增删查改
那么这样的话显然不满足开闭原则,实际上应该定义一个抽象类,这个抽象类只提供一个统一的接口,当需要增加功能时只需要继承这个抽象类,并实现抽象方法即可。
更进一步,我们可以提供一个接口,直接调用接口,把各种实现类的对象传递给抽象类的指针并产生多态。即使是子类的子类也可以传给抽象类产生多态。
😁单一职责原则
SRP,Single Responsibility Principle,单一职责原则。对类来说,类的职责应该是单一的,一个类只能对外提供一种功能。换句话说,变动这个类的理由或动机只能有一个,如果第二个改动类的理由,就不是单一职责。单一职责相当于降低了各种职责的耦合度,如果一个类负责多个职责,那么改动类的某一职责时,可能会影响到类行使其他职责的能力。
😁依赖倒置原则
DIP,Dependence Inversion Principle,依赖倒置原则。抽象不应该依赖于细节,细节应该依赖于抽象。换句话说,依赖于抽象接口,而不是依赖于具体的类的实现,也就是面向抽象接口编程。依赖倒置原则是面向对象编程的标志,在具体软件设计时,上层模块不应该依赖于底层模块,底层模块更不应该依赖上层模块,而是上层模块和底层模块都向中间靠拢,共同依赖于二者中间的抽象接口层。整个软件程序设计的依赖关系应该终止于抽象接口层,上层和底层互不关心,甚至使用什么编程语言都不关心。抽象接口层提供一个标准或者协议,它对上提供访问的接口,对下提供实现的标准,抽象接口层本身不执行任何操作,具体的功能由它的实现去完成。
举例来说,假如我们要组装一台电脑,现在要选择硬盘、内存、屏幕。那么电脑类要集成硬盘、内存、屏幕这些组件,但是我们希望电脑类和组件类之间不应该是相互依赖的关系,我们就可以直接给出一套接口,各个组件厂商只要实现这些抽象接口就可以装入我们的电脑中。
电脑类定义如下
各个厂商根据组件的抽象类去实现,来入围电脑类
这样组件类和电脑类都依赖于抽象接口层,两者都向接口层靠近,这就是面向接口编程,也就是我们的依赖倒置原则。我们直接把各个组件的实现类定义对象并传到电脑类中即可
假如后来,三星也想入围这个电脑,那么三星直接去实现屏幕的抽象类即可
我们再直接把三星的屏幕传入电脑类即可
这就是面向接口编程。
😁接口隔离原则
ISP,Interface Segegation Principle,接口隔离原则。一个接口对外只提供一种功能,不同功能应该通过不同的接口提供,而不是把多种功能都封装到一个接口中,否则的话,可能有的客户只需要功能A不需要功能B,但是提供A功能的接口内还封装了功能B,这就造成了客户被迫依赖某种他们不需要的功能。
😁里氏替换原则
LSP,Liskov Substitution Principle,里氏替换原则。任何地方出现的抽象类,都可以使用该抽象类的实现类来代替,这其实类似于C++中的类型兼容性原则,所有基类出现的地方都可以用子类对象来代替。实际上,继承增强了类与类之间的耦合性,在继承中应该尽量不要重写(覆盖)基类中的非抽象方法,子类可以有自己的方法,但是不能重新定义或修改基类的方法,否则如果基类中发生改变,所有的子类都可能受影响,应该尽量使用组合或者聚合而不是继承。其实,准确来说,应该是尽量不要继承可实例化的类(非抽象类),而应该是从抽象类中继承。
举例来说,应该去实现function1()这样的抽象方法,而不能去覆盖基类的方法function2()。当子类覆盖或实现基类的方法时,方法的前置条件(形参)要比基类方法的输入参数宽松;当子类实现父类的抽象方法时,方法的后置条件(方法返回值)要比基类更严格。
😁合成复用原则
CARP,Composite/Aggregate Reuse Principle,优先使用对象组合而不是继承原则。使用继承的时候,基类的变化可能会影响到子类,而如果使用组合/聚合就可以降低这种依赖关系。组合/聚合降低了类与类之间的耦合性,一个类的变化对另一个类的影响相对较小,如果要使用继承,必须要遵守里氏替换原则。
😁迪米特法则
LOD,Law of Demeter,迪米特法则,也叫做最少知道原则(The Least Knowledge Principle)。一个类对于其它类知道的越少越好,只和朋友说话,不要和陌生人说话,这里的朋友是指作为成员变量、方法输入输出参数的类(朋友类),如果是出现在一个方法内部的类就不是朋友类。迪米特法则降低了耦合度,各个模块之间通过一个接口来实现调用,而模块之间不需要知道对方的内部实现逻辑,并且一个模块的内部改变也不会影响到另一个模块(黑盒原理)。如果两个类不直接通信,那么这两个类就不应当发生直接的相互作用。如果一个类需要调用另一个类的某个方法的话,可以通过第三个类转发这个调用。
朋友圈确定:
- 当前对象本身;
- 以参量形式传入到当前对象方法中的对象;
- 当前对象的实例变量直接引用的对象;
- 当前对象的实例变量如果是一个聚集,那么聚集中的元素都是朋友;
- 当前对象所创建的对象;
满足条件之一就是朋友,否则就是陌生人。
4. 什么是设计模式
模式就是解决问题的固定套路,设计模式(Design pattern)就是一套经过前人反复使用,总结出来的程序设计经验。设计模式总共分为三大类:
第一类是创建型模式 ,该模式通常和对象的创建有关,涉及到对象实例化的方式。包括:单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式五种;
第二类是结构型模式,结构型模式描述的是如何组合类和对象来获得更大的结构。包括:代理模式、装饰者模式、适配器模式、桥接模式、组合模式、外观模式、享元模式共7种模式。
第三种是行为型模式,用来描述对类或对象怎样交互和怎样分配职责。共有:模板模式、命令模式、责任链模式、策略模式、中介者模式、观察者模式、备忘录模式、访问者模式、状态模式、解释器模式、迭代器模式11种模式。
🚀一、单例模式
1. 什么是单例模式
单例模式是创建型模式的一种,正常情况下,我们定义一个类是可以创建很多个对象的,而单例模式顾名思义就是指一个类只能创建一个实例对象,也就是说在整个程序空间中,这个类只有一个对象,并且对外提供一个全局访问点来访问这个唯一的实例对象。单例模式主要分为两类:
饿汉式单例模式:一开始就创建好了一个唯一的对象;
懒汉式单例模式:在使用实例对象的时候去创建该唯一的对象;
单例模式的结构图:
2. 单例模式的实现
2.1 懒汉式单例模式
😂如何保证只有一个实例对象
当我们在使用类来new创建一个对象的时候,会自动调用构造函数,每创建一个对象都会调用构造函数来构造一个新的对象
在上面程序中,我们new了两个对象,会调用两次构造函数,并创建出两个不同的对象,我们可以直接通过判断来测试一下
既然我们希望这个类只有一个实例对象,那么就应该禁止类的外部访问构造函数,因为每次在类的外部调用构造函数都会构造出一个新的实例对象。解决办法就是把构造函数设置为私有属性,在类的内部完成实例化对象的创建,这样就对外隐藏了创建对象的方法。但是类的出现就是要定义对象的,我们要使用类创建的对象,所以还需要提供一个全局访问点来获取类内部创建好的对象
上面程序所示的就是一个懒汉式单例模式的实现。这里面有几点要注意的:
(1)为了让这个类所定义的所有对象共享属性,应该把属性设置为static类型,因为static类型的属性属于整个类而不是属于某个对象。
(2)为了保证单例模式,应该在全局访问点get_single()函数中加一个判断,如果对象已经被创建了,那么就直接返回这个对象,如果对象还没有被创建,那么久new创建一个对象,并返回该对象。
(3)因为是在使用到对象的时候,才去创建对象(single初始化为NULL,在全局访问点get_single被调用的时候才去创建对象),有点偷懒的感觉,所以称之为懒汉式单例模式。
我们再来测试一下,是不是真正的实现了单例
运行测试程序,看打印结果
通过打印结果可以看到,创建的两个对象s1和s2是相等的,也就是说我们实现了单例,通过全局访问点获取的实例对象是同一个。
通过上面的分析,我们可以得到实现单例模式的步骤
1. 构造函数私有化;
2. 提供全局访问点;
3. 内部定义一个static静态指针指向当前类定义的对象;
😂懒汉式单例模式的缺陷
通过懒汉式单例模式,我们实现了一个类只创建一个实例对象,且只有在用到实例对象的时候,才会通过全局访问点去new创建这个对象,节省了资源。但是,懒汉式单例模式有一个致命的缺点,就是在C++的构造函数中,不能保证线程安全。什么意思呢,也就是说,在多个线程都去创建对象,调用全局访问点get_single()的时候,会面临资源竞争问题,假如在类的构造函数中增加一个延迟函数,我们第一个线程调用get_single()的时候,会进入构造函数,这时,因为延时的存在,第一个线程可能会在这里卡顿一会,假如正好这时候第二个线程也调用get_single()去创建实例对象,而第一个线程还在构造函数中延时,这样在get_single()函数中(single == NULL)这个判断条件依然成立,第二个线程也会进入构造函数。这样,两个线程创建的对象就不再是同一个对象了,也就不是单例模式了。下面,我们就详细分析多线程与懒汉式。
2.2 懒汉式单例模式与多线程(DCL与饿汉式)
😂多线程构造对象
首先,我们把类改造一下,在构造函数中加一个延时,并在类中加一个计数器来记录构造函数的调用次数
这样,一个类就定义好了。接下来,我们要在main进程中创建三个线程,每个线程都去创建一个对象,在Windows下多线程编程应包含头文件<process.h>,并且会用到线程创建函数_beginthread(),对于_beginthread()函数的使用可以直接转到源码查看函数原型
该函数包含三个参数,分别代表如下含义:
第一个参数是_beginthread_proc_type,通过上面的typedef可知,它是一个回调函数(函数指针),指向新开辟的线程的起始地址;
第二个参数_StackSize是新线程的堆栈大小,可以直接给个0,表示和主线程共用堆栈;
第三个参数_ArgList是一个参数列表,它表示要传递给新开辟线程的参数,新线程没有参数的话可以传入NULL;
函数返回值可以理解为创建好的线程的句柄。
首先搭建测试程序如下
这里用到了一个函数WaitForSingleObject(),它用于等待子线程结束。因为子线程是依附于主线程存在的(共用堆栈、内存四区),如果子线程还没结束主线程就结束了,那么子线程也将不复存在,所以需要等待子线程结束后,主线程才能结束。该函数类似于Linux中的thread_join函数。
搭建好测试程序后,在定义一个线程函数
编译运行测试函数
可以看到,构造函数调用了三次,每个线程都创建了一个新的对象,已经不再是单例模式了。对于这个问题的解决主要有两种,下面分别介绍。
😂饿汉式单例模式
第一种解决方法就是在类中定义static SingletonPattern*指针的时候就创建一个对象,在全局访问点get_single()直接返回创建好的对象,因为对象早就提前创建好了,这样即使多个线程调用创建对象所得到的也是同一个对象。因为对象还没使用就创建好了,所以叫做饿汉式单例模式。
上面的测试程序不用修改,我们只需要修改类即可
再次运行前面的测试函数,看打印结果
从打印结果可以看到,三个不同的线程只调用了一次类的构造函数,得到的是同一个对象。
😂 DCL(double-checked locking)
既然多个线程会竞争资源,那么如何才能防止多个线程之间的竞争呢?最简单的方法就是对临界区资源加一个锁🔒,当一个线程持有锁的时候,其他线程挂起等待锁的释放,只有持有锁的线程才能进入临界资源,这就解决了多线程资源竞争的问题(此处涉及到多线程同步问题)。这里还有一个问题,当我们第一次判断(single == NULL)后,如果之前没有创建对象,那么就进入下面的临界区
当第一个线程创建完对象后释放了锁,第二个线程进入临界区又创建了一个对象,这也违反了单例原则。所以应该加入一个二次检查,如果第一个线程已经创建了对象(指针不为NULL),那么第二个线程即使获取了锁,也不再创建新的对象,而是直接使用第一个线程创建的对象,这就是二次检测的原因。
对全局访问点get_single()修改过后,再次运行测试函数
3. 总结
单例模式主要有懒汉式和饿汉式两种实现,饿汉式不会有线程安全的问题,但是提前构造对象占用了一定的资源,如果对内存要求较低的场景可以使用饿汉式实现;懒汉式应使用DCL机制来避免多线程竞争资源的问题,并且懒汉式可以在需要使用对象的时候才去创建对象,节省了资源。
🚀二、工厂模式
1. 简单工厂模式
1.1 什么是简单工厂模式
Simple Factory Pattern,简单工厂模式,也叫做静态工厂方法模式(Static Factory Method Pattern)。属于类的创建型设计模式的一种,通过一个专门的类(工厂)来负责创建其他类的实例(具体产品),这些类都有一个共同的抽象类作为基类(抽象产品)。
简单工厂模式中的三个角色
- 工厂角色:Creator,用于创建所有的具体产品实例,是简单工厂模式的核心。工厂类直接被外部调用,并提供一个静态方法,根据传入的参数不同来创建不同产品类的实例。
- 抽象产品角色:Product,它是工厂类所创建的所有实例的类的共同基类,用于描述产品的公共接口。它和工厂类是依赖的关系(作为工厂类静态方法的返回值)。
- 具体产品角色:Concrete Product,它是抽象产品类的子类,也是工厂类要创建的目标类。
1.2 简单工厂模式实例
假设我们要生产一个手机,由苹果手机,小米手机,首先我们应该定义一个抽象产品类PhoneInterface
然后根据抽象产品类来实现具体的产品类,假设我们有苹果手机和小米手机
接下来就应该创建一个工厂类,在工厂类中定义了创建所有产品类实例的逻辑,以及选择创建哪个产品类实例的判断。
这样,客户就可以通过工厂类来生产客户所需要的手机,并可以成功完成手机型号的生产。
1.3 简单工厂模式的优缺点
优点:工厂类是整个简单工厂模式的核心,通过工厂类对外隐藏了创建实例的具体细节,用户直接使用工厂类去创建自己需要的实例,而不必关心实例是如何创建出来的,也不必关心内部结构是怎么组织的。
缺点:正所谓成也萧何,败也萧何,简单工厂模式的优点来源于工厂类,其缺点也来源于工厂类,因为所有实例的创建逻辑都集中在工厂类中,一旦工厂出现问题,所有实例的创建都将无法进行,并且增删产品都要去修改工厂类来实现,不符合开闭原则。因为简单工厂模式不符合开闭原则,所以它不是标准的设计模式。
2. 工厂模式
2.1 什么是工厂模式
factory pattern,工厂模式同样属于类的创建型模式,也成为多态工厂模式。工厂模式对简单工厂模式不遵守开闭原则这一缺点做了修正,工厂模式多出了一个抽象工厂角色作为接口,实际的生产工作在具体工厂类中实现,这样进一步的抽象化使得工厂方法模式可以使系统在不修改具体工厂角色的情况下引进新的产品。简单来说,就是把简单工厂中的工厂细分为不同产品的工厂,每个工厂生产一种产品。
- 抽象工厂角色(Creator),所有具体工厂都要实现这个接口;
- 具体工厂(Concrete Creator),负责实例化具体产品对象;
- 抽象角色(Product),和简单工厂模式一样,它是工厂类所创建的所有实例的类的共同基类,用于描述产品的公共接口。
- 具体产品角色(Concrete Product),具体工厂类所要实例化的对象。
2.2 工厂模式实例
抽象产品类和具体产品类不变
首先定义一个抽象的工厂类,来定义具体工厂的统一接口
然后定义一个苹果手机工厂,用于生产苹果手机,再定义一个小米手机工厂,用于生产苹果手机,那么这两个工厂就是具体工厂角色
当我们需要生产手机产品的时候,直接使用苹果手机工厂去创建苹果手机对象,用小米手机工厂去创建小米手机对象即可。假如,我们增加需求,需要华为手机,这时只要增加一个华为工厂和华为手机的实现类即可,工厂的抽象类和手机的抽象类都不用动,符合开闭原则。
2.3 简单工厂与工厂模式对比
简单工厂模式把所有的创建逻辑都放在了一个工厂类中,而工厂模式则是提供了一个抽象的工厂接口,由具体工厂去创建产品实例,极大的方便了增加产品,删除产品等操作,且很好的符合了开闭原则,简单工厂模式每次增删产品都要直接修改工厂类,而工厂模式只需要增加一个具体工厂类和一个具体产品类就可以完成功能的扩充。
在简单工厂模式中,客户端是面向具体工厂编程,增加产品要在工厂类中修改代码;而工厂模式是面向抽象工厂编程,增加产品只要创建一个新的具体工厂就可以了,这就是面向抽象类编程。
🚀三、抽象工厂模式
1. 什么是抽象工厂模式
Abstract Factory Pattern,抽象工厂模式。假设在前面的工厂模式下,我们对产品线提出了进一步的要求,因为手机既要在国内销售,又要在国外销售,所以同一品牌手机增加了大陆版、美版、港版等产品线。如果使用工厂模式,每一个品牌的手机,每一个版本的手机都要单独一个具体工厂来创建,很不方便‘这时候,就有了抽象工厂模式,抽象工厂模式可以创建一个产品族(包含多条产品线)。抽象工厂模式用官方语言描述就是,一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。 她也有四种角色,和工厂模式一样。
- 抽象工厂角色(Creator),所有具体工厂都要实现这个接口,可以创建多个不同等级的产品(多条产品线);
- 具体工厂(Concrete Creator),负责实例化具体产品对象,多条产品线;
- 抽象角色(Product),和简单工厂模式一样,它是工厂类所创建的所有实例的类的共同基类,用于描述产品的公共接口。
- 具体产品角色(Concrete Product),具体工厂类所要实例化的对象。
2. 抽象工厂模式实例
首先创建一个抽象手机类
根据抽象手机类定义大陆版苹果手机类,美版苹果手机类,大陆版小米手机类,美版小米手机类
接下来定义一个抽象工厂类,该抽象工厂类中包含两个接口,一个是苹果手机生产线,一个是小米手机生产线
根据抽象工厂来定义两个具体工厂,一个是大陆版手机工厂,一个是美版手机工厂
客户可以直接使用两个具体工厂去生产大陆版苹果/小米手机或美版苹果/小米手机
3. 抽象工厂类总结
当增加一个新的产品族时,只需要增加一个新的具体工厂即可,如果整个产品族只有一个等级的产品,比如只有一条生产线,抽象工厂就和工厂模式一样了。
🚀四、建造者模式
1. 什么是建造者模式
Bulider Pattern,建造者模式,也叫做生成器模式,是一种对象创建型模式。建造者模式用于创建具有多个部件的复合对象,并隐藏了复合对象的创建过程,不同的部件建造者(Builder生成器)有不同的建造方法。通过建造者模式实现了对象的构建和对象的表示的分离,也就是说,通过同样的构建过程(建造逻辑)可以创建出不同的表示(使用不同的建造者产生不同的建造方式)。
建造者模式中的4种角色:
- 抽象建造者角色Builder:为建造各个组件提供统一的抽象接口;
- 具体建造者角色ConcreteBuilder:实现抽象建造者提供的抽象接口,定义各个组件的建造方法,是组件建造的具体实施者;
- 指挥者Director:调用具体建造者来建造产品的各个组件,指挥者并不知道产品的具体信息,指挥者只负责规定并保证各个组件的建造过程和建造逻辑(指挥建造的过程,比如先装发动机再装轮子);
- 产品角色Product:被建造的复杂对象,包含组合对象的各个部件;
2. 建造者模式的实现
首先我们定义一个汽车产品类,这个汽车包含外壳、发动机、车轮三个部件,并且汽车产品类中应该包含设置各个部件和获取各个部件的方法。汽车产品类是我们最终要建造的目标,是客户的需求。
定义一个抽象的建造者基类,类中统一了建造部件的接口和返回产品成品的方法。
定义具体建造者类,具体建造者类是产品部件的具体建造者,也就是汽车的生产商,我们定义两个汽车生产商,一个厂家生产卡车,一个厂家生产火车。
最后,应该定义一个指挥者,指挥者是汽车的设计师,它负责规划建造汽车的逻辑步骤,指挥汽车厂家(具体建造者)干活,而具体的工作有汽车厂家去干,所以指挥者不关心汽车的具体细节,只负责设计建造汽车各个部件的逻辑关系,比如先制造汽车外壳,然后安装发动机,最后安装车轮。
最后是根据客户需求去建造汽车,假设客户需要一辆卡车。
假如客户提出新需求,需要一辆火车,那么我们直接让指挥者去指挥火车厂商生产即可。
3. 建造者模式和工厂模式对比
建造者模式和工厂模式的区别是:工厂模式强调的是结果,不考虑对象的建造过程,只关注产生一个客户所需要的结果。比如,客户需要一辆大众汽车,那么就直接使用大众汽车工厂来生产一辆大众汽车,只关注大众汽车这个结果,不关心汽车外壳、发动机、轮子等部件的建造过程。建造者模式强调的是建造过程,要关注每一个部件的建造方式,以及各个部件的建造逻辑,最终组合出需要的对象。
🚀五、原型模式
1. 什么是原型模式
Prototype Pattern,原型模式是一种对象创建型模式,它采取复制原型对象的方法来创建对象的实例,所以称之为Clone,被复制出来的对象具有和原型一摸一样的数据,并且在通过Clone创造另一个一模一样的对象时,不需要知道创造的过程。根据对象克隆深度层次的不同,有浅度克隆与深度克隆。
2. 原型模式的实现
既然原型模式是复制一个一模一样的对象,那么就一定要注意潜在的深拷贝浅拷贝问题。
3. 创建型设计模式总结
顾名思义,创建型设计模式就是处理对象创建过程的设计模式。创建型模式主要是将系统所需要的用到的具体类封装起来,在内部实现这些具体类的创建和结合,并对外隐藏这个过程细节。创建型设计模式主要包括:
- 单例模式
- 简单工厂模式
- 工厂模式
- 抽象工厂模式
- 建造者模式
- 原型模式
其中,简单工厂模式因为不符合开闭原则,它不属于标准的23种设计模式。
🚀六、代理模式
1. 什么是代理模式
Proxy Pattern,代理模式,是构造型的设计模式之一,它可以为其他对象提供一种代理来控制对这个对象的访问。所谓的代理,就是指一个具有与被代理对象(代理元)相同接口的类,客户端只有通过Proxy来实现与被代理类的交互,并且在交互过程中 ,代理可以增加其它操作。
代理可以分为多个种类
- 远程代理:可以隐藏一个对象在不同地址空间的事实,可以理解为将工作委托给远程的代理(服务器)来完成;
- 虚拟代理:通过代理来存放需要很长时间实例化的对象;
- 安全代理:用来控制真实对象的访问权限;
- 智能引用:当调用真实对象时,代理处理另外一些事;
在代理模式中,有四种角色
- 抽象主题角色subject:真实主题与代理主题的共同接口,并供客户端使用;
- 真实主题角色RealSubject: 定义了代理角色所代表的真实实体。
- 代理主题角色Proxy:含有一个引用,使得代理可以访问真实主题角色,并提供一个和抽象主题角色相同的接口,以便于代理角色可以用来替代真是主题角色。代理角色通常在将客户端调用传递给真实主题对象之前或者之后执行某些操作,而不是单纯返回真实的对象,也就是说代理可以执行真是主题角色之外的其他操作。
2. 代理模式的案例
假设现在有一个客户需要买车,他可以去实体店买,也可以在瓜子二手车买,瓜子二手车就是一个代理,它可以销售实体店的车,并提供节假日打折活动。
🚀七、装饰模式
1. 什么是装饰模式
Decorator Pattern,装饰模式,也叫做包装模式,是结构型模式的一种。装饰模式动态的给一个对象增加了额外的功能,并且这种扩充功能的方式对客户是透明的。装饰模式的具体实现就是把一些功能封装在一个个单独的子类中,并让这些子类包含要被装饰的对象,当有需要功能扩充的时候,客户就可以有选择的通过装饰类来装饰某个对象。装饰模式可以理解为继承的一种替代,他比继承更加灵活,客户可以根据需要自由选择。
- Component:被装饰的主体,定义了一个对象接口,可以给这些对象增加装饰功能;
- ConcreteComponent:具体的对象,继承于Component;
- Decorator:装饰者,继承于Component并从Component类的外部完成对Component类功能的扩充,并且Component类并不需要知道装饰者的存在;
2. 装饰模式案例
假设我们要设计一个游戏角色Hero,它最初只有跑步的技能,但是通过升级可以完成功能扩充,比如飞行,发射激光等。
首先定义一个Component角色,也就是要被装饰的主题的抽象接口。
然后定义一个具体的要被装饰的对象ConcreteComponent
最后添加两个装饰者,每个装饰者都可以完成对被装饰对象不同的功能扩充,一个装饰者可以为英雄角色扩充飞行技能,另一个装饰者可以为英雄角色扩充激光发射功能
最后是客户需求,首先我们让一个英雄角色从跑步到飞行再到激光发射,一步步的获取技能
然后我们在创建一个英雄角色,它可以直接获取激光功能
通过这两个英雄角色我们可以看到,客户可以根据需求任意组合对待装饰对象的功能扩充。
🚀八、适配器模式
1. 什么是适配器模式
Adapter Pattern,构造型模式之一,也叫做变压器模式和装饰模式都是一种包装模式(Wrapper),通过适配器模式可以改变现有类的接口形式。适配器模式可以将一个类的接口转换成客户希望的另一种形式的接口,使得原本因接口不兼容而无法工作的类可以一起工作,适用于双方都不适合修改的场景。
- Target:客户期待的接口,可以是抽象类或者接口;
- Adapter:适配器,对Target和Adaptee进行适配,通过在内部包装一个Adaptee对象,来把源接口转换成目标接口;
- Adaptee:适配者,也就是需要被适配的类;
2. 适配器模式案例
假设我们现在只有一个RS232标准的接口,但是客户需求一个TTL标准的接口,这时就可以通过一个适配器把RS232接口适配成TTL电平标准。
🚀九、组合模式
1. 什么是组合模式
Composite Pattern,组合模式,是一种结构型模式。
组合模式将对象组合成树形结构以表示“整体-部分”的层次结构,并使得用户对单个对象和组合对象的使用具有一致性。
组合模式构造了一个树形的对象结构,并且通过一个属性对象可以可以访问整棵树的对象。
组合模式的三种角色:
- Component:抽象角色,代表树形结构的抽象结点,它定义了所有实现类的统一接口(属性、方法、行为),并提供了访问和管理子结点的接口;简言之,Component定义了Leaf和Composite共同的操作接口,比如增加子结点,删除子结点等行为,是一个抽象基类。
- Leaf:叶子结点,叶子结点中没有子结点,类似于树中的叶子结点,或文件系统中的文件。
- Composite :枝结点,可以存储子结点,并实现子结点的操作,类似于文件系统的文件夹。
2. 组合模式案例
首先定义一个抽象类,为Leaf和Composite提供统一接口,假设要做一个文件系统,文件夹(Composite)中可以放入文件和文件夹,文件(Leaf)中不可以放入任何子结点。
定义一个文件类,文件中不可加入子结点
定义一个文件夹类,可以加入问价或文件夹
客户端操作
🚀十、桥接模式
1. 什么是桥接模式
Bridge Pattern,桥接模式,是一种结构型设计模式。
桥接模式基于类的最小设计原则,通过使用封装,聚合以及继承等行为来让不同的类承担不同的责任。它的主要特点是把抽象(abstraction)与行为实现(implementation)分离开来,从而可以保持各部分的独立性以及应对它们的功能扩展。
也就是说,通过桥接模式可以实现抽象部分与实现部分的解耦合,使得抽象和实现都可以独立的发生变化,当通过继承不能实现开闭原则的时候,就可以考虑桥接模式。比如,图形和颜色,把图形设计一个抽象类,颜色设计一个抽象类,然后根据需要的图形去实现图形类,并根据需要的颜色实现颜色类,通过两个抽象类就可以实现颜色和图形的组合。
- Abstractions:抽象类接口,包含了一个对行为实现类Implementor的引用,也就是说二者具有关联关系。
- Refined Abstraction:抽象类接口的实现类,实现了Abstractions中定义的接口,并可以调用Implementor中的方法(包含了Implementor的引用)。
- Implementor:行为实现类接口,定义了一系列操作。
- Concretelmp lementor:具体实现类,实现了Implementor中的操作。
2. 桥接模式案例
实现对不同图形上不同颜色,首先定义两个抽象类,颜色类和图形类
定义三个具体的颜色
实现具体的图形
客户端操作,为图形上色
🚀十一、外观模式
1. 什么是外观模式
Facade Pattern,外观模式,结构型设计模式之一。外观模式为一个子系统集合提供了一个一致的简单界面,并且外观包含了对各个子系统的引用,客户端可以通过这个外观来访问各个子系统。
外观模式中的角色:
- Facade:外观类,作为调用接口,提供所有子系统的一致访问界面;
- Clients:通过外观角色来访问各个子系统;
- Subsystem:子系统,实现子系统的功能;
2. 外观模式案例
首先创建子系统集合,假设为电脑系统,定义显示屏子系统、主机子系统、键盘子系统
如果客户不适应外观类,而是直接访问这个子系统,那么操作将会比较繁琐
要创建每个子系统对象,并逐个调用其方法来访问子系统。
创建一个外观类,它包含对所有子系统的引用
通过外观来访问所有子系统
🚀十二、享元模式
1. 什么是享元模式
Flyweight Pattern,享元模式,结构型设计模式。
在面向对象系统的设计和实现中,创建对象是最常见的操作,这里就会有一个问题:如果一个应用程序使用了太多对象,就会造成很大的存储开销。特别是对于大量轻量级(细粒度)的对象,比如在文档编辑器的设计过程中,我们如果为每一个字母创建一个对象的时候可能会因为大量的对象而造成存储开销的浪费,例如一个字母a在文档中出现了10000次,而实际上我们可以让这10000个a共享一个对象,这种情况下,我们可以将对象状态分为内部状态和外部状态。在享元模式中可以共享(不会变化)的状态称为内部状态(Intrinsic State),而那些需要外部环境来设置的不能共享的内容称为外部状态(Extrinsic State),其中外部状态和内部状态是相互独立的,外部状态的变化不会引起内部状态的变化。内部状态直接存储在对象中,而外部状态(字母a的大小、字体)可以在适当时机作为参数传递给对象。享元模式的目的就是使用共享技术来实现大量细粒度对象的复用。
享元模式的角色:
- Flyweight:抽象享元角色,定义了所有具体享元类的接口,通过这个接口可以传入外部状态并作用于外部状态;
- FlyweightFactory:享元工厂,创建并管理享元对象;
- ConcreteFlyweight:可共享的内部状态;
- UnsharedConcreteFlyweight:可以不共享的享元子类;
2. 享元模式案例
假设一个字符串和一个标志作为一对,可以通过标志来确定字符串,比如只要传入标志1就得到字符串hello,也就是说,通过flag可以确定一个字符串。
创建一个工厂
客户端通过工厂获取享元对象,并传入一个标志
🚀十三、模板模式
1. 什么是模板模式
Template Pattern,模板方法模式,是一种行为型模式。通过模板模式可以把特定步骤的算法接口定义在抽象基类中,通过子类继承对抽象算法进行不同的实现来达到改变算法行为的目的。通俗来讲就是,在抽象类中定义好算法步骤并统一接口,在子类中实现接口,这就实现了算法操作步骤和算法实现的解耦合。模板模式一般应用于,具有同样的操作步骤,但是这些操作的细节不同的场景。
- AbstractClass:定义了算法的框架和步骤;
- ConcreteClass:实现AbstractClass中的方法;
2. 模板模式的案例
我们把穿衣服看做一个固定流程,先穿外套,再穿裤子,最后穿鞋
然后定义穿睡衣,穿西装两个类,穿衣服的顺序都是固定的,但是穿的衣服有所不同
最后客户端执行穿衣服操作
🚀十四、命令模式
1. 什么是命令模式
Command Pattern,命令模式,是一种行为型设计模式。命令模式就是把命令对象、命令的创建者、命令的调用者、命令的执行者(接收者)分离,首先看一个命令模式中的四个角色:
- Command:抽象命令,定义了操作的接口,把命令封装成一个类,通过继承在ConcreteCommand中来实现具体的命令操作;
- ConcreteCommand:具体命令,实现抽象命令的接口,是被命令调用者Invoker调用的主体,并且包含了一个对命令接收者Receiver的引用;
- Receiver:命令的执行者,收到命令后执行相应的操作;
- Invoker:命令的调用者,客户创建命令,并通过Incoker去调用命令,Invoker包含了对Command的引用,并负责去调用命令中的操作;
2. 命令模式的案例
以办理银行业务为例,首先创建一个银行职员作为命令接收者,他可以执行存款取款操作。
创建一个命令抽象类,并通过继承实现一个存款命令类和一个取款命令类,这两个具体命令方法分别可以调用命令执行者的存款操作和取款操作。
创建一个命令调用者,它可以调用命令对象(命令调用者Invoker中包含了命令Command的引用,它可以调用命令,而具体命令ConcreteCommand中包含了命令接收者Receiver的引用,具体命令可以调用Receiver的操作,客户通过命令调用者Invoker来命令Receiver进行相应操作)。
最后客户端创建命令,并通过Invoker来调用命令,使Receiver执行相应操作
🚀十五、责任链模式
1.什么是责任链模式
Chain of Responsibility Pattern,CoR责任链模式,是行为型设计模式之一。责任链模式就像一个链表,将对象连成一个链式结构,并沿着这条链传递请求,直到请求被某个对象处理。在责任链模式中,客户端只要把请求放到对象链上即可,不需关心请求的传递过程和处理细节,实现了请求发送和请求处理的解耦合。
- Handler:抽象处理者,定义了处理请求的接口,并包含一个指向下一个对象的指针;
- ConcreteHandler:具体处理者,负责处理请求或把请求沿着对象链传递给下一个具体处理者;
2. 责任链模式案例
定义抽象处理者和具体处理者
客户端发出请求
🚀十六、策略模式
1. 什么是策略模式
Strategy Pattern,策略模式,行为型模式之一。策略模式可以定义一个算法族,把一系列算法封装起来并提供一个统一接口,这样算法之间的切换或其他变化不会影响客户端。其关键在于,把算法的抽象接口封装在一个类中,算法的实现由具体策略类来实现,,而算法的选择由客户端决定。
- Strategy:抽象策略类,定义算法族的统一接口;
- ConcreteStrategy:具体策略类,实现了具体的算法操作;
- Context:上下文,包含一个策略类的引用,根据不同策略执行不同操作,策略的选择由客户端决定;
2. 策略模式的案例
定义一个排序算法策略
定义上下文
客户端选择具体排序算法
🚀十七、中介者模式
1. 什么是中介者模式
Mediator Pattern,中介者模式,行为型模式之一。类与类之间的交互都放在一个中介对象中进行,即类通过中介和另一个类交互,类与类之间不用互相引用就能实现交互,降低了类与类之间的耦合。但是需要通过中介者进行交互的类中包含了中介者的引用,而中介者也包含了所有需要交互的类的引用。举例来说,比如男女相亲,男生女生之间互不相识(交互双方不需互相引用),但是他们都认识媒人(交互者包含了中介者的引用),而且没人也认识男生和女生(中介者包含了交互者的引用),再比如第三方招聘平台、招聘者、应聘者之间的关系也是这样。
- Mediator:抽象中介者,定义了同事对象到中介者对象的接口;
- ConcreteMediator:具体中介者,它包含了所有具体同事类的引用,并实现抽象中介类中的接口;
- Colleague:抽象同事类;
- ConcreteColleague:具体同事类,每个同事类只知道自己的行为,但是他们都包含中介类的引用(都认识中介);
2.中介者模式案例
大学生通过BOOS直聘找工作,那么大学生和HR就需要通过BOOS直聘找工作,BOOS直聘就是中介,首先定义一个抽象中介类。
定义具体的中介BOOS直聘,大学生和公司HR通过BOOS直聘求职或招聘。
定义抽象同事类,也就是需要交互的类,他应该包含一个中介类的引用,因为大学生和HR都应该认识BOOS直聘平台。
定义学生类和HR类
大学生和HR通过BOOS进行交互
🚀十八、观察者模式
1. 什么是观察者模式
Observer Pattern,观察者模式,行为型模式之一。观察者模式提供了一种一对多的模式,多个观察者对象同时监听一个主题对象,一旦主题对象发生变化,能够自动通知所有的观察者对象。它提供了一种关联对象之间的同步机制,他们之间通过通信来保持状态同步。
- Subject:抽象主题角色,被观察的对象,当被观察的状态发生变化时,会通知所有的观察者,Subject一般包含了所有观察者对象的引用(集合);
- ConcreteSubject:具体主题,被观察者的具体实现,当状态发生改变时,向所有观察者发出通知;
- Observer:抽象观察者,提供统一的接口,在得到通知时执行某些操作;
- ConcreteObserver:具体观察者,实现抽象观察者提供的接口;
2. 观察者模式案例
假设两军作战,观察者为士兵
被观察者为哨兵,一旦发现敌军,则通知所有士兵,被观察者含有一个观察者类型的容器,用于保存所有观察者的引用。
士兵收到通知,进行统一的行动,假设军队总共三个士兵
🚀十九、备忘录模式
1. 什么是备忘录模式
Memento Pattern,备忘录模式,行为型模式之一。备忘录模式在不破坏封装性的前提下,捕获一个对象的内部状态,并在外部保存,并在需要的时候恢复对象以前的状态。可以理解为,在备忘录中保存了一个对象的备份,当对象已经发生了改变,并且我们需要恢复对象以前的状态时,可以通过一个备忘录管理器来恢复以前的状态。
- Originator:原生者角色,需要在备忘录中保存的对象,负责创建一个备忘录,并进行保存和恢复状态的操作;
- Memento:备忘录,用于保存Originator的内部状态;
- Caretaker:管理者,包含一个备忘录的引用,负责操作和管理备忘录;
2. 备忘录模式案例
定义一个备忘录
定义一个原生者
定义一个管理者
客户端测试,修改一个对象属性,并通过备忘录恢复状态
🚀二十、访问者模式
1. 什么是访问者模式
Visitor Pattern,访问者模式,是一种行为型设计模式。访问者模式把数据结构和作用于数据结构上的操作进行了分离,在不修改已有类的前提下可以增加新的操作,而新增新的操作就相当于新增一个访问者。
- Visitor:抽象访问者角色,声明了访问操作的方法,方法的参数为被访问的元素;
- ConcreteVisitor:具体访问者角色,实现抽象访问者中声明的方法;
- Element:抽象元素角色,声明接受访问的操作,并接收一个访问者对象作为参数;
- ConcreteElement:具体元素角色,实现被访问的操作;
- ObjectStructure:结构对象角色,包含一个具体元素的引用的容器;
2. 访问者模式案例
定义一个抽象访问者类和一个抽象元素类
以领导访问公司部门为例,创建两个公司部门
创建访问者,董事长和部门分管领导
首先在客户端实现,分管部门的领导对自己的部门进行访问
然后我们在客户端增加操作,假设董事长要来视察,因为董事长职位最大,可以对整个公司进行访问,这是可以创建一个结构对象角色,把所有的部门都包含进来
然后在客户端进行操作,首先把各个部门对象加入到集团公司对象中,然后董事长进行访问
🚀二十一、状态模式
1. 什么是状态模式
State Pattern,状态模式,是一种行为型设计模式。通过改变对象的内部状态来达到改变对象行为的目的,“这个对象表现得就好像改变了它的类一样”。其实说白了就是,根据用户是输入的条件,满足一定条件就改变对象的行为,不同条件执行不同的操作。
- State:抽象状态,定义了一个接口,接口声明了一个与上下文环境相关的状态的行为;
- ConcreteState:具体状态,定义了本状态的行为和转换到另一个状态的判定条件;
- Context:上下文、环境,负责状态的转换,包含了一个表示当前状态的State类型的引用;
2. 状态模式案例
定义状态类
实现状态对应行为
定义一个Context类
客户端实现状态行为的执行和转换
🚀二十二、解释器模式
1. 什么是解释器模式
interpreter Pattern,解释器模式,是一种行为型模式。解释器模式提供一种对自定义语句的解释机制,解释器模式包含以下几种角色:
- Context:解释器的上下文环境,包含了不属于解释器的其他信息;
- AbstractExpression:抽象解释器,定义了一个抽象的解释操作接口;
- ConcreteExpression:具体解释器,实现对相关操作的解释;
2. 解释器模式案例
定义上下文环境
定义抽象解释器类,并实现一个加法解释器,加法解释器的操作是对传入的数据执行+1操作
客户端操作,向加法解释器传入一个数据,打印处理结果
🚀二十三、迭代器模式
1. 什么是迭代器模式
Iterator Pattern,迭代器模式,是行为型模式的一种。迭代器模式提供了一种从外部遍历访问一个容器的方法,并且在不需知道容器内部细节的前提下就可以完成对容器的顺序遍历。所以,创建迭代器的容器应该将自身的引用(this指针)传递给迭代器,迭代器通过持有的这个容器的引用来实现对容器的遍历。
- Iterator:迭代抽象类,用于提供实现迭代的最小方法集,一般包括获取开始对象、获取下一个对象、获取当前对象、判断是否结束这几个方法;
- ConcreteIterator:具体的迭代器,实现抽象迭代器定义的方法;
- Aggregate:聚集抽象类,可以理解为一个容器的接口;
- ConcreteAggregate:具体聚集类,容器的实现类;
2. 迭代器模式案例
定义一个抽象的迭代器类
定义抽象容器类
实现一个int类型的迭代器
定义一个int类型的容器
客户端,通过迭代器来遍历数组
- 点赞
- 收藏
- 关注作者
评论(0)