施磊C++ | 进阶学习笔记 | 5.设计模式

五、设计模式

文章目录

这里贴出常用的23中设计模式。视频课程仅包含部分,剩余部分需要找其他课程或者资料进行自学。

1.设计模式三大类型概述

C++设计模式是一套被广泛认可的用于解决常见软件设计问题的最佳实践,它们可以帮助开发者编写更加清晰、可维护和可扩展的代码。根据解决的问题类型,设计模式通常被分为三大类:创建型、结构型和行为型。以下是对每一大类的概述及其特点:

一、创建型设计模式

创建型设计模式主要关注于对象的创建机制,帮助使系统独立于如何创建、组合和表示对象。

  • 特点
    • 将对象的创建和使用分离,增加代码的灵活性和可维护性。
    • 通过定义创建对象的接口或方法,使得子类或具体实现类可以决定实例化哪个类。
  • 常见模式
    • 单例模式(Singleton):确保一个类只有一个实例,并提供一个全局访问点。
    • 工厂方法模式(Factory Method):定义一个用于创建对象的接口,让子类决定实例化哪一个类。
    • 抽象工厂模式(Abstract Factory):提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
    • 建造者模式(Builder):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
    • 原型模式(Prototype):通过复制现有的实例来创建新的实例,而不是通过新建类。
二、结构型设计模式

结构型设计模式关注于类和对象的组合,用于形成更大的结构,以解决如何将对象和类组合成较大的结构,同时保持结构的灵活和高效。

  • 特点
    • 通过组合和继承等方式,将对象或类组合成更大的结构。
    • 强调对象之间的静态关系,以及如何通过不同的组合方式获得更加灵活的程序结构。
  • 常见模式
    • 适配器模式(Adapter):将一个类的接口转换成客户期望的另一个接口。
    • 桥接模式(Bridge):将抽象部分与实现部分分离,使它们可以独立变化。
    • 组合模式(Composite):将对象组合成树形结构以表示"部分-整体"的层次结构。
    • 装饰器模式(Decorator):动态地给一个对象添加一些额外的职责。
    • 外观模式(Facade):提供一个统一的接口,用来访问子系统中的一群接口。
    • 享元模式(Flyweight):运用共享技术有效地支持大量细粒度的对象。
    • 代理模式(Proxy):为其他对象提供一种代理以控制对这个对象的访问。
三、行为型设计模式

行为型设计模式特别关注对象之间的通信,以及如何通过对象之间的协作来实现特定的功能。

  • 特点
    • 强调对象之间的动态关系,以及如何通过对象之间的交互来实现特定的行为。
    • 通过定义对象之间的交互规则和通信方式,使得系统更加灵活和可扩展。
  • 常见模式
    • 责任链模式(Chain of Responsibility):为请求创建一个接收者对象的链。
    • 命令模式(Command):将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化。
    • 解释器模式(Interpreter):给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
    • 迭代器模式(Iterator):提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露其内部的表示。
    • 中介者模式(Mediator):用一个中介对象来封装一系列的对象交互。
    • 备忘录模式(Memento):在不破坏封装的前提下,捕获并保存一个对象的内部状态,以便在将来的时间点上恢复对象到这个状态。
    • 观察者模式(Observer):定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
    • 状态模式(State):允许一个对象在其内部状态改变时改变它的行为。
    • 策略模式(Strategy):定义一系列的算法,把它们一个个封装起来,并使它们可相互替换。
    • 模板方法模式(Template Method):定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。
    • 访问者模式(Visitor):表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

综上所述,C++中的创建型、结构型和行为型设计模式各具特点,分别关注于对象的创建、组合以及对象之间的通信和协作。这些设计模式在软件开发中具有重要的应用价值,可以帮助开发者编写更加清晰、可维护和可扩展的代码。

2.设计模式三大原则

设计模式的三大原则通常指的是开闭原则 (Open/Closed Principle)、里氏替换原则 (Liskov Substitution Principle)和依赖倒置原则(Dependency Inversion Principle),它们是面向对象设计的基本原则,旨在提高代码的灵活性、可维护性和可扩展性。以下是这三个原则的清晰简洁解释:

  1. 开闭原则(Open/Closed Principle)

    • 解释:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着当需要添加新功能时,应该通过扩展现有代码(例如添加新类、新接口等)来实现,而不是修改已有代码。
    • 目的:提高代码的灵活性和可维护性,减少因修改已有代码而引入的潜在错误。
  2. 里氏替换原则(Liskov Substitution Principle)

    • 解释:子类必须能够替换它们的基类而不会导致程序出错。这要求子类必须完全遵守基类所定义的接口契约,即子类在替换基类时,其行为应该与基类保持一致。
    • 目的:确保系统的稳定性和可靠性,避免子类破坏基类的行为预期。
  3. 依赖倒置原则(Dependency Inversion Principle)

    • 解释:高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。这意味着在设计中,应该通过接口或抽象类来定义高层模块和低层模块之间的交互,而不是直接依赖于具体的实现类。
    • 目的:降低模块之间的耦合度,提高系统的可扩展性和可维护性。通过依赖抽象而不是具体实现,可以更容易地在不改变高层模块的情况下替换低层模块的实现。

这三大原则共同构成了面向对象设计的基础,它们指导我们如何设计更加灵活、可维护和可扩展的软件系统。遵循这些原则,可以帮助我们避免常见的设计问题,提高代码的质量和可维护性。

3.单例模式

单例模式:一个类不管创建多少次对象,永远只能得到该类型的一个对象的实例

常用到的,比如日志模块,数据库模块

需要注意的五个点

