组件协作模式(模板方法、策略模式、观察者模式)

模板方法属于“组件协作”模式,其实就是通过晚绑定的方式来实现框架与应用程序之间的松耦合。属于组件协作模式的还有:策略模式(Strategy)、事件模式(Observer/Event)

模板方法(Template Method)

模板方法应用的背景

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

比如有一项任务需要执行5步,其中1、3、5步是稳定的,是不会变更的,而2、4步是变化的。如果采用模板方法去实现,那么应该将主流程的运行过程放在一个类里,由固定的代码实现,1、2、3、4、5步的函数定义,也放在这个类里,但其中2和4设置成虚函数,由继承该类的子类实现,如此,使用此类来执行任务,只需实现虚函数2和4的具体内容。

错误样例:
原先Library开发人员(框架构建者)需要实现:
– 开发1、3、5三个步骤

Application开发人员需要实现:
– 开发2、4两个步骤
– 自行调用12345,实现程序主流程

正确样例:

原先Library开发人员(框架构建者)需要实现:
– 开发1、3、5三个步骤
– 实现程序主流程

Application开发人员需要实现:
– 开发2、4两个步骤

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

策略模式(Strategy)

在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使对象变得异常复杂,而且有时候支持不适用的算法也是一个性能负担。(换句话说就是,if-else罗列一大堆,而且场景将来还会扩展,if else还会增多

比如,现在需要根据不同的国家的税法去计算一个税的金额,如果通过if-else的方法,判断不同国家执行对应代码,可能要依次判断好多行,并随着业务扩展,新国家税法加入,导致if-else会不断增加。
那么这个时候就需要考虑策略模式,构造一个税法的基类,定义虚函数计算税法,构造多个不同国家的税法子类继承基类。再构造一个新类,控制流程执行,新类里有税法基类的指针,采用多态的方式执行不同国家的税法。如此,便可在有新的国家业务出现的时候,只需新增税法子类,其他代码继续复用。

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

观察者模式(Observer/Event)

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

错误示范:

// 需求:点一下按键,执行文件的切割,分成多个小份,但需要更新打包进度条

class FileSplitter{
    string m_filePath;
    int m_fileNumber;
    ProgressBar *m_progressBar; // m_progressBar是实现细节,是具体的通知控件,FileSplitter对它产生了编译时依赖

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

    void split(){
        // ...
        for(int i = 0; i < m_fileNumber; ++i){
            // ...分成小份写入
            float curr = i+1;
            m_progressBar->setValue( curr / m_fileNumber ); // 更新
        }
    }
}

class MainForm : public Form{
    TextBox *txtFilePath;
    TextBox *txtFileNumber;

public:
    void Button01_Click(){
        string filePath = txtFilePath->getText(); // 假设点击即可获取的参数
        int number = atoi(txtFileNumber->getText().c_str());

        FileSplitter splitter(filePath, number);
        splitter.split(); // 执行文件的切割打包
    }
}

上面这个示范,违背了第一大原则:依赖倒置原则,即高层模块不能依赖低层模块,二者都应该依赖抽象,抽象不能依赖实现细节。

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

class FileSplitter{
    string m_filePath;
    int m_fileNumber;
    IProgress *m_iprogress; // 抽象通知机制

public:
    FileSplitter(const string &filePath, int fileNumber, IProgress *iprogress):
        m_filePath(filePath), 
        m_fileNumber(fileNumber),
        m_iprogress(iprogress) {
    }

    void split(){
        // ...
        for(int i = 0; i < m_fileNumber; ++i){
            // ...分成小份写入
            float curr = i+1;
            m_iprogress->DoProgress( curr / m_fileNumber ); // 更新
        }
    }
}

class MainForm : public Form, public IProgress{ // 多继承时,一般最好只能一个主的继承类,其他都是接口或抽象基类
    TextBox *txtFilePath;
    TextBox *txtFileNumber;
    ProgressBar *progressBar;

public:
    void Button01_Click(){
        string filePath = txtFilePath->getText(); // 假设点击即可获取的参数
        int number = atoi(txtFileNumber->getText().c_str());

        FileSplitter splitter(filePath, number, this);
        splitter.split(); // 执行文件的切割打包
    }

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

至此,FileSplitter就没有耦合一个界面类MainForm。当然想要实现接受多个通知,可以将FileSplitter里的m_iprogress用容器装起来,封装add和remove函数。

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

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注