C++设计模式--Strategy 策略模式 和 Observer 观察者模式

“组件协作”模式:
现代软件专业分工之后的第一个结果是“框架与应用程序的划分”,“组件协作”模式通过晚期绑定,来实现框架与应用程序之间的松耦合,是二者之间协作时常用的模式。
典型模式
• Template Method
• Strategy
• Observer / Event

1. Strategy 策略模式

动机( Motivation)
在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使对象变得异常复杂;而且有时候支持不使用的算法也是一个性能负担(有的算法代码几乎不用,却要装到缓存中)。
如何在运行时根据需要透明地更改对象的算法?将算法与对象本身解耦,从而避免上述问题?

代码示例
税的计算,有许多国家。刚开始支持CN,US,DE,后来由于业务需求变化,需要添加另一些国家,例如法国FR

第一种做法,利用枚举类型实现。如果要添加法国的税法计算方法,则需要改变代码,在源代码中添加新的代码。违背了开放封闭原则(对扩展开放,对更改封闭),类模块应该尽可能以扩展的方式应对未来的变化,而不是找到源代码并修改源代码这种方式应对变化。改源代码,需要重现编译,测试,部署,代价高。

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){  //更改
			//...
		}

        //....
     }
    
};

第二种方式。创建一个TaxStrategy基类,各国的作为子类。在SalesOrder类中,使用工厂模式为多态指针创建具体的子类对象,具体的创建由StrategyFactory决定。
当要添加法国的税法计算时,直接创建一个法国的子类,这是扩展的方式。SalesOrder类中不需要改变,得到了复用性。遵循了开放封闭原则。
设计模式领域的复用指的是编译单位,二进制层面的复用性,而不是代码的复用。真正的复用指的是编译,测试,部署之后,不再改变。

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); //多态调用
        //...
    }
    
};

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

SalesOrder稳定,基类TaxStrategy不变,子类税法计算可以变化。

作用
将算法的责任和本身进行解耦,使得:

  1. 算法可独立于使用外部而变化
  2. 客户端方便根据外部条件选择不同策略来解决不同问题
    策略模式仅仅封装算法(包括添加 & 删除),但策略模式并不决定在何时使用何种算法,算法的选择由客户端来决定

C++设计模式--Strategy 策略模式 和 Observer 观察者模式_第1张图片
要点总结
Strategy及其子类为组件提供了一系列可重用的算法,从而可以使得类型在运行时方便地根据需要在各个算法之间进行切换。
Strategy模式提供了用条件判断语句以外的另一种选择,消除条件判断语句,就是在解耦合。含有许多条件判断语句的代码通常都需要Strategy模式。看到if else 就要去想想是否使用Strategy模式。(如果条件判断语句是绝对不变的,例如根据性别判断,只有两种情况,所以不会用到Strategy模式)
如果Strategy对象没有实例变量,那么各个上下文可以共享同一个Strategy对象,从而节省对象开销。

优点

  1. 策略类之间可以自由切换
    由于策略类都实现同一个接口,所以使它们之间可以自由切换。
  2. 易于扩展
    增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“
  3. 避免使用多重条件选择语句(if else),充分体现面向对象设计思想。

缺点

  1. 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
  2. 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。

应用场景
动态选择多种复杂行为

2. Observer 观察者模式

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

动机( Motivation)
在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系” ——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。
使用面向对象技术,可以将这种依赖 关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。

代码示例
做一个文件分割器。MainForm是一个窗口界面。有一个用户需求,提供文件分割的进度展示,所以添加了ProgressBar。

class FileSplitter
{
	string m_filePath; 
	int m_fileNumber; 
	
	//*******添加进度条*******//
	ProgressBar* m_progressBar; //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();

	}
};

第一种方式。违背了依赖倒置原则。A依赖B,就是说A编译的时候B需要存在,才能编译通过。
ProgressBar是一种实现细节,非常容易改变,可能是进度条,后续可能又变成其他形式。所以,导致了抽象依赖于实现细节,违背了依赖倒置原则。

class IProgress{ 
public:
	virtual void DoProgress(float value)=0;
	virtual ~IProgress(){}
};

class FileSplitter
{
	string m_filePath; 
	int m_fileNumber; 
	
	Iprogress* iprogress;  // 抽象的通知机制

public:
    // 构造函数
	FileSplitter(const string& filePath, int fileNumber, Iprogress* iprogress) :
		m_filePath(filePath), 
		m_fileNumber(fileNumber),
		m_iprogress(iprogress){

	}

	void split(){

		//1.读取大文件

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

	}
};
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());

		FileSplitter splitter(filePath, number, this);

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

	}
};

第二种方式。在第一种方式中,ProgressBar是一种具体的通知控件,导致抽象依赖于实现细节。为了解决这一问题,在第二种方式中,使用一种抽象的通知机制代替。FileSplitter类不依赖具体的界面通知类。
C++中支持多继承,但不推荐使用,会带来很多复杂的耦合性问题;但推荐一种:一个是主的继承类,其他都是借口(或抽象基类)

第三种方式。支持多个通知,即多个多个观察者。

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:
	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 << ".";
	}
};

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

C++设计模式--Strategy 策略模式 和 Observer 观察者模式_第2张图片

在观察者模式中有如下角色:
Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。
ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
Observer:抽象观察者,是观察者者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。
ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。

Observer相当于IProgress,Update()相当于DoProgress,Attach相当于addIProgress,Detach相当于removeIProgress,Notify相当于onProgress,ConcreteSubject相当于FileSplitter,ConcreteObserver相当于MainForm 和 ConsoleNotifier ,为具体的观察者

要点总结
使用面向对象的抽象, Observer模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达致松耦合。
目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播。
观察者自己决定是否需要订阅通知,目标对象对此一无所知。
Observer模式是基于事件的UI框架中非常常用的设计模式,也是MVC模式的一个重要组成部分。

使用场景
关联行为场景,需要注意的是,关联行为是可拆分的,而不是“组合”关系。
事件多级触发场景。
跨系统的消息交换场景,如消息队列、事件总线的处理机制。

优点
解除耦合,让耦合的双方都依赖于抽象,从而使得各自的变换都不会影响另一边的变换。

缺点
在应用观察者模式时需要考虑一下开发效率和运行效率的问题,程序中包括一个被观察者、多个观察者,开发、调试等内容会比较复杂,而且在Java中消息的通知一般是顺序执行,那么一个观察者卡顿,会影响整体的执行效率,在这种情况下,一般会采用异步实现。

你可能感兴趣的:(笔记,C++,设计模式)