C++设计模式

设计模式

1、设计模式简介

  • 什么是设计模式?

“每一个模式描述了一个在我们周围不断重复发生的问题, 以及该问题的解决方案的核心。这样,你就能一次又一次 地使用该方案而不必做重复劳动”。

  • 深入理解面向对象

向下:深入理解三大面向对象机制

​ • 封装,隐藏内部实现

​ • 继承,复用现有代码

​ • 多态,改写对象行为

向上:深刻把握面向对象机制所带来的抽象意义,理解如何使用 这些机制来表达现实世界,掌握什么是“好的面向对象设计”

  • 如何解决复杂性?

更高层次来讲,人们处理复杂性有一个通用的技术,即抽象。 由于不能掌握全部的复杂对象,我们选择忽视它的非本质细节, 而去处理泛化和理想化了的对象模型。

软件设计的金科玉律:复用!

2、面向对象设计原则

  • 依赖倒置原则(DIP Dependence Inversion Principle) (编译时依赖)

    高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象(稳定) 。

    抽象(稳定)不应该依赖于实现细节(变化) ,实现细节应该依赖于抽象(稳定)。

  • 开放封闭原则(OCP Open Close Principle)

    对扩展开放,对更改封闭。 尽可能不要动源代码

    类模块应该是可扩展的,但是不可修改。

  • 单一职责原则(SRP Single Responsibility Principle)

    一个类应该仅有一个引起它变化的原因。

    变化的方向隐含着类的责任。

  • Liskov 替换原则(LSP Liskov Substitution Principle)

    子类必须能够替换它们的基类(IS-A)。

    继承表达类型抽象。

  • 接口隔离原则(ISP Interface Segregation Principle)

    不应该强迫客户程序依赖它们不用的方法。

    接口应该小而完备。

  • 优先使用对象组合,而不是类继承 (CRP Composite Reuse Principle)

    类继承通常为“白箱复用”,对象组合通常为“黑箱复用”。

    继承在某种程度上破坏了封装性,子类父类耦合度高。 • 而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。

  • 封装变化点

    使用封装来创建对象之间的分界层,让设计者可以在分界层的 一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。

  • 针对接口编程,而不是针对实现编程

    不将变量类型声明为某个特定的具体类,而是声明为某个接口。

    客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。

    减少系统中各部分的依赖关系,从而实现“高内聚、松耦合” 的类型设计方案。

接口标准化很重要!

设计模式设计原则

  • 单⼀职责原则:就⼀个类⽽⾔,应该仅有⼀个引起它变化的原因。
  • 开放封闭原则:软件实体可以扩展,但是不可修改。即⾯对需求,对程序的改动可以通过增加代码来完成,但是不 能改动现有的代码。
  • 里氏代换原则:⼀个软件实体如果使⽤的是⼀个基类,那么⼀定适⽤于其派⽣类。即在软件中,把基类替换成派⽣ 类,程序的⾏为没有变化。
  • 依赖倒转原则:抽象不应该依赖细节,细节应该依赖抽象。即针对接⼝编程,不要对实现编程。
  • 迪⽶特原则:如果两个类不直接通信,那么这两个类就不应当发⽣直接的相互作⽤。如果⼀个类需要调⽤另⼀个类 的某个⽅法的话,可以通过第三个类转发这个调⽤。
  • 接⼝隔离原则:每个接⼝中不存在派⽣类⽤不到却必须实现的⽅法,如果不然,就要将接⼝拆分,使⽤多个隔离的 接⼝。

三、Template Method模式

  • 设计模式分类:

    创建型(Creational)模式:将对象的部分创建工作延迟到子 类或者其他对象,从而应对需求变化为对象创建时具体类型实 现引来的冲击。

    结构型(Structural)模式:通过类继承或者对象组合获得更灵 活的结构,从而应对需求变化为对象的结构带来的冲击。

    行为型(Behavioral)模式:通过类继承或者对象组合来划分 类与对象间的职责,从而应对需求变化为多个交互的对象带来 的冲击。

  • 重构获得模式 Refactoring to Patterns

    面向对象设计模式是“好的面向对象设计”,所谓“好的面向对象设计”指是那些可以满足 “应对变化,提高复用”的设计 。

    现代软件设计的特征是“需求的频繁变化”。设计模式的要点是 “寻找变化点,然后在变化点处应用设计模式,从而来更好地应对 需求的变化”.“什么时候、什么地点应用设计模式”比“理解设 计模式结构本身”更为重要。

    设计模式的应用不宜先入为主,一上来就使用设计模式是对设计 模式的最大误用。没有一步到位的设计模式。敏捷软件开发实践提 倡的“Refactoring to Patterns”是目前普遍公认的最好的使用设 计模式的方法。

  • 重构关键方法