1、需要将构造函数私有化,这样保证使用者无法通过构造函数创建新的单例对象

2、需要定义一个唯一的static实例对象

3、需要提供对外的接口返回这个唯一的实例对象

4、需要删除拷贝构造函数和赋值运算符重载函数,保证使用者不能通过者二者构造新的对象

5、在类内声明了static对象,还需要在类外进行定义

分为两类:

饿汉式单例模式:还没有获取实例对象,实例对象就已经产生了
懒汉式单例模式:唯一的实例对象,直到第一次获取它的时候,才产生(初始化)

1.饿汉单例模式

饿汉单例模式 一定是线程安全的

**饿汉式单例模式在类加载时就创建实例。**这种方式的特点是线程安全,因为实例在类加载时就已经被初始化,而类加载是线程安全的(由类加载器保证)。此外,饿汉式单例模式的实现相对简单。然而,它的缺点是即使实例没有被使用,它也会在类加载时被创建,这可能会导致内存浪费。

创建步骤:

1.构造函数私有化 使得用户不能随意调用构造函数,没有那么轻易的创建对象的实例

2.定义一个唯一的类的实例对象(既然已经让用户难以调用构造函数,那么类应该提供这个唯一的实例化对象)

3.定义接口让用户有办法获取类的唯一实例化对象的方法,通常返回的都是指针类型

c++ 复制代码
#include<iostream>
#include<thread>
#include<list>
#include<mutex>
#include<atomic>
#include<queue>
#include<condition_variable>
using namespace std;

class Singleton
{
public:
	static Singleton* getInstance()//3.定义接口让用户有办法获取类的唯一实例化对象的方法
	{
		return &instance;
	}
private:
	static Singleton instance;//2.定义一个唯一的类的实例对象(既然已经让用户难以调用构造函数,那么类应该提供这个唯一的实例化对象)
	Singleton()
	{

	}// 1.构造函数私有化 使得用户不能随意调用构造函数,没有那么轻易的创建对象的实例
	Singleton(const Singleton&) = delete;
	Singleton& operator = (const Singleton&) = delete;
};

Singleton Singleton::instance;

int main()
{
    //打印出来的p1 p2 p3 都是同一块地址
	Singleton* p1 = Singleton::getInstance();
	Singleton* p2 = Singleton::getInstance();
	Singleton* p3 = Singleton::getInstance();
	cout << p1 << " " << p2 << " " << p3 << endl;

	return 0;
}
2.懒汉单例模式

**懒汉式单例模式在首次使用时才创建实例。**这种方式的特点是实现了延迟加载,即只有在需要实例时才创建它,从而节省了内存。

把静态变量设置为指针,通过初始化为空的方式不去分配内存,直到使用时(调用get)才去分配内存。

创建步骤:

1.构造函数私有化 使得用户不能随意调用构造函数,没有那么轻易的创建对象的实例

2.定义一个唯一的类的实例对象(既然已经让用户难以调用构造函数,那么类应该提供这个唯一的实例化对象)

3.定义接口让用户有办法获取类的唯一实例化对象的方法,通常返回的都是指针类型

c++ 复制代码
#include<iostream>
#include<thread>
#include<list>
#include<mutex>
#include<atomic>
#include<queue>
#include<condition_variable>
using namespace std;

class Singleton
{
public:
	static Singleton* getInstance()//3.定义接口让用户有办法获取类的唯一实例化对象的方法
	{
		if (instance == nullptr)
		{
			instance = new Singleton();
		}
		return instance;
	}
private:
	static Singleton *instance;//2.定义一个唯一的类的实例对象(既然已经让用户难以调用构造函数,那么类应该提供这个唯一的实例化对象)
	Singleton()
	{

	}// 1.构造函数私有化 使得用户不能随意调用构造函数,没有那么轻易的创建对象的实例
	Singleton(const Singleton&) = delete;
	Singleton& operator = (const Singleton&) = delete;
};

Singleton* Singleton::instance = nullptr;

int main()
{
	Singleton* p1 = Singleton::getInstance();
	Singleton* p2 = Singleton::getInstance();
	Singleton* p3 = Singleton::getInstance();
	cout << p1 << " " << p2 << " " << p3 << endl;

	return 0;
}

然而,懒汉式单例模式在多线程环境下可能会出现线程安全问题,即多个线程可能会同时创建实例,导致违反单例原则。为了解决这个问题,可以在创建实例的方法上加上同步关键字(synchronized),但这会降低性能。

为了解决懒汉式单例模式在多线程环境下的线程安全问题和性能问题,可以采用双重检查锁定(Double-Checked Locking)和volatile关键字。双重检查锁定可以确保在创建实例时只进行一次同步操作,而volatile关键字可以确保变量的可见性和禁止指令重排序,从而避免在创建实例时出现线程安全问题。

4.线程安全的懒汉单例模式

**可重入函数:**这个函数还没执行完,可不可以再被调用一次

在单线程中不可能发生(除了递归),在多线程中可能,线程1还没运行完,线程2就来运行了

如果这个函数可以在多线程环境下直接运行而且不发生竞态条件,那就是可重入函数

而懒汉单例模式中,getIntance并不是线程安全的

线程1进去了,还没给instance赋值,时间片到了给了线程2,那线程2就给instance赋值了,所以不是可重入函数,所以懒汉单例模式并不是线程安全的

1.锁+双重判断
c++ 复制代码
#include<iostream>
#include<thread>
#include<list>
#include<mutex>
#include<atomic>
#include<queue>
#include<condition_variable>
using namespace std;

