C++ 设计模式

发布于:2023-01-01 ⋅ 阅读:(409) ⋅ 点赞:(0)

一、面向对象设计原则

1.单一职责原则:

一个类,只有一个引起它变化的原因。应该只有一个职责,每一个职责都是变化的一个轴线,如果一个类有一个以上的职责,这些职责就耦合在了一起。这会导致脆弱的设计。当一个职责发生变化时,可能会影响其它的职责。另外,多个职责耦合在一起,会影响复用性。例如:要实现逻辑和界面的分离。

2.开闭原则:

开闭原则是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,需要使用接口和抽象类。

3.里氏代换原则:

里氏代换原则(LSP)面向对象设计的基本原则之一。里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

4.依赖倒转原则:

依赖倒置原则(Dependence Inversion Principle)是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

5.接口隔离原则:

接口隔离原则是说使用多个隔离的接口比使用单个接口要好,降低类之间耦合度。客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。

6.合成复用原则:

合成复用原则指在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分。新对象通过委派调用已有对象的方法达到复用其已有功能的目的。简单来说:尽量使用组合/聚合关系,少用继承。

7.迪米特法则:

又称为最少知道原则,一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。也就是说一个软件实体应当尽可能少与其他实体发生相互作用。这样,当一个模块修改时,就会尽量少的影响其他的模块,扩展会相对容易,这是对软件实体之间通信的限制,他要求限制软件实体之间通信的宽度和深度。

二、创建型模式

1.单例模式

只有一个对象,不能再额外创建一个新的对象

实现思路:

STEP1:将构造函数访问权限设置成private 或者protected ,不允许类的外部创建对象
STEP2∶ 用一个静态成员指针指向创建的对象
STEP3:提供一个静态成员函数,获取这个对象的首地址
应用场景:

1.应用程序的日志应用,一般都可以用单例模式实现,只能有一个实例去操作文件
⒉读取配置文件,读取的配置项都是公有的,只能有一个实例去操作文件
3.数据连接池、多线程的线程池

(1)   饿汉式

在单例类定义的时候就进行的实例化
 

#include<iostream>
 
using namespace std;

class Hungry 
{
public:
	static Hungry *getInstance()
	{
		return hs;
	}

	static void releaseInstance()
	{
		if(hs != NULL)
		{
			delete hs;
			hs = NULL;
		}
	}

	void show()
	{
		cout << "123456" << endl;
	}

private:
	Hungry()
	{
		cout << "创建了一个单例" << endl;
	}

	~Hungry()
	{
		cout << "释放了一个单例" << endl;
	}

	static Hungry *hs;
};

Hungry *Hungry::hs = new Hungry;


int main(int argc, const char *argv[])
{
	//Hungry *hs = Hungry::getInstance();
	//hs->show();
	
	Hungry::getInstance()->show();
	Hungry::releaseInstance();

	return 0;
}

优点:线程安全

缺点:还没有使用的时候,就已经创建了对象,浪费了内存空间,Release后无法再使用对象(解决方案:不实现释放方法)

(2)  懒汉式

在第一次用到类实例的时候才会去实例化

#include<iostream>
 
using namespace std;

class Lazy 
{
public:
	static Lazy *getInstance()
	{
		if(ls != NULL)
		{
			return ls;
		}
		pthread_mutex_lock(&lock);
		if(ls == NULL)
		{
			ls = new Lazy;
		}
		pthread_mutex_unlock(&lock);
		return ls;
	}

	static void releaseInstance()
	{
		if(ls != NULL)
		{
			delete ls;
			ls = NULL;
		}
	}

	void show()
	{
		cout << "123456" << endl;
	}

private:
	Lazy()
	{
		cout << "创建了一个单例" << endl;
	}

	~Lazy()
	{
		cout << "释放了一个单例" << endl;
	}

	static Lazy *ls;
	static pthread_mutex_t lock;
};

Lazy *Lazy::ls = NULL;
pthread_mutex_t Lazy::lock = PTHREAD_MUTEX_INITIALIZER;


int main(int argc, const char *argv[])
{
	//Lazy *ls = Lazy::getInstance();
	//ls->show();
	
	Lazy::getInstance()->show();
	Lazy::releaseInstance();
	Lazy::getInstance()->show();
	Lazy::releaseInstance();

	return 0;
}

优点:在使用的时候,才会分配内存创建对象

缺点:在多线程中使用,会出现创建多个对象的问题,线程不安全

内存释放问题:当需要释放时添加一个内部类,利用静态变量对象释放时,会调用析构函数

线程安全解决方案:使用互斥锁

 

 2.工厂模式

(1) 简单工厂模式

简单工厂模式不能算作是一种设计模式,更像一种编程习惯相当于定义一个非常简单的类主要负责产生不同的产品对象
实现思路:

