[A]. 总结2.组件协作:模版方法模式,策略模式,观察者模式

声明:本栏目的 [A] 系列的学习笔记,学习对象为 B 站授课视频 C++设计模式(李建忠),参考教材为《设计模式:可复用面向对象软件的基础》()。本栏目 [A] 系列文章中的图件和笔记,部份来自上述资源。

GOF设计模式–23种模式的分类

从目的来看:

  • 创建型
  • 结构型
  • 行为型

从范围来看:

  • 类模式处理类与子类的静态关系
  • 对象模式处理对象间的动态关系

从封装变化角度对模式分类!!!:

  • 组件协作
    现代软件专业分工之后的第一个结果是“框架与应用程序的划分”,“组件协作”模式通过晚期绑定,来实现框架与应用程序之间的松耦合,是二者之间协作时常用的模式。
    典型模式
    • Template Method
    • Observer / Event
    • Strategy
  • 单一职责
  • 对象创建
  • 对象性能
  • 接口隔离
  • 状态变化
  • 数据结构
  • 行为变化
  • 领域问题
重构获得模式 Refactoring to patterns
  • 好的设计模式:应对变化,提高复用。这里的复用不是代码块的复用,而是编译单元的复用,是二进制层面的复用。
  • 设计模式的选择,要点在于 “寻找变化点,然后在变化点处应用设计模式,从而更好的应对需求的变化”。
  • 没有一步到位的设计模式,不宜上来就使用设计模式。敏捷开发倡导 “重构获得模式”。

重构关键技法:

  • 静态 -> 动态
  • 早绑定 -> 晚绑定
  • 继承 -> 组合
  • 编译时依赖 -> 运行时依赖
  • 紧耦合 -> 松耦合
模版方法模式 (Template Method)
  • 动机:在软件构建过程中,对于某一项任务,它常常有稳定的整体操作结构,但各个子步骤却有很多改变的需求,或者由于固有的原因(比如框架与应用之间的关系)而无法和任务的整体结构同时实现。
  • 如何在确定稳定操作结构的前提下,来灵活应对各个子步骤的变化或者晚期实现需求?
    在这里插入图片描述
    图1. 应用场景举例
  • 定义一个操作中的算法的骨架 (稳定),而将一些步骤延迟(变化)到子类中。Template Method使得子类可以不改变(复用)一个算法的结构即可重定义(override 重写)该算法的某些特定步骤。——《设计模式》GoF
    对于图1中的需求场景举例:

(1)对于图1中的需求,一般做法是:

 lib开发人员
class Library{
public:
	void Step1(){
		//...
	}

    void Step3(){
		//...
    }

    void Step5(){
		//...
    }
};
 app 开发人员
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();

}

(2)对于图1中的需求,模版方法模式的做法是:

 lib开发人员
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() { //�ȶ�
		//.....
	}
// 将稳定的处理流程 run()中的不稳定的 step2和step4 交给派生类去实现。
	virtual bool Step2() = 0;//�仯
    virtual void Step4() =0; //�仯
};
  • 显然 以上如果step2和step4发生改动,模版方法模式中的代码改动的要比 一般方法的代码改动少,而且模版方法中将一般方法中放在 main 里的固定的流程 框架代码写在了 Library 里,稳定的东西就应该复用。
策略模式 (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); //��̬����
        //...
    }
};
  • 以上基类 TaxStrategy 有多个派生类,每个国家的计算税款方式对应一个派生类,SalesOrder类中的成员变量为TaxStrategy 指针,通过构造函数将指定国家派生类赋值给这个基类指针,从而调用指定国家的计算税款方法。新增国家 只需添加一个派生类即可。对修改封闭,对拓展开发。
观察者模式(observer / Event)
  • 动机:在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系” ——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。
  • 使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。
  • 定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。——《设计模式》GoF
  • 先看一个场景举例,对大文件进行文件分割,有展示分割进度的模块需要实时从文件分割模块获取文件分割的进度。
  • 使用设计模式前的做法是:
    MainForm Button1_Click 中,获取大文件的文件路径, 以及要分割成的小文件数量。然后将这两个信息 传给负责文件分割的类 FileSplitter, FileSplitter类中会调用展示分割进程的类ProgressBar(进度条展示)。
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 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);
		}
	}
};
  • 以上代码违背了依赖倒置原则: (1)高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象(稳定) 。(2)抽象(稳定)不应该依赖于实现细节(变化) ,实现细节应该依赖于抽象(稳定)。简单来说就是底层模块应该尽可能通用,容易产生修改的模块应该放在上层。
  • 以上 FileSpiltter 中的成员变量 ProgressBar 是个容易变化的实现细节(展示进度的方式有进度条,百分比等等),所以这里是 稳定的高层模块依赖了易变化的实现细节。
  • 观察者模式的做法:
    (1) 将抽象通知机制,IProgress,将具体的通知控件 ProgessBar 变成了 抽象的 通知机制。具体的通知控件 比如以下的 ProgressBar 或者 ConsoleNotifier 都可以继承 IProgress。那么在 FileSpiltter 中 的成员变量 List<IProgress*> m_iprogressList 就可以放入不同的通知控件子类,
/// 注意,C++ 虽然支持多继承,但是不推荐使用多继承,一方面容易违背单一职责的原则,另一方面也容易带来很多问题。
/// 但是 支持一种多继承的形式,一个父类是is关系,即MainForm是一种Form, 其他的父类都是接口类(工具)。
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); // notice
		splitter.addIProgress(&cn); //  notice
		splitter.split();
		splitter.removeIProgress(this);
	}
	virtual void DoProgress(float value){
		progressBar->setValue(value);
	}
};

class ConsoleNotifier : public IProgress {  // 
public:
	virtual void DoProgress(float value){
		cout << ".";
	}
};
///
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++;
		}
	}
};
  • 观察者模式,A 类,依赖于容易变化的 B 类,不要直接将 B 作为 A 的成员变量,而是增加一个 B 抽象基类 C, C 是稳定的抽象类。将 C 类指针作为 A 类的 成员变量,A传入不同的 功能 B 类都可以赋给C类指针,那么A 里面的代码就是稳定的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值