面向对象设计的设计模式(十三):模板方法模式

时间:2022-06-20
本文章向大家介绍面向对象设计的设计模式(十三):模板方法模式,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
定义

在模板方法模式(Template Method Pattern)中,定义一个操作中的算法的框架,而将一些步骤的执行延迟到子类中,使得子类可以在不改变算法的结构的前提下即可重新定义该算法的某些特定步骤。

适用场景

通常一个算法需要几个执行步骤来实现,而有时我们需要定义几种执行步骤一致,但是却可能在某个步骤的实现略有差异的算法。也就是说我们既需要复用实现相同的步骤,也可以通过在某个步骤的不同实现来灵活扩展出更多不同的算法。

在这种场景下,我们可以使用模板方法模式:定义好一个算法的框架,在父类实现可以复用的算法步骤,而将需要扩展和修改其他步骤的任务推迟给子类进行。

现在我们清楚了模板方法模式的适用场景,下面看一下这个模式的成员和类图。

成员与类图

成员

模板方法模式的成员除了客户端以外,只有两个成员:

  • 算法类(Algorithm):算法类负责声明算法接口,算法步骤接口。并实现可复用的算法步骤接口,且将需要子类实现的接口暴露出来。
  • 具体算法类(Concrete Algorithm):具体算法类负责实现算法类声明的算法步骤接口。

有些参考资料定义这两个成员为Abstract ClassConcrete Class

下面通过类图来看一下命令模式各个成员之间的关系:

模式类图

模板方法模式类图

由上图可以看出,Algorithmexcute方法是算法接口,它在内部调用了三个步骤方法:step1,step2,step3。而step2是未暴露在外部的,因为这个步骤是需要各个子类复用的。因此Algorithm只将step1step3暴露了出来以供子类来调用。

代码示例

场景概述

模拟一个制作三种热饮的场景:热美式咖啡,热拿铁,热茶。

场景分析

这三种热饮的制作步骤是一致的,都是三个步骤:

  • 步骤一:准备热水
  • 步骤二:加入主成分
  • 步骤三:加入辅助成分(也可以不加,看具体热饮的种类)

虽然制作步骤是一致的,但是不同种类的热饮在每一步可能是不同的:咖啡和茶叶主成分是咖啡粉和茶叶;而辅助成分:美式咖啡和茶叶可以不添加,而拿铁还需添加牛奶。

而第一步是相同的:准备热水。

根据上面对模板方法模式的介绍,像这样算法步骤相同,算法步骤里的实现可能相同或不同的场景我们可以使用模板方法模式。下面我们看一下如何用代码来模拟该场景。

代码实现

首先我们创建算法类HotDrink

//================== HotDrink.h ==================

@interface HotDrink : NSObject

- (void)makingProcess;

- (void)addMainMaterial;

- (void)addIngredients;

@end



//================== HotDrink.m ==================

@implementation HotDrink

- (void)makingProcess{

    NSLog(@" ===== Begin to making %@ ===== ", NSStringFromClass([self class]));

    [self boilWater];
    [self addMainMaterial];
    [self addIngredients];
}


- (void)prepareHotWater{

    NSLog(@"prepare hot water");
}


- (void)addMainMaterial{

    NSLog(@"implemetation by subClasses");
}


- (void)addIngredients{

    NSLog(@"implemetation by subClasses");
}


@end

HotDrink向外部暴露了一个制作过程的接口makingProcess,这个接口内部调用了热饮的所有制作步骤方法:

- (void)makingProcess{

     //准备热水     
    [self prepareHotWater];

    //添加主成分
    [self addMainMaterial];

    //添加辅助成分
    [self addIngredients];
}

HotDrink只向外暴露了这三个步骤中的两个需要子类按照自己方式实现的接口:

//添加主成分
- (void)addMainMaterial;

//添加辅助成分
- (void)addIngredients;

因为热饮的第一步都是一致的(准备热水),所以第一步骤的接口没有暴露出来给子类实现,而是直接在当前类实现了,这也就是模板方法的一个可以复用代码的优点。

OK,我们现在创建好了算法类,那么根据上面的需求,我们接着创建三个具体算法类:

  • HotDrinkTea :热茶
  • HotDrinkLatte :热拿铁
  • HotDrinkAmericano:热美式
//================== HotDrinkTea.h ==================

@interface HotDrinkTea : HotDrink

@end



//================== HotDrinkTea.m ==================

@implementation HotDrinkTea


- (void)addMainMaterial{

    NSLog(@"add tea leaf");
}


- (void)addIngredients{

    NSLog(@"add nothing");
}


@end

热茶在addMainMaterial步骤里面是添加了茶叶,而在addIngredients步骤没有做任何事情(这里先假定是纯的茶叶)。

类似地,我们看一下两种热咖啡的实现。首先是热拿铁HotDrinkLatte:

//================== HotDrinkLatte.h ==================

@interface HotDrinkLatte : HotDrink

@end



//================== HotDrinkLatte.m ==================

@implementation HotDrinkLatte

- (void)addMainMaterial{

    NSLog(@"add ground coffee");
}


- (void)addIngredients{

    NSLog(@"add milk");
}


@end

热拿铁在addMainMaterial步骤里面是添加了咖啡粉,而在addIngredients步骤添加了牛奶。

下面再看一下热美式HotDrinkAmericano

//================== HotDrinkAmericano.h ==================

@interface HotDrinkAmericano : HotDrink

@end



//================== HotDrinkAmericano.m ==================

@implementation HotDrinkAmericano

- (void)addMainMaterial{

    NSLog(@"add ground coffee");
}


- (void)addIngredients{

    NSLog(@"add nothing");
}

@end

热美式在addMainMaterial步骤里面是添加了咖啡粉,而在addIngredients步骤没有做任何事,因为美式就是纯的咖啡,理论上除了水和咖啡不需要添加任何其他东西。

到现在三种热饮类创建好了,我们现在分别制作这三种热饮,并看一下日至输出:

===== Begin to making HotDrinkTea =====
prepare hot water
add tea leaf
add nothing
===== Begin to making HotDrinkLatte =====
prepare hot water
add ground coffee
add milk
===== Begin to making HotDrinkAmericano =====
prepare hot water
add ground coffee
add nothing

上面的日至输出准确无误地反映了我们所定义的这三种热饮制作过程:

  • 热茶:准备热水 + 茶叶
  • 热拿铁:准备热水 + 咖啡 + 牛奶
  • 热美式:准备热水 + 咖啡

下面看一下上面代码对应的类图。

代码对应的类图

模板方法模式代码示例类图

优点

  • 复用性高:将相同的代码放在父类中,而不同的部分则由子类实现
  • 扩展性高:可以通过创建不同的子类来扩展不同的算法
  • 符合开闭原则:可变与不可变的部分分离,而且不同的可变部分(子类)也是相互分离的,所以符合了开闭原则

缺点

  • 导致类的个数增加:对于每一个算法实现都需要一个子类,如果实现过多的话会导致类的个数增加
  • 由继承关系导致的缺点:如果父类需要增加或减少它的行为,则所有的子类都需要同步修改一次

iOS SDK 和 JDK中的应用

  • 在 iOS SDK 中,我们可以重写 UIViewdrawRect:方法可以自定义绘图,是模板方法模式的一种实践。
  • 在JDK中,java.lang.Runnable是使用JDK的经典场景:Runnable接口可以作为抽象的命令,而实现了Runnable的线程即是具体的命令。