静态 -> 动态

早绑定 -> 晚绑定

继承 -> 组合

编译时依赖 -> 运行时依赖

紧耦合 -> 松耦合

  • “组件协作”模式:

现代软件专业分工之后的第一个结果是“框架与应用程序的划分”,“组件协作”模式通过晚期绑定(一个早的东西调用一个晚的东西),来实现框架与应用程序之间的松耦合,是二者之间协作时常用的模式。

典型模式

​ •Template Method

​ •Observer / Event

​ • Strategy

  • 动机

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

在确定稳定操作结构的前提下,来灵活应对各个子步骤的变化或者晚期实现需求。

  • 模式定义

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

注意:

  • 稳定(相对)的骨架 是这个设计模式的前提,必须要有稳定点。

  • 设计模式是在稳定点和变化点之间隔离开,找到平衡点

  • 稳定架构中有变化 稳定需要写成non-virtual函数 变化的需要写成virtual函数

  • 延迟或者变化操作需要基类添加虚函数让子类去实现 或者override。

总结:

  • Template Method模式是一种非常基础性的设计模式,在面向对象系统中有着大量的应用。它用最简洁的机制(虚函数的多态性) 为很多应用程序框架提供了灵活的扩展点,是代码复用方面的基本实现结构。

  • 除了可以灵活应对子步骤的变化外,“不要调用我,让我来调用 你”的反向控制结构是Template Method的典型应用。

  • 在具体实现方面,被Template Method调用的虚方法可以具有实现,也可以没有任何实现(抽象方法、纯虚方法),但一般推荐将它们设置为protected方法。

代码对比

//程序库开发人员
class Library{

public:
	void Step1(){
		//...
	}

    void Step3(){
		//...
    }

    void Step5(){
		//...
    }
};

//应用程序开发人员
class Application{
public:
	bool Step2(){
		//...
    }

    void Step4(){
		//...
    }
};

int main()
{
	Library lib();
	Application app();

	lib.Step1();

	if (app.Step2()){
		lib.Step3();
	}

	for (int i = 0; i < 4; i++){
		app.Step4();
	}

	lib.Step5();

}

使用Template Method模式后

//程序库开发人员
class Library{
public:
	//稳定 template method
    void Run(){
        
        Step1();

        if (Step2()) { //支持变化 ==> 虚函数的多态调用
            Step3(); 
        }

        for (int i = 0; i < 4; i++){
            Step4(); //支持变化 ==> 虚函数的多态调用
        }

        Step5();

    }
	virtual ~Library(){ }

protected:
	
	void Step1() { //稳定
        //.....
    }
	void Step3() {//稳定
        //.....
    }
	void Step5() { //稳定
		//.....
	}

	virtual bool Step2() = 0;//变化
    virtual void Step4() =0; //变化
};

//应用程序开发人员   继承
class Application : public Library {
protected:
	virtual bool Step2(){
		//... 子类重写实现
    }

    virtual void Step4() {
		//... 子类重写实现
    }
};




int main()
	{
    // 多态
	    Library* pLib=new Application();
	    lib->Run();

		delete pLib;
	}
}

四、Strategy策略模式

  • 动机

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

如何在运行时根据需要透明地更改对象的算法?将算法与对象本 身解耦,从而避免上述问题?

  • 模式定义

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

总结:

  • Strategy及其子类为组件提供了一系列可重用的算法,从而可以使得类型在运行时方便地根据需要在各个算法之间进行切换。

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

  • 注意:

    代码中不推荐使用if else switch case
    if else绝对不变情况 可以不使用策略模式 例如: 一周七天
    实际业务变化不太可能一成不变的 大多数情况遇到if...else...可使用策略模式

  • 如果Strategy对象没有实例变量,那么各个上下文可以共享同一个 Strategy对象,从而节省对象开销。

代码对比:

// 枚举
enum TaxBase {
	CN_Tax,
	US_Tax,
	DE_Tax,
	FR_Tax       //更改
};