mutex mtx;
//锁+双重判断
class Singleton
{
public:
	static Singleton* getInstance()
	{
		//锁的粒度太大了 在单线程环境中也要不停地加锁解锁
		//lock_guard<mutex> guard(mtx);
		if (instance == nullptr)
		{
			//放到if里面,只要创建过,后面就不会进来if
			lock_guard<mutex> guard(mtx);
			if (instance == nullptr)
			{
				instance = new Singleton();
				/*
					1.开辟内存
					2.构造对象
					3.给instance赋值
				*/
			}
		}
		return instance;
	}
private:
	//不加volatile的默认情况下,线程会对代码数据段拷贝一份副本,自己看自己的副本,加上以后不拷贝副本,只要instance发生改变,所有线程都能立马看到它改变了
	//注意在下面初始化的时候也要加上volatile关键字
	static Singleton * volatile instance;
	Singleton()
	{

	}
	Singleton(const Singleton&) = delete;
	Singleton& operator = (const Singleton&) = delete;
};

Singleton* volatile Singleton::instance = nullptr;

int main()
{
	Singleton* p1 = Singleton::getInstance();
	Singleton* p2 = Singleton::getInstance();
	Singleton* p3 = Singleton::getInstance();
	cout << p1 << " " << p2 << " " << p3 << endl;

	return 0;
}

注意:1.不加volatile的默认情况下,线程会对代码数据段拷贝一份副本,自己看自己的副本,加上以后不拷贝副本,只要instance发生改变,所有线程都能立马看到它改变了

2.在下面初始化的时候也要加上volatile关键字

3.new具体步骤补充

  1. 开辟内存new 操作符首先为对象分配足够的内存空间。这是通过调用底层的内存分配函数(如 malloc,尽管在 C++ 中更常见的是使用 operator new)来完成的。这个步骤确保了对象有足够的空间来存储其数据成员。
  2. 构造对象 :一旦内存被分配,new 操作符就会在该内存位置上调用类的构造函数来初始化对象。这是对象实际被"创建"或"构造"的时刻,它的数据成员被赋予初始值(如果有的话)。
  3. instance 赋值 :最后,new 操作符返回指向新构造对象的指针,这个指针随后被赋值给静态成员变量 instance。这一步是将新创建的对象与类的静态成员变量关联起来的关键。
2.简洁的线程安全懒汉单例模式
c++ 复制代码
class Singleton
{
public:
	static Singleton* getInstance()
	{
		static Singleton instance;
		return &instance;
	}
private:
	Singleton()
	{

	}
	Singleton(const Singleton&) = delete;
	Singleton& operator = (const Singleton&) = delete;
};

在C++中,类的静态局部变量的内存确实在程序启动时就已经为其预留,但是变量的初始化会延迟到第一次执行到它所在的代码块,所以这也是一种懒汉单例模式

而函数静态局部变量的初始化,在汇编指令上已经自动添加线程互斥指令了,因此不用担心线程安全的问题

c++ 复制代码
#include<iostream>
#include<thread>
#include<list>
#include<mutex>
#include<atomic>
#include<queue>
#include<condition_variable>
using namespace std;

class Singleton
{
public:
	static Singleton* getInstance()
	{
		static Singleton instance;
		return &instance;
	}
private:
	Singleton()
	{

	}
	Singleton(const Singleton&) = delete;
	Singleton& operator = (const Singleton&) = delete;
};

int main()
{
	Singleton* p1 = Singleton::getInstance();
	Singleton* p2 = Singleton::getInstance();
	Singleton* p3 = Singleton::getInstance();
	cout << p1 << " " << p2 << " " << p3 << endl;

	return 0;
}

5.简单工厂(Simple Factor)、工厂方法(Factory Method)

工厂模式:主要是封装了对象的创建操作

1.简单工厂
c++ 复制代码
#include<iostream>
#include<thread>
#include<list>
#include<mutex>
#include<atomic>
#include<queue>
#include<condition_variable>
#include<memory>
using namespace std;

class Car
{
public:
	Car(string name):_name(name){}
	virtual void show() = 0;

	string _name;
};

class BMW :public Car
{
public:
	BMW(string name) :Car(name) {}
	void show()
	{
		cout << "获得了一辆宝马汽车" << _name << endl;
	}
};

class Audi :public Car
{
public:
	Audi(string name) :Car(name) {}
	void show()
	{
		cout << "获得了一辆奥迪汽车" << _name <<endl;
	}
};

enum CarType
{
	Bmw, AUDI
};

class SimpleFactory
{
public:
	Car* createCar(CarType ct)
	{
		switch (ct)
		{
		case Bmw:
			return new BMW("X1");
		case AUDI:
			return new Audi("A6");
		default:
			cerr << "传入工厂的参数不正确:" << ct << endl;
			break;
		}
		return nullptr;
	}
};

int main()
{
	/*1.原来是这样的,但是对于用户来说根本不需要知道什么X1,X6什么的
	Car* p1 = new BMW("X1");
	Car* p2 = new Audi("A6");
	*/

	/*2.
	SimpleFactory* factory = new SimpleFactory();
	Car* p1 = factory->createCar(Bmw);
	Car* p2 = factory->createCar(AUDI);
	p1->show();
	p2->show();
	*/

	//3.使用智能指针管理资源
	unique_ptr<SimpleFactory> factory(new SimpleFactory());
	unique_ptr<Car> p1(factory->createCar(Bmw));
	unique_ptr<Car> p2(factory->createCar(AUDI));

	p1->show();
	p2->show();

	return 0;
}

该例子中使用SimpleFactory类封装两个汽车类的创建操作

一共2种使用方法,即代码中的2种,直接用或者通过智能指针间接用

