C++设计模式 之 “组件协作”模式:Template Method、Strategy、Observer
“组件协作”模式:
#现代软件专业分工之后的第一个结果是“框架与应用程序的划分”,“组件协作”模式通过晚期绑定,来实现框架与应用程序之间的松耦合,是二者之间协作时常用的模式。
#典型模式: Template Method、 Strategy、 Observer / Event
part 1 Template Method 模版模式
动机(Motivation)
#在软件构建过程中,对于某一项任务,它常常有稳定的整体操作结构,但各个子步骤却有很多改变的需求,或者由于固有的原因(比如框架与应用之间的关系)而无法和任务的整体结构同时实现。
#如何在确定稳定操作结构的前提下,来灵活应对各个子步骤的变化或者晚期实现需求?
结构化软件设计流程
面向对象软件设计流程
早绑定与晚绑定
晚绑定机制的实现方法:虚函数、函数指针(C中最常用,C++虚函数的底层原理也是函数指针)。
(模版方法的)模式定义
定义一个操作中的算法的骨架 (稳定),而将一些步骤延迟(变化)到子类中。Template Method使得子类可以不改变(复用)一个算法的结构即可重定义(override 重写)该算法的某些特定步骤。——《设计模式》GoF
结构(UML图)
代码实现
稳定的代码写成非虚函数;变化的代码写成虚函数。
Library(库)代码是相对稳定的,对照于上图的 AbrastractClss。代码示例如下:
class Library {
public:
void run() { step1(); step2(); step3(); }
~Library() {};
protected:
void step1() { cout << "step1" << endl; }; // 稳定的需求
void step3() { cout << "step3" << endl; }; // 稳定的需求
virtual void step2() = 0; // 变化的需求
};
其中,step2() 函数是一些不稳定的需求,Library 不能预判出将来会出现的需求,所以交由子类去实现——定义为纯虚函数。
Application 是根据新的需求,所编写的应对新变化的代码。代码示例如下:
class Application : public Library{
protected:
virtual void step2() { cout << "Application::step2" << endl; }; // 根据不同需求,子类重写之
};
其中,step2()函数是根据新的需求,把对策写在纯虚函数里,交由程序在运行时执行。
客户端程序的调用如下:
int main() {
Library* pLib = new Application();
pLib->run();
delete pLib;
return 0;
}
Q&A
Q1:为什么基类的虚构方法是虚函数?
A1:因为子类以指针的方式实现(Father* p = new Son() )构造和调用。所以当子类析构时,子类的析构函数才会被调用——父类的虚析构函数被子类的虚构函数重载(override)了。反之——如果父类的析构函数不定义为虚函数——以 Father* 型别实现的指针,在析构它的对象时,只会调用它本身的——Father类——虚构函数,这样会造成子类的内存泄漏。
Q2:为什么Library类库的实现细节——step1()、step3()函数——定义为 protected ?
A2: 因为这两个方法——step1()、step3()函数——对外界没什么关系,主要是与本类和子类有关系。
要点总结
#Template Method模式是一种非常基础性的设计模式,在面向对象系统中有着大量的应用。它用最简洁的机制(虚函数的多态性)为很多应用程序框架提供了灵活的扩展点,是代码复用方面的基本实现结构。
#除了可以灵活应对子步骤的变化外,“不要调用我,让我来调用你”的反向控制结构是Template Method的典型应用。
#在具体实现方面,被Template Method调用的虚方法可以具有实现,也可以没有任何实现(抽象方法、纯虚方法),但一般推荐将它们设置为protected方法。
#绝大多数软件框架中都有 Template Method 模式。
#设计模式必须基于一个稳定点,如果全不稳定就不存在任何适用的设计模式了;反之,如果全稳定就不需要设计模式了。
#Template Method 意为“样板”。run()方法就是样板,细节定义为纯虚函数,交由子类(用户)去定义。
part 2 策略模式 Strategy
动机(Motivation)
#在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使对象变得异常复杂;而且有时候支持不使用的算法也是一个性能负担。
#如何在运行时根据需要透明地更改对象的算法?将算法与对象本身解耦,从而避免上述问题?
模式定义
定义一系列算法,把它们一个个封装起来,并且使它们可互相替换(变化)。该模式使得算法可独立于使用它的客户程序(稳定)而变化(扩展,子类化)。——《设计模式》GoF
UML
代码实现
要实现一个计算税率的类。由于中国、美国以及将来可能会出现的各国之间的税率算法是不一样的,所以要采用一种设计模式来抵御变化(业务需求的变化)。从外层开始,一个类一个类的交代,直到内层核心的类。
1. Context类,用于存储计算税率时要用到的“上下文”。
struct Context {
// 为了简化代码,暂不定义计算税率时的上下文
};
2. TaxStrategy类,为实现具体税率算法的上层抽象。定义的纯虚函数目的是实现运行时多态。
struct TaxStrategy {
virtual double calculate(const Context& c) = 0;
virtual ~TaxStrategy(); // 抽象基类的析构方法一定要定义为虚函数
};
3. ChinaTax类,具体税率算法的实现类。
struct ChinaTax : public TaxStrategy{
virtual double calculate(const Context& c) {
return 1.0;
}
};
4. StrategyFactory 利用工厂方法模式实习了具体抽象类的动态创建。比如,可以生成ChinaTax类、America类对象等。
struct StrategyFactory{
TaxStrategy* newStrategyFactory() {}
};
5. SalesOrder类,属于客户端的调用——根据我对《大话设计模式》的理解。
struct SalesOrder {
TaxStrategy *strategy;
SalesOrder(StrategyFactory& sf){
this->strategy = sf.newStrategyFactory();
}
~SalesOrder() { delete this->strategy; };
double calculateTax() {
Context c;
double val = strategy->calculate(c); // 多态调用
}
};
变化
假设有一天需求增加了,需要增加对美国(America)的业务,所以税率算法需要增加对美国的支持。有了策略模式,只需让新增的类继承抽象基类 TaxStrategy ,重写虚函数 calculate() 即可!
// 拓展美国业务
// 客户需求的变化
struct AmericaTax : public TaxStrategy {
virtual double calculate(const Context& c) {
return 2.0;
}
};
要点总结
#所谓设计模式和面向对象所讲的“复用”:编译单位、二进制层面的复用。当代码写完、编译、测试、部署之后原封不动,对需求变化比较稳定叫作复用;源代码级别的复制粘贴不叫复用。
#Strategy及其子类为组件提供了一系列可重用的算法,从而可以使得类型在运行时方便地根据需要在各个算法之间进行切换。
#Strategy模式提供了用条件判断语句以外的另一种选择,消除条件判断语句,就是在解耦合。含有许多条件判断语句的代码通常都需要Strategy模式。
#如果Strategy对象没有实例变量,那么各个上下文可以共享同一个Strategy对象,从而节省对象开销。
part 3 观察者模式 Observer
动机(Motivation)
在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系”——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。
使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。
应用
解决了原代码破坏依赖倒置原则(DIP)——一般情况下,依赖关系是编译时依赖。
接口(Interface)在C++中就是抽象基类。
观察者模式在 Java 中称为 Listener ; C#中称为 Event。
代码展示一 : from 《大话设计模式》
1. 抽象的、稳定的类型:Subject、Observer。代码如下:
class Observer { public: virtual void update() = 0; }; class Subject { public: void attach(Observer* observer) { _observers.push_back(observer); } void detach(Observer* observer) { _observers.remove(observer); } void notify() { for (auto &e : _observers) e->update(); } private: list<Observer*> _observers; };
2. 具体的、变化的类型:ConcreteSubject、ConcreteObserver。代码如下:
class ConcreteSubject : public Subject { public: void setState(string state) { _state = state; } string getState() { return _state; } private: string _state; }; class ConcreteObserver : public Observer { public: ConcreteObserver(ConcreteSubject* subject) { _subject = subject; } virtual void update() { _state = _subject->getState(); cout << _state << endl; } private: string _state; ConcreteSubject* _subject; };
3. 用户调用范例,代码如下:
int main() { ConcreteSubject* s = new ConcreteSubject(); s->attach(new ConcreteObserver(s)); s->setState("online!"); s->notify(); return 0; }
代码展示二 : from 《GoF 23》
1. 稳定的、抽象的“上层”类:Subject、Observer。代码如下:
struct Subject; // 声明类型,不然 Observer 调用时会报错 struct Observer { virtual void update(Subject* sub) = 0; virtual ~Observer() {}; }; struct Subject { list<Observer*> _observers; void attach(Observer* observer) { _observers.push_back(observer); } void detach(Observer* observer) { _observers.remove(observer); } void notify() { for (auto &e : _observers) e->update(this); } virtual ~Subject() {}; };
2.变化的、具体的类:ConcreteSubject、ConcreteObserver。代码如下:
struct ConcreteSubject : public Subject { string _subjectState; string getState() { return _subjectState; } }; struct ConcreteObserver : public Observer { string _observerState; ConcreteSubject* _subject; ConcreteObserver(ConcreteSubject* concreteSubject) { _subject = concreteSubject; _subject->attach(this); } virtual void update(Subject* sub) { _observerState = _subject->getState(); cout << _observerState << endl; } virtual ~ConcreteObserver() { _subject->detach(this); } };
3. 用户调用该模式代码范例:
int main() { ConcreteSubject* boss = new ConcreteSubject; ConcreteObserver* employee = new ConcreteObserver(boss); boss->_subjectState = "come back!"; boss->notify(); return 0; }
模式定义
定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。——《设计模式》GoF
UML
要点总结
#使用面向对象的抽象,Observer模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达致松耦合。
#目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播。
#观察者自己决定是否需要订阅通知,目标对象对此一无所知。
#Observer模式是基于事件的UI框架中非常常用的设计模式,也是MVC模式的一个重要组成部分。