class SalesOrder{
    TaxBase tax;
public:
    double CalculateTax(){
        //...
        
        if (tax == CN_Tax){
            //CN***********
        }
        else if (tax == US_Tax){
            //US***********
        }
        else if (tax == DE_Tax){
            //DE***********
        }
		else if (tax == FR_Tax){  //更改
			//...
		}

        //....
     }
    
};

使用策略模式后:

class TaxStrategy{
public:
    virtual double Calculate(const Context& context)=0;
    // 作为父类一定要写虚析构函数
    virtual ~TaxStrategy(){}
};


class CNTax : public TaxStrategy{
public:
    virtual double Calculate(const Context& context){
        //***********
    }
};

class USTax : public TaxStrategy{
public:
    virtual double Calculate(const Context& context){
        //***********
    }
};

class DETax : public TaxStrategy{
public:
    virtual double Calculate(const Context& context){
        //***********
    }
};



//扩展
//*********************************
class FRTax : public TaxStrategy{
public:
	virtual double Calculate(const Context& context){
		//.........
	}
};

// 这里的代码不用改动
class SalesOrder{
private:
    //抽象类不能创建对象,创建指针才有多态性,创建对象没有多态性。
    TaxStrategy* strategy;

public:
    SalesOrder(StrategyFactory* strategyFactory){
        this->strategy = strategyFactory->NewStrategy();
    }
    ~SalesOrder(){
        delete this->strategy;
    }

    public double CalculateTax(){
        //...
        Context context();
        
        double val = 
            strategy->Calculate(context); //多态调用
        //...
    }
    
};

五、Observer(Event) 观察者模式

  • 动机

    在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系” ——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密, 将使软件不能很好地抵御变化。

    使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。

  • 模式定义

    定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都 得到通知并自动更新。

  • 要点总结

    使用面向对象的抽象,Observer模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达致松耦合。

    目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播。

    观察者自己决定是否需要订阅通知,目标对象对此一无所知。

    Observer模式是基于事件的UI框架中非常常用的设计模式,也是 MVC模式的一个重要组成部分。

注意:

接口就是抽象基类

不推荐使用多继承 可以同时继承基类和接口

代码对比

class FileSplitter
{
	string m_filePath;
	int m_fileNumber;
	ProgressBar* m_progressBar;

public:
	FileSplitter(const string& filePath, int fileNumber, ProgressBar* progressBar) :
		m_filePath(filePath), 
		m_fileNumber(fileNumber),
		m_progressBar(progressBar){

	}

	void split(){

		//1.读取大文件

		//2.分批次向小文件中写入
		for (int i = 0; i < m_fileNumber; i++){
			//...
			float progressValue = m_fileNumber;
			progressValue = (i + 1) / progressValue;
			m_progressBar->setValue(progressValue);
		}

	}
};
class MainForm : public Form
{
	TextBox* txtFilePath;
	TextBox* txtFileNumber;
	ProgressBar* progressBar;

public:
	void Button1_Click(){

		string filePath = txtFilePath->getText();
		int number = atoi(txtFileNumber->getText().c_str());

		FileSplitter splitter(filePath, number, progressBar);

		splitter.split();

	}
};

使用观察者模式后:

// 接口  抽象基类
class IProgress{
public:
	virtual void DoProgress(float value)=0;
	virtual ~IProgress(){}
};


class FileSplitter
{
	string m_filePath;
	int m_fileNumber;

	List<IProgress*>  m_iprogressList; // 抽象通知机制,支持多个观察者
	
public:
     // 这里没有发生改变  不管添加多少观察者  构造函数不必发生变化
	FileSplitter(const string& filePath, int fileNumber) :
		m_filePath(filePath), 
		m_fileNumber(fileNumber){

	}


	void split(){

		//1.读取大文件

		//2.分批次向小文件中写入
		for (int i = 0; i < m_fileNumber; i++){
			//...

			float progressValue = m_fileNumber;
			progressValue = (i + 1) / progressValue;
			onProgress(progressValue);//发送通知
		}

	}


	void addIProgress(IProgress* iprogress){
		m_iprogressList.push_back(iprogress);
	}