简单工厂模式(Simple Factory)的缺点主要包括以下几个方面:

  1. 违反开闭原则
    • 开闭原则要求软件实体(类、模块、函数等)应该是可扩展的,但不可修改的。然而,在简单工厂模式中,每当需要增加新的产品时,都需要修改工厂类中的判断逻辑,从而违反了开闭原则。
  2. 高内聚问题
    • 简单工厂模式中的工厂类通常负责所有产品的创建,这导致工厂类的职责过重,不符合高内聚的原则。高内聚要求一个模块或类应该只负责一个功能或一个紧密相关的功能集合。
  3. 不利于扩展和维护
    • 由于简单工厂模式中的工厂类集中了所有产品的创建逻辑,随着产品种类的增加,工厂类的逻辑将变得越来越复杂,不利于系统的扩展和维护。
    • 当需要添加新产品时,需要修改工厂类的代码,这增加了代码的维护成本。
  4. 测试困难
    • 在简单工厂模式中,由于工厂类与具体产品类之间存在紧密的耦合关系,这增加了单元测试的难度。为了测试某个具体产品类,可能需要先实例化工厂类,并调用其创建方法,这可能会引入不必要的依赖和复杂性。
  5. 缺乏灵活性
    • 简单工厂模式通常使用静态方法或全局方法来创建对象,这限制了对象的创建方式和灵活性。例如,在某些情况下,可能需要使用不同的创建策略或根据不同的上下文创建不同的对象实例,但简单工厂模式无法提供这种灵活性。

所以有了工厂方法和抽象工厂

2.工厂方法
c++ 复制代码
#include<iostream>
#include<thread>
#include<list>
#include<mutex>
#include<atomic>
#include<queue>
#include<condition_variable>
#include<memory>
using namespace std;

class Car
{
public:
	Car(string name):_name(name){}
	virtual void show() = 0;

	string _name;
};

class BMW :public Car
{
public:
	BMW(string name) :Car(name) {}
	void show()
	{
		cout << "获得了一辆宝马汽车" << _name << endl;
	}
};

class Audi :public Car
{
public:
	Audi(string name) :Car(name) {}
	void show()
	{
		cout << "获得了一辆奥迪汽车" << _name <<endl;
	}
};

class Factory
{
public:
	virtual Car* createCar(string name) = 0;//这个就是所谓的工厂方法
};

class BMWFactory :public Factory
{
public:
	Car* createCar(string name)
	{
		return new BMW(name);
	}
};

class AudiFactory :public Factory
{
public:
	Car* createCar(string name)
	{
		return new Audi(name);
	}
};

int main()
{
	unique_ptr<Factory> bmwfty(new BMWFactory());
	unique_ptr<Factory> audifty(new AudiFactory());
	unique_ptr<Car> p1(bmwfty->createCar("X6"));
	unique_ptr<Car> p2(audifty->createCar("A8"));

	p1->show();
	p2->show();

	return 0;
}

Factory的纯虚函数就是工厂方法

其实就是对每个类有又单独创建了一个创建它的对象的类,就相当于封装了

1.完成了对对象的封装操作

2.贴合了软件的开闭原则(对原来已有的功能封闭,对扩展新功能开放)

一个工厂对应了一个类的创建,如果类很多的话会导致工厂也很多

缺点:灵活性受限

  • 工厂方法模式通常用于创建单个产品对象,如果需要创建多个相关或依赖的产品对象,可能需要使用其他模式(如抽象工厂模式)来替代。

6.抽象工厂(Abstract Factory)

对有一组关联关系的产品簇提供产品对象的统一创建

c++ 复制代码
#include<iostream>
#include<thread>
#include<list>
#include<mutex>
#include<atomic>
#include<queue>
#include<condition_variable>
#include<memory>
using namespace std;

class Car
{
public:
	Car(string name):_name(name){}
	virtual void show() = 0;

	string _name;
};

class BMW :public Car
{
public:
	BMW(string name) :Car(name) {}
	void show()
	{
		cout << "获得了一辆宝马汽车" << _name << endl;
	}
};

class Audi :public Car
{
public:
	Audi(string name) :Car(name) {}
	void show()
	{
		cout << "获得了一辆奥迪汽车" << _name <<endl;
	}
};

//车的相关系列产品 车灯
class Light
{
public:
	virtual void show() = 0;
};

class BmwLight :public Light
{
public:
	void show() { cout << " BMW Light" << endl; }
};

class AudiLight :public Light
{
public:
	void show() { cout << " Audi Light" << endl; }
};

//抽象工厂 对有一组关联关系的产品簇提供产品对象的统一创建
class AbstractFactory
{
public:
	//这个就是所谓的工厂方法
	virtual Car* createCar(string name) = 0;//工厂方法 创建车
	virtual Light* createCarLight() = 0;//工厂方法 创建汽车关联的产品车灯
};

class BMWFactory :public AbstractFactory
{
public:
	Car* createCar(string name)
	{
		return new BMW(name);
	}
	Light* createCarLight()
	{
		return new BmwLight();
	}
};

class AudiFactory :public AbstractFactory
{
public:
	Car* createCar(string name)
	{
		return new Audi(name);
	}
	Light* createCarLight()
	{
		return new AudiLight();
	}
};

int main()
{
	unique_ptr<AbstractFactory> bmwfty(new BMWFactory());
	unique_ptr<AbstractFactory> audifty(new AudiFactory());
	unique_ptr<Car> p1(bmwfty->createCar("X6"));
	unique_ptr<Car> p2(audifty->createCar("A8"));
	unique_ptr<Light> l1(bmwfty->createCarLight());
	unique_ptr<Light> l2(audifty->createCarLight());

	p1->show();
	p2->show();

	l1->show();
	l2->show();
	return 0;
}