1:设计一个简单工厂类,提供一个静态方法,可以根据客户端指定的产品名称,返回具体的产品
2:设计一个产品抽象类,用来规范每个产品具备的属性与方法
3:实现具体的产品类

示例:

抽象产品类:

具体产品A:

具体产品B:

 简单工厂类:

 实现方式:

 缺点:将全部创建逻辑集中到了一个工厂类中,不具有通用性,如果需要添加啼的类,则就需要改变工厂头,添加新的产品时,需要修改SimpleFactory:productxxxx方法。

(2) 工厂方法模式

相对于简单工厂,工厂方法模式将单一的工厂类分成了多个具体的小工厂,并抽象出一个工厂接口,这个接口只负责定义创建的方式。

实现思路:

1:设计一个抽象工厂类,规范一个创建产品的接口
2:设计不同的工厂来创建不同的产品

产品类与简单工厂一致

抽象类工厂:

 具体工厂A:

具体工厂B:

 

使用方法:

 优点:

当某个具体产品需要修改时,只需要修改具体工厂和产品就可以了,此时不会影响其他产品
当有一个新产品需要添加进来的时候,只需要添加一个工厂和产品就可以了,不需要修改以前写的代码。

缺点:

当添加新产品时,需要编写新的具体产品类,而且还需要提供与之对应的具体工厂类
由此会导致系统类的个数将成对增加,在一定程度上增加系统的复杂度,有更多的类需要编译与运行,会给系统带来一些额外的开销。

(3) 抽象工厂模式

在工厂方法模式中,用户每次使用都只能创建一个同类型的对象,工厂方法就满足不了需求了,可以将多个工厂方法组合到一个类,专门用于创建多个产品,或者叫做创建产品家族。

实现思路:

抽象工厂:子类必须实现其工厂方法创建产品家族
具体工厂:实现抽象工厂接口,负责实现工厂方法,一个具体工厂可以创建一组产品
抽象产品:产品家族的父类,由此可以衍生很多子产品
具体产品:衍生自抽象产品,由工厂方法直接创建

优点:

允许客户使用抽象的接口创建一组相关产品,不需要产出的具体产品是什么,客户就可以从具体的产品中解耦出来
一个具体工厂可以创建多个产品,与工厂方法模式相比,可以少产生具体工厂的类数量

缺点:

抽象工厂是使用组合的方式将工厂方法集合到一个类中,当新增一个产品家族就要修改类及其下面的具体工厂类,所以它的扩展性比较差
每新增一个产品子类都要创建一个类,当产品子类过多时会产生很多类,导致系统复杂性增加

三、结构型模式

1.适配器模式

将一个类的接口转换成客户期望的另外一个接口,使得原本不兼容的类可以一起工作

(1)  对象模式

将需要适配的对象进行包装,然后提供适配后的接口

实现思路:

1:用一个类记录下客户需要的接口
2:用适配器类包裹适配者对象,然后继承客户需求即可的类
3:实现客户需求的接口,在这些接口中使用适配者对象

(2)  类模式

适配器类继承需求和适配者,然后重新实现需求接口,在需求接口中调用适配者提供的接口

实现思路:

1:用一个类记录下客户需求的接口
2:使用适配器类继承需求接口和适配者类,调用适配者类的函数接口实现需求接口

示例:

#include<iostream>

using namespace std;

class PowerTarget //用户需要的接口,抽象类
{
public:
	virtual ~PowerTarget(){;}
	virtual int getPower110V() = 0;
};

class Outlet
{
public:
	int getPower220V()
	{
		return 220;
	}
};

class Adapter : public PowerTarget //适配器类,对象模式
{
public:
	Adapter(Outlet *outlet) : outlet(outlet)
	{

	}

	~Adapter()
	{
		if(outlet != NULL)
		{
			delete outlet;
		}
	}

	int getPower110V()
	{
		return outlet->getPower220V()/2;
	}
private:
	Outlet *outlet;
};

class Adapter2 : public PowerTarget,private Outlet //适配器类,类模式
{
public:
	int getPower110V()
	{
		return getPower220V()/2;
	}
};

int main(int argc, const char *argv[])
{	
	PowerTarget *a = new Adapter2;
	cout << "输出电压:" << a->getPower110V() <<endl;
	delete a;
/*
	PowerTarget *a = new Adapter(new Outlet);
	cout << "输出电压:" << a->getPower110V() <<endl;
	delete a;
*/
	return 0;
}

优点:可以让任何两个没有关联的类一起运行,提高了类的复用
缺点:过度使用适配器,会让系统杂乱无章,不利整体进行把握

2.代理模式

代理模式为其他对象提供一种代理以控制这个对象的访问,主要是当客户不想或者不能直接引用另外一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

使用场景:

虚拟代理:通过一个消耗资源较少的对象来代表一个消耗资源较多的对象,可以在一定程度上节省系统的运行开销。
缓冲代理:为某一个操作的结果提供临时的缓冲存储空间,以便在后续使用中能够共享这些结果,优化系统性能,缩短执行时间。
保护代理:可以控制对一个对象的访问权限,为不同用户提供不同级别的使用权限。
远程代理:为位于两个不同地址空间对象的访问提供了一种实现机制,可以将一些消耗资源较多的对象和操作移植到更好的计算机上,提高系统的整体运行效率。

实现思路:

抽象角色:声明真实对象和代理对象的共同接口
代理角色:代理对象角色内部包含有对真实对象的引用,从而可以操作真实对象,同时代理对象提供与真实对象相同的接口以便在任何时刻都能代理真实对象,代理对象可以在执行真实对象操作时,附加其他的操作,相当于对真实对象进行封装。

优点:

1.代理模式能够将调用用于隔离真实访问的对象,在一定程度上降低了系统耦合度
⒉.代理对象在客户端和目标对象之间起到一个中介的作用,可以起到对目标对象的保护

缺点:

1.由于在客户端和真实主题之间增加了一个代理对象,所以会造成请求的处理速度变慢
⒉.实现代理类也需要额外的工作,从而增加了系统系统的实现复杂度

示例:

#include<iostream>

using namespace std;

class Hourse //原接口抽象类
{
public:
	virtual ~Hourse() {;}
	virtual void getHourse() = 0;//出租房子
};

class Hourser : public Hourse //房东
{
public:
	void getHourse()
	{
		cout << "出租房子" << endl;
	}
};

class Proxy : public Hourse //房产中介
{
public:
	Proxy(Hourse *hourse = NULL) :hourse(hourse)
	{

	}

	~Proxy()
	{
		if(hourse != NULL)
		{
			delete hourse;
		}
	}

	void getHourse()
	{
		cout << "验证房源,验证客户,签约合同" << endl;
		hourse->getHourse();
	}
private:
	Hourse *hourse;
};

int main(int argc, const char *argv[])
{
	Proxy *p =new Proxy(new Hourser);
	p->getHourse();
	delete p;
	return 0;
}

(3) 组合模式

将对象组合成树形结构用于表示整体与部分的层次结构

实现思路:

Component(抽象构建):

为叶子构件和容器构件对象定义接口
可以包含所有子类共有行为的声明和实现
声明了访问及管理子构建的接口

Leaf(叶子构件):

叶子节点没有子节点
实现了Compoent 中定义的行为
可以通过异常方式进行处理C

omposite(容器构件):

容器节点包含子节点(可以是叶子构件,也可以是容器构件)
它提供了一个集合用于存储子节点
实现了Component中定义的行为,包括访问及管理子构建的接口
在业务方法中可以递归调用其子节点的业务方法

四、行为型模式

1.策略模式

策略模式的作用就是将具体的算法从业务逻辑中剥离出来,成为一系列独立算法类,使得他们可以相互替换。

实现思路:

策略接口角色(Strategy)
具体策略实现角色(ConcreteStrategY
策略上下文角色StrategyContext

优点:

各自使用封装的算法,可以很容易地引入新的算法来满足相同的接口
算法的细节完全封装在Strategy类中,可以在不影响Context类的情况下更改算法的实现
由于实现的是同一个接口,所以策略之间可以自由切换

缺点:

用户必须知道所有的策略,了解它们之间的区别,以便适时选择恰当的算法
策略模式将造成产生很多策略类,可以通过使用享元模式在一定程序上减少对象的数量

2.观察者模式

观察者定义一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,主题对象在状态发生变化时会通知所有观察者对象,是它们能够自动更新自己,这种交互也成为发布-订阅,目标是通知的发布者,它在发出通知时并不需要知道谁是它的观察者,可以在任意数目的观察者订阅并接收通知。

Subject(目标主题):

1.目标知道它的观察者,可以有任意多个观察者观察同一个目标
⒉提供注册和删除观察者对象的接口
Observer(观察者):

为那些在目标发生改变时需要获得通知的对象定义一个更新接口
ConcreteSubject(具体目标):

1.将有关状态存入各ConcreteSubject对象
2.当它的状态发生改变时,向它的各个观察者发出通知
ConcreteObserver(具体观察者):

1.维护─个指向具体目标对象的一个引用
2.存储有关状态,这些状态应与目标的状态保持─致
3.实现Observer的更新接口以使自身状态与目标的状态保持一致

优点:

1.具体主题和具体观察者是松耦合关系
⒉观察者模式满足开-闭原则

缺点:

1.如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间
2.如果在观察者和观察目标之间有循环依赖,观察目标会触发它们之间进行循环调用,可能导致系统崩溃
3.观察者模式没有相应的机制让观察者知道锁观察的目标对象时怎么发生变化的,而仅仅只是知道观察目标发生了变化
 

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到