C++设计模式

  • Post author:
  • Post category:其他




设计模式



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



版权声明:本文为qq_49175257原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。