缺点:不支持单一产品的变化

  • 抽象工厂模式适用于一组相关产品的创建,但如果只有一个产品发生变化,那么整个工厂都需要进行修改,可能不够灵活。

其他的类甚至也要重写AbstractFactory里面新加的这个产品,不然自己的类会变成虚函数,但是实际上其他类本身也不提供这个产品(比如宝马课程生产一个螺丝奥迪可能就没有,这个时候就挺尴尬)

小结:

简单工厂 Simple Factory :

**优点:**把对象的创建封装在一个接口函数里面,通过传入不同的标识,返回创建的对象

客户不用自己负责new对象,不用了解对象创建的详细过程

**缺点:**提供创建对象实例的接口函数不闭合,不能对修改关闭

工厂方法 Factory Method

**优点:**Factory基类,提供了一个纯虚函数(创建产品),定义派生类(具体产品的工厂)负责创建对应的

产品,可以做到不同的产品,在不同的工厂里面创建,能够对现有工厂,以及产品的修改关闭

**缺点:**实际上,很多产品是有关联关系的,属于一个产品簇,不应该放在不同的工厂里面去创建,这样

一是不符合实际的产品对象创建逻辑,二是工厂类太多了,不好维护

抽象工厂 Abstract Factory

**优点:**把有关联关系的,属于一个产品簇的所有产品创建的接口函数,放在一个抽象工厂里面AbstractFactroy,派生类(具体产品的工厂)应该负责创建该产品簇里面所有的产品

**缺点:**抽象工厂模式适用于一组相关产品的创建,但如果只有一个产品发生变化,那么整个工厂都需要进行修改,可能不够灵活。

7.代理模式(Proxy)

通过代理类,来控制实际对象的访问权限

代理模式(Proxy Pattern) 是一种结构型设计模式,它提供一个对象的代理,以控制对这个对象的访问。代理对象作为客户端和目标对象之间的中介,客户端通过代理对象间接地访问目标对象。代理模式常用于延迟加载、访问控制、缓存等功能。

优点
  1. 隐藏实现细节:客户端通过代理对象访问目标对象,不需要知道目标对象的具体实现。
  2. 增强目标对象:可以在不修改目标对象代码的情况下,为目标对象添加额外的功能。
  3. 控制访问:可以对目标对象的访问进行权限控制。
  4. 减少系统开销:例如,通过代理实现延迟加载,减少系统资源的消耗。
缺点
  1. 性能损耗:代理对象会增加一层调用开销,虽然这个开销通常很小,但在高性能要求的场景下可能会成为瓶颈。
  2. 代码复杂度增加:引入代理模式后,系统的代码复杂度会增加。

步骤:

1.抽象公共类

2.委托类(继承自公共类)

3.代理类(继承自公共类)

4.以组合的方式使用代理对象

5.客户直接访问代理对象 相当于客户只能访问助理,不能直接访问老板

c++ 复制代码
#include<iostream>
#include<thread>
#include<list>
#include<mutex>
#include<atomic>
#include<queue>
#include<condition_variable>
#include<memory>
using namespace std;

//客户   助理proxy   老板 委托类

class VideoSite//1.抽象类
{
public:
	virtual void freeMovie() = 0;//免费看电影
	virtual void vipMovie() = 0;//vip看
	virtual void ticketMovie() = 0;//用券才能看
};

class FixBugVideoSite:public VideoSite//2.委托类
{
public:
	virtual void freeMovie()
	{
		cout << "观看免费电影" << endl;
	}
	virtual void vipMovie()
	{
		cout << "观看vip电影" << endl;
	}
	virtual void ticketMovie()
	{
		cout << "用券观看电影" << endl;
	}
};

//3.代理类
class FreeVideoSiteProxy :public VideoSite
{
public:
	FreeVideoSiteProxy() { pVideo = new FixBugVideoSite(); }
	~FreeVideoSiteProxy() { delete pVideo; }
	virtual void freeMovie()
	{
		pVideo->freeMovie();//通过代理对象的freemovie访问委托类真正的freemovie
	}
	virtual void vipMovie()
	{
		cout << "您目前只是普通用户,需要升级VIP才能观看VIP电影" << endl;
	}
	virtual void ticketMovie()
	{
		cout << "您目前没有券,需要购买电影券才能观看该电影" << endl;
	}
private:
	VideoSite* pVideo;//4.以组合的方式使用代理对象
	//或者去掉构造和析构,直接调用委托类也行 
	//FixBugVideoSite Video
};

class VipVideoSiteProxy :public VideoSite
{
public:
	VipVideoSiteProxy() { pVideo = new FixBugVideoSite(); }
	~VipVideoSiteProxy() { delete pVideo; }
	virtual void freeMovie()
	{
		pVideo->freeMovie();//通过代理对象的freemovie访问委托类真正的freemovie
	}
	virtual void vipMovie()
	{
		pVideo->vipMovie();
	}
	virtual void ticketMovie()
	{
		cout << "您目前没有券,需要购买电影券才能观看该电影" << endl;
	}
private:
	VideoSite* pVideo;
};

//这些都是通用的API接口,使用的都是基类的指针或者引用 通过多态访问虚函数就是了
void watchMovice(unique_ptr<VideoSite> &ptr)
{
	ptr->freeMovie();
	ptr->vipMovie();
	ptr->ticketMovie();
}