	void removeIProgress(IProgress* iprogress){
		m_iprogressList.remove(iprogress);
	}


protected:
    // 虚函数 可以允许子类override
	virtual void onProgress(float value){
		
		List<IProgress*>::iterator itor=m_iprogressList.begin();

		while (itor != m_iprogressList.end() )
			(*itor)->DoProgress(value); //更新进度条
			itor++;
		}
	}
};
// 继承父类和接口
class MainForm : public Form, public IProgress
{
	TextBox* txtFilePath;
	TextBox* txtFileNumber;

	ProgressBar* progressBar;

public:
	void Button1_Click(){

		string filePath = txtFilePath->getText();
		int number = atoi(txtFileNumber->getText().c_str());

		ConsoleNotifier cn;

		FileSplitter splitter(filePath, number);

		splitter.addIProgress(this); //订阅通知
		splitter.addIProgress(&cn)//订阅通知

		splitter.split();

		splitter.removeIProgress(this);

	}

	virtual void DoProgress(float value){
		progressBar->setValue(value);
	}
};

class ConsoleNotifier : public IProgress {
public:
	virtual void DoProgress(float value){
		cout << ".";
	}
};

六、单例模式

单例模式:保证⼀个类仅有⼀个实例,并提供⼀个访问它的全局访问点。

有两种模式懒汉和饿汉:

饿汉:饿了就饥不择⻝了,所以在单例类定义的时候就进⾏实例化。

懒汉:顾名思义,不到万不得已就不会去实例化类,也就是在第⼀次⽤到的类实例的时候才会去实例化。

饿汉模式(线程安全)

在最开始的时候静态对象就已经创建完成,设计⽅法是类中包含⼀个静态成员指针,该指针指向该类的⼀个对象, 提供⼀个公有的静态成员⽅法,返回该对象指针,为了使得对象唯⼀,构造函数设为私有。

#include <iostream>
#include <algorithm>
using namespace std;
class SingleInstance {
public:
 	static SingleInstance* GetInstance() {
 	static SingleInstance ins;
 	return &ins;
 }
 	~SingleInstance(){};
private:
 //涉及到创建对象的函数都设置为private
 	SingleInstance() { std::cout<<"SingleInstance() 饿汉"<<std::endl; }
 	SingleInstance(const SingleInstance& other) {};
 	SingleInstance& operator=(const SingleInstance& other) {return *this;}
};
int main(){
 //因为不能创建对象所以通过静态成员函数的⽅法返回静态成员变量
	 SingleInstance* ins = SingleInstance::GetInstance();
	 return 0;
}
//输出 SingleInstance() 饿汉

懒汉模式(线程安全需要加锁)

尽可能的晚的创建这个对象的实例,即在单例类第⼀次被引⽤的时候就将自己初始化,C++ 很多地⽅都有类型的思想,⽐如写时拷⻉,晚绑定等

#include <pthread.h>
#include <iostream>
#include <algorithm>
using namespace std;
class SingleInstance {
public:
 	static SingleInstance* GetInstance() {
 		if (ins == nullptr) {
 			pthread_mutex_lock(&mutex);
 			if (ins == nullptr) {
 				ins = new SingleInstance();
			 }
			 pthread_mutex_unlock(&mutex);
 		}
 		return ins;
 	}
 ~SingleInstance(){};
 //互斥锁
 static pthread_mutex_t mutex;
private:
 //涉及到创建对象的函数都设置为private
 	SingleInstance() { std::cout<<"SingleInstance() 懒汉"<<std::endl; }
 	SingleInstance(const SingleInstance& other) {};
 	SingleInstance& operator=(const SingleInstance& other) { return *this; }
 //静态成员
 	static SingleInstance* ins;
};
//懒汉式 静态变量需要定义
SingleInstance* SingleInstance::ins = nullptr;
pthread_mutex_t SingleInstance::mutex;
int main(){
 //因为不能创建对象所以通过静态成员函数的⽅法返回静态成员变量
 	SingleInstance* ins = SingleInstance::GetInstance();
	delete ins;
 	return 0;
}
//输出 SingleInstance() 懒汉

单例模式的适⽤场景

(1)系统只需要⼀个实例对象,或者考虑到资源消耗的太⼤⽽只允许创建⼀个对象。

(2)客户调⽤类的单个实例只允许使⽤⼀个公共访问点,除了该访问点之外不允许通过其它⽅式访问该实例(就 是共有的静态⽅法)。

  • 3
    点赞
  • 14
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:游动-白 设计师:我叫白小胖 返回首页
评论

打赏作者

Lucky dog123

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值