情景分析
公司接到一个XX模型公司的的订单 , 需要生产10万悍马的车模 , 只做基本的实现 , 不靠虑扩展性 . 那么就开始埋头干 .
先按照最一般的经验设计类图 , 如图所示 :
非常简单的实现 , 悍马车有两个型号 , H1和H2 . 按照需求 , 只需要悍马模型 , 那就给你悍马模型 . 先写一个抽象类 , 然后两个不同型号的模型实现类 , 通过简单的继承就可以实现业务要求 . 我们先从抽象类开始编写 .
//抽象悍马模型
class HummerModel
{
public:
/*
*首先 , 这个模型要能够发动起来 , 别管是手摇发动 , 还是店里发动 , 反正
*是要能够发动起来 , 那这个实现要在实现类里
*/
virtual void start() {}
//能发动 , 还要能停下来 , 那才是真本事
virtual void stop() {}
//按喇叭会出声音 , 是滴滴叫 , 还是哔哔叫
virtual void alarm() {}
//引擎会轰隆隆地响 , 不响那是假的
virtual void engineBoom() {}
//那模型应该会跑吧 , 别管是人推的 , 还是电力驱动的 , 总之会跑
virtual void run() {}
在抽象类中 , 我们定义了悍马模型都必须具有的特质 : 能够发动 , 停止 , 喇叭回响 , 引擎可以轰鸣 . 但是每个型号的悍马实现是不同的 . H1型号的悍马如下 :
//H1型号悍马模型
class HummerH1Model : public HummerModel
{
protected:
void start() { cout<<"悍马H1发动..."<<endl; }
void stop() { cout<<"悍马H1停车..."<<endl; }
void alarm() { cout<<"悍马H1鸣笛..."<<endl; }
void engineBoom() { cout<<"悍马H1引擎声音是这样的..."<<endl; }
public:
void run()
{
//发动汽车
start();
//引擎开始轰鸣
engineBoom();
//然后就开始跑了 , 跑的过程中遇到一条狗当道 , 就按喇叭
alarm();
//到达目的地就停车
stop();
}
};
注意看 , run()方法, 这是一个汇总的方法 , 一个模型生产成功了 , 总要拿给客户检测的 , 怎么检测 ,让它跑起来 . 通过run()方法 , 把模型的所有功能都测试到了 .
H2型悍马的代码 , 如下 :
//H2型号悍马模型
class HummerH2Model : public HummerModel
{
protected:
void start() { cout<<"悍马H2发动..."<<endl; }
void stop() { cout<<"悍马H2停车..."<<endl; }
void alarm() { cout<<"悍马H2鸣笛..."<<endl; }
void engineBoom() { cout<<"悍马H2引擎声音是这样的..."<<endl; }
public:
void run()
{
//发动汽车
start();
//引擎开始轰鸣
engineBoom();
//然后就开始跑了 , 跑的过程中遇到一条狗当道 , 就按喇叭
alarm();
//到达目的地就停车
stop();
}
};
好了 , 程序编写到这里 , 已经发现问题了 , 两个实现类的run()方法都是完全相同的 , 那这个run()方法的实现应该出现在抽象类 , 不应该出现在实现类上 , 抽象是所有子类的共性封装 .
注意 : 在软件开发当中 , 如果相同的一段代码复制过两次 , 就需要对设计产生怀疑 , 架构师要明确地说明为什么相同的逻辑要出现两次或更多次 .
修改之后的类图 :
模板方法的定义
定义 : 定义一个操作中算法的框架,而将一些步骤延迟到子类中 . 使得子类可以不改变算法的结构即可重定义该算法中的某些特定步骤 .
UML类图 :
代码如下 :
//模板方法模式
#if 0
//抽象悍马模型
class HummerModel
{
protected:
/*
*首先 , 这个模型要能够发动起来 , 别管是手摇发动 , 还是店里发动 , 反正
*是要能够发动起来 , 那这个实现要在实现类里
*/
virtual void start() {}
//能发动 , 还要能停下来 , 那才是真本事
virtual void stop() {}
//按喇叭会出声音 , 是滴滴叫 , 还是哔哔叫
virtual void alarm() {}
//引擎会轰隆隆地响 , 不响那是假的
virtual void engineBoom() {}
public:
void run()
{
//发动汽车
start();
//引擎开始轰鸣
engineBoom();
//然后就开始跑了 , 跑的过程中遇到一条狗当道 , 就按喇叭
alarm();
//到达目的地就停车
stop();
}
};
//H1型号悍马模型
class HummerH1Model : public HummerModel
{
protected:
void start() { cout<<"悍马H1发动..."<<endl; }
void stop() { cout<<"悍马H1停车..."<<endl; }
void alarm() { cout<<"悍马H1鸣笛..."<<endl; }
void engineBoom() { cout<<"悍马H1引擎声音是这样的..."<<endl; }
};
//H2型号悍马模型
class HummerH2Model : public HummerModel
{
protected:
void start() { cout<<"悍马H2发动..."<<endl; }
void stop() { cout<<"悍马H2停车..."<<endl; }
void alarm() { cout<<"悍马H2鸣笛..."<<endl; }
void engineBoom() { cout<<"悍马H2引擎声音是这样的..."<<endl; }
};
int main()
{
HummerModel* _hummer;
//XXX公司要H1型号的悍马
_hummer = new HummerH1Model();
_hummer->run();
delete _hummer;
_hummer = nullptr;
cout<<"===================="<<endl;
//XXX公司要H2型号的悍马
_hummer = new HummerH2Model();
_hummer->run();
delete _hummer;
_hummer = nullptr;
return 0;
}
#endif
运行结果 :
模板方法模式的应用
模版方法模式的结构
- 抽象方法 : 父类中只声明但不加以实现,而是定义好规范,然后由它的子类去实现 .
- 模版方法 : 由抽象类声明并加以实现 . 一般来说,模版方法调用抽象方法来完成主要的逻辑功能,并且,模版方法大多会定义为final类型(Java中),指明主要的逻辑功能在子类中不能被重写 .
- 钩子方法 : 由抽象类声明并加以实现 . 但是子类可以去扩展,子类可以通过扩展钩子方法来影响模版方法的逻辑 .
抽象类的任务是搭建逻辑的框架,通常由经验丰富的人员编写,因为抽象类的好坏直接决定了程序是否稳定性 .
模板方法模式的优点
- 口封装不变部分,扩展可变部分
把认为是不变部分的算法封装到父类实现,而可变部分的则可以通过继承来继续扩展 . 在悍马模型例子中,是不是就非常容易扩展? 例如增加一个H3型号的悍马模型,很容易呀,增加一个子类,实现父类的基本方法就可以了 . - 提取公共部分代码,便于维护
我们例子中刚刚走过的弯路就是最好的证明,如果我们不抽取到父类中,任由这种散乱的代码发生,想想后果是什么样子? 维护人员为了修正一个缺陷,需要到处查找类似的代码! - 行为由父类控制,子类实现
基本方法是由子类实现的,因此子类可以通过扩展的方式增加相应的功能,符合开闭原则 .
模板方法模式的缺点
按照我们的设计习惯,抽象类负责声明最抽象、最一般的事物属性和方法,实现类完成具体的事物属性和方法 . 但是模板方法模式却颠倒了,抽象类定义了部分抽象方法,由子类实现,子类执行的结果影响了父类的结果,也就是子类对父类产生了影响,这在复杂的项目中,会带来代码阅读的难度,而且也会让新手产生不适感 .
模板方法模式的使用场景
- 多个子类有公有的方法,并且逻辑基本相同时 .
- 重要、复杂的算法,可以把核心算法设计为模板方法,周边的相关细节功能则由各个子
类实现 . - 重构时,模板方法模式是一个经常使用的模式,把相同的代码抽取到父类中,然后通过钩子函数(见“模板方法模式的扩展”)约束其行为 .
参考书籍 :
<<设计模式之禅 第二版>>
<<设计模式>>