int main()
{
	/*1.只有委托类,没有代理类
	同一个用户p1看的时候可能就还得对这些调用加if else判断
	来判断身份从而控制访问权限,什么电影能看,什么不能看,很麻烦,不灵活*/
	VideoSite* p1 = new FixBugVideoSite();
	p1->freeMovie();
	p1->vipMovie();
	p1->ticketMovie();
	
	/*2.
	通过代理,不同身份的用户可以对不同类型的电影具有不同的访问权限
	*/

	//第五步,客户直接访问代理对象 相当于客户只能访问助理,不能直接访问老板
	//游客
	unique_ptr<VideoSite> p2(new FreeVideoSiteProxy());
	watchMovice(p2);
	//VIP
	unique_ptr<VideoSite> p3(new VipVideoSiteProxy());
	watchMovice(p3);
	return 0;
}

类和接口的说明:

  1. VideoSite :这是一个抽象基类,定义了三个纯虚函数freeMovievipMovieticketMovie,分别代表观看免费电影、VIP电影和用券观看电影的功能。这个类作为所有视频站点(包括代理和委托)的接口。
  2. FixBugVideoSite :这是VideoSite的一个具体实现,即委托类。它实现了所有三个虚函数,分别输出相应的观看信息。这个类代表了一个实际的视频站点,提供了观看电影的具体功能。
  3. FreeVideoSiteProxyVipVideoSiteProxy :这两个类都是VideoSite的代理类。它们各自持有一个指向VideoSite(实际上是FixBugVideoSite)的指针,用于在需要时调用委托类的功能。代理类通过重写虚函数来控制对委托类功能的访问,例如,普通用户(FreeVideoSiteProxy)不能观看VIP电影或用券观看电影,而VIP用户(VipVideoSiteProxy)则可以观看VIP电影,但仍然不能用券观看(在这个例子中,VIP用户是否能用券观看取决于代理类的实现,这里简单地限制了)。

委托类和代理类的虚函数都是一样的,都是抽象类里面的函数

代理类经过检查发现不合法,没有权限,就不会调用委托类对象

8.装饰器模式

装饰器模式:主要是增加现有类的功能

为了增强现有类的功能,通过实现子类的方式,重写接口,是可以完成功能扩展的,但是代码中有太多的子类添加进来了

**装饰器模式(Decorator Pattern)**是一种结构型设计模式,它允许你向一个现有的对象添加新的功能,同时又不改变其结构。装饰器模式通过创建一个包装对象(即装饰器)来包裹原始对象,从而可以在运行时动态地给对象添加职责。

优点
  1. 灵活性:可以在不修改原有类的情况下增加新的功能。
  2. 扩展性:通过组合而非继承来扩展功能,避免了继承带来的高耦合和代码膨胀问题。
  3. 复用性:装饰器和具体组件可以独立变化,互不干扰。
缺点
  1. 装饰链复杂:如果装饰链太长,调试和维护会变得复杂。
  2. 性能:因为每次调用都会通过多个装饰器,可能会有一定的性能开销。
c++ 复制代码
#include<iostream>
#include<thread>
#include<list>
#include<mutex>
#include<atomic>
#include<queue>
#include<condition_variable>
#include<memory>
using namespace std;

class Car
{
public:
	virtual void show() = 0;
};

class Bmw :public Car
{
public:
	void show()
	{
		cout << "这是一辆宝马汽车,配置有:基类配置";
	}
};

class Audi :public Car
{
public:
	void show()
	{
		cout << "这是一辆奥迪汽车,配置有:基类配置";
	}
};

class Benz :public Car
{
public:
	void show()
	{
		cout << "这是一辆奔驰汽车,配置有:基类配置";
	}
};

//装饰器的基类 装饰器可以让增加的功能互相组合
class CarDecorator :public Car
{
public:
	CarDecorator(Car* p) :pCar(p) {}

private:
	Car* pCar;
};

//装饰器1 定速巡航
class ConcreteDecorator01 :public Car
{
public:
	ConcreteDecorator01(Car *p):pCar(p){}
	void show()
	{
		pCar->show();
		cout << ",定速巡航";
	}
private:
	Car* pCar;
};

//装饰器2 定速巡航
class ConcreteDecorator02 :public Car
{
public:
	ConcreteDecorator02(Car* p) :pCar(p) {}
	void show()
	{
		pCar->show();
		cout << ",自动刹车";
	}
private:
	Car* pCar;
};

//装饰器3 车道偏离
class ConcreteDecorator03 :public Car
{
public:
	ConcreteDecorator03(Car* p) :pCar(p) {}
	void show()
	{
		pCar->show();
		cout << ",车道偏离";
	}
private:
	Car* pCar;
};

int main()
{
	Car* p1 = new ConcreteDecorator01(new Bmw());
	//功能组合
	p1 = new ConcreteDecorator02(p1);
	p1 = new ConcreteDecorator03(p1);
	p1->show();
	cout << endl;

	Car* p2 = new ConcreteDecorator02(new Audi());
	p2->show();
	cout << endl;

	Car* p3 = new ConcreteDecorator03(new Benz());
	p3->show();
	cout << endl;

	return 0;
}

9.代理和装饰的区别

C++中的装饰器模式(Decorator Pattern)和代理模式(Proxy Pattern)都是结构型设计模式,但它们在目的、功能扩展方式、结构修改以及关注点等方面存在显著的区别。

一、目的
  • 装饰器模式:主要用于动态地为对象添加额外的职责,而不改变其结构。它允许在不改变现有对象代码的情况下,通过创建一系列的装饰器类来增加、扩展或修改对象的功能。
  • 代理模式:主要用于控制对其他对象的访问。它在客户端和实际对象之间引入了一个代理对象,客户端通过代理对象访问实际对象。代理对象可以用于控制访问权限、延迟加载、远程访问等。
二、功能扩展方式
  • 装饰器模式:通过组合多个装饰器类来实现功能扩展。每个装饰器类都实现了与被装饰对象相同的接口,并可以在调用接口方法之前或之后添加额外的行为。
  • 代理模式:主要通过代理对象来控制访问,实际功能一般是由被代理对象提供的。代理对象可以在访问实际对象之前或之后添加额外的逻辑,如权限检查、日志记录等。
三、结构修改
  • 装饰器模式:通常不改变对象的结构,只是在其上添加装饰器。装饰器与被装饰对象具有相同的接口,因此可以替换或组合使用。
  • 代理模式:虽然也引入了新的代理对象,但代理对象通常包含了额外的逻辑,这些逻辑在访问实际对象之前或之后执行。此外,代理模式可能会改变客户端与实际对象之间的交互方式。
四、关注点
  • 装饰器模式:关注于对象的功能增强。它允许在不修改现有代码的情况下,动态地为对象添加新的行为或功能。
  • 代理模式:关注于对象的访问控制和管理。它提供了对实际对象访问的间接层,以便在访问过程中添加额外的逻辑或控制。
五、应用场景
  • 装饰器模式
    • 组件扩展:在大型项目中,随着业务的增加,需要添加新的功能时,装饰器可以避免修改原有的基础组件。
    • API增强:当提供API给第三方调用时,装饰器可以用于添加额外的功能,如日志记录、安全校验等。
    • 权限管理:装饰器可以用来控制对原有特定接口的访问权限。
    • 缓存机制:在网络请求或数据库查询等操作中,装饰器可以用来添加额外的缓存、重试、超时处理等功能。
  • 代理模式
    • 延迟加载:可以在需要时才创建实际对象,节省资源。
    • 远程代理:用于控制对远程对象的访问,通常用于网络编程中。
    • 保护代理:用于控制对对象的访问权限,增强安全性。
    • 缓存/缓冲代理:用于缓存频繁访问的数据,以减少计算或网络请求的开销。
    • 智能引用代理:用于管理对象的生命周期,确保对象在不再需要时被正确释放。

10.适配器模式

适配器模式:让不兼容的接口可以在一起工作

**适配器模式(Adapter Pattern)**是一种结构型设计模式,它允许接口不兼容的类一起工作。适配器模式将类的接口转换成客户端所期望的另一种接口形式,使得原本不兼容的类可以合作无间。

优点
  1. 提高灵活性:通过适配器,客户端可以透明地访问不兼容的接口,提高了系统的灵活性。
  2. 复用性:适配器使得已有的类可以被复用,而无需修改它们的源代码。
  3. 解耦:适配器模式有助于将接口和实现解耦,使得系统更加模块化。
缺点
  1. 代码复杂度增加:引入适配器会增加系统的代码量和复杂度。
  2. 性能损耗:在某些情况下,适配器可能会导致性能上的损耗,因为它需要在客户端和适配对象之间进行额外的转换。
c++ 复制代码
#include<iostream>
#include<thread>
#include<list>
#include<mutex>
#include<atomic>
#include<queue>
#include<condition_variable>
#include<memory>
using namespace std;

//VGA接口的电脑 TV投影仪也是VGA接口
class VGA
{
public:
	virtual void play() = 0;
};

//TV01表示支持VGA接口的投影仪
class TV01 :public VGA
{
public:
	void play()
	{
		cout << "通过VGA接口连接投影仪,进行视频播放" << endl;
	}
};

//实现一个电脑类,只支持VGA接口
class Computer
{
public:
	//由于电脑只支持VGA接口,所以该方法的参数也只能支持VGA接口的指针和引用
	void playVideo(VGA* pVGA)
	{
		pVGA->play();
	}
};

//进了一批新的投影仪,都只支持HDMT接口,根本都插不到电脑上
class HDMI
{
public:
	virtual void play() = 0;
};

class TV02 :public HDMI
{
public:
	void play()
	{
		cout << "通过HDMI接口连接投影仪,进行视频播放" << endl;
	}
};

//由于电脑(VGA接口)和投影仪(HDMI接口)无法直接相连,所以需要添加适配器类
class VGAToHDMTAdapter :public VGA
{
public:
	VGAToHDMTAdapter(HDMI *p):pHdmi(p){}
	//该方法相当于就是转换头,做不同接口的信号转换的
	void play()
	{
		pHdmi->play();
	}
private:
	HDMI* pHdmi;
};


int main()
{
	Computer computer;
	//电脑本身就支持VGA,通过VGA投影到投影仪上
	computer.playVideo(new TV01());

	/*TV02只支持HDMI,不支持AGV
	computer.playVideo(new TV02());
	表现为VGA*不接受一个TV02指针类型的参数

	方法1:换一个支持HDMI接口的电脑,这个就叫代码重构
	方法2:买一个转换头(适配器),能够把VGA信号转成HDMI信号,这是添加适配器类*/

	//通过转换头,可以通过HDMI接口投影仪播放视频
	computer.playVideo(new VGAToHDMTAdapter(new TV02()));

	return 0;
}

下面是对代码中各个部分的详细讲解:

  1. 抽象接口定义
  • VGAHDMI 是两个抽象基类,分别定义了具有 play() 方法的接口。这两个接口代表两种不同的视频输出标准。
  1. 具体实现类
  • TV01 继承自 VGA ,表示一个支持VGA接口的投影仪,其 play() 方法实现了通过VGA接口播放视频的功能。
  • TV02 继承自 HDMI ,表示一个支持HDMI接口的投影仪,其 play() 方法实现了通过HDMI接口播放视频的功能。
  1. 电脑类
  • Computer 类有一个方法 playVideo(VGA* pVGA),这个方法接受一个 VGA 接口的指针作为参数,并调用该指针的 play() 方法。这表示电脑只能通过VGA接口播放视频。
  1. 适配器类
  • VGAToHDMTAdapter 类继承自 VGA ,但它内部持有一个 HDMI 接口的指针。这个适配器类实现了 VGA 接口的 play() 方法,但在这个方法内部,它调用的是内部 HDMI 接口指针的 play() 方法。这样,VGAToHDMTAdapter 就起到了将HDMI接口转换为VGA接口的作用。

11.观察者模式

也称为监听者模式发布-订阅模式

它属于行为型模式 ,而行为型主要关注的是对象之间的通信

观察者模式主要关注的是对象的一对多的关系,也就是多个对象都依赖一个对象,当该对象的状态发生改变时,其他对象都能接收到相应的通知

观察者模式(Observer Pattern)是一种行为设计模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。

优点
  1. 松耦合:观察者和被观察者之间通过抽象接口进行交互,降低了它们之间的耦合度。
  2. 灵活性:观察者可以在任何时候增加或删除,而不会影响被观察者的行为。
  3. 扩展性强:可以在不修改被观察者代码的情况下增加新的观察者。
缺点
  1. 性能开销:如果被观察者状态频繁变化,并且有很多观察者,那么通知所有观察者可能会带来较大的性能开销。
  2. 内存泄漏风险:如果没有正确管理观察者的生命周期,可能会导致内存泄漏。
  3. 循环依赖:观察者之间可能相互依赖,导致复杂的依赖关系网。

例如:

一组数据(数据对象),通过这一组数据生成 曲线图(对象1)/ 柱状图(对象2)/ 圆饼图(对象3)

当数据对象改变时,对象1,2,3应该及时收到相应的通知

Subject主题有更改的时候,应该及时通知相应的观察者,去处理相应的事件

c++ 复制代码
#include<iostream>
#include<thread>
#include<list>
#include<mutex>
#include<atomic>
#include<queue>
#include<condition_variable>
#include<memory>
#include<unordered_map>
#include<list>
using namespace std;

//观察者抽象类
class Observer
{
public:
	virtual void handle(int msgid) = 0;
};

//第一个观察者实例
class Observer1 :public Observer
{
public:
	void handle(int msgid)
	{
		switch (msgid)
		{
		case 1:
			cout << "Observer1 recv 1 msg" << endl;
			break;
		case 2:
			cout << "Observer1 recv 2 msg" << endl;
			break;
		default:
			cout << "Observer1 recv unkown msg" << endl;
			break;
		}
	}
};

//第二个观察者实例
class Observer2 :public Observer
{
public:
	void handle(int msgid)
	{
		switch (msgid)
		{
		case 2:
			cout << "Observer2 recv 2 msg" << endl;
			break;
		default:
			cout << "Observer2 recv unkown msg" << endl;
			break;
		}
	}
};

//第三个观察者实例
class Observer3 :public Observer
{
public:
	void handle(int msgid)
	{
		switch (msgid)
		{
		case 1:
			cout << "Observer3 recv 1 msg" << endl;
			break;
		case 3:
			cout << "Observer3 recv 3 msg" << endl;
			break;
		default:
			cout << "Observer3 recv unkown msg" << endl;
			break;
		}
	}
};

//主题类
class Subject
{
public:
	//给主题增加观察者对象
	void adObserver(Observer* obser, int msgid)
	{
		_subMap[msgid].push_back(obser);
	}
	//主题检测发生改变,通知相应的观察者对象处理事件
	void dispatch(int msgid)
	{
		auto it = _subMap.find(msgid);
		//没找着说明没人对这件事情感兴趣
		if (it != _subMap.end())
		{
			for (Observer* pObser : it->second)
			{
				pObser->handle(msgid);
			}
		}
	}
private:
	unordered_map<int, list<Observer*>> _subMap;
};


int main()
{
	Subject subject;
	Observer* p1 = new Observer1();
	Observer* p2 = new Observer2();
	Observer* p3 = new Observer3();

	subject.adObserver(p1, 1);
	subject.adObserver(p1, 2);
	subject.adObserver(p2, 2);
	subject.adObserver(p3, 1);
	subject.adObserver(p3, 3);

	int msgid = 0;
	for (;;)
	{
		cout << "输入消息id:" ;
		cin >> msgid;
		if (msgid == -1)
			break;
		subject.dispatch(msgid);
	}
	return 0;
}

当主题改变的时候,对消息关注的对象会收到通知

相关推荐
Red Red1 小时前
网安基础知识|IDS入侵检测系统|IPS入侵防御系统|堡垒机|VPN|EDR|CC防御|云安全-VDC/VPC|安全服务
网络·笔记·学习·安全·web安全
贰十六2 小时前
笔记:Centos Nginx Jdk Mysql OpenOffce KkFile Minio安装部署
笔记·nginx·centos
知兀2 小时前
Java的方法、基本和引用数据类型
java·笔记·黑马程序员
Ysjt | 深2 小时前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++
ephemerals__2 小时前
【c++丨STL】list模拟实现(附源码)
开发语言·c++·list
Microsoft Word3 小时前
c++基础语法
开发语言·c++·算法
Natural_yz3 小时前
大数据学习17之Spark-Core
大数据·学习·spark
qq_172805593 小时前
RUST学习教程-安装教程
开发语言·学习·rust·安装
一只小小汤圆3 小时前
opencascade源码学习之BRepOffsetAPI包 -BRepOffsetAPI_DraftAngle
c++·学习·opencascade