Java-设计模式之模板模式

模板模式

引子

这是一个很简单的模式,却被非常广泛的使用。之所以简单是因为在这个模式中仅仅使用到了继承关系。

继承关系由于自身的缺陷,被专家们扣上了“罪恶”的帽子。“使用委派关系代替继承关系”,“尽量使用接口实现而不是抽象类继承”等等专家警告,让我们这些菜鸟对继承“另眼相看”。

其实,继承还是有很多自身的优点所在。只是被大家滥用的似乎缺点更加明显了。合理的利用继承关系,还是能对你的系统设计起到很好的作用的。而模板方法模式就是其中的一个使用范例。

定义与结构

模板方法(Template Method)模式:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。这里的算法的结构,可以理解为你根据需求设计出来的业务流程。特定的步骤就是指那些可能在内容上存在变数的环节。

可以看出来,模板方法模式也是为了巧妙解决变化对系统带来的影响而设计的。使用模板方法使系统扩展性增强,最小化了变化对系统的影响。这一点,在下面的举例中可以很明显的看出来。

来看下这个简单模式的结构吧:

  1. 抽象类(Abstract Class):定义了一到多个的抽象方法,以供具体的子类来实现它们;而且还要实现一个模板方法,来定义一个算法的骨架。该模板方法不仅调用前面的抽象方法,也可以调用其他的操作,只要能完成自身的使命。

  2. 具体类(Concrete Class):实现父类中的抽象方法以完成算法中与特定子类相关的步骤。

下面是模板方法模式的结构图。直接把《设计模式》上的图拿过来用下:

在这里插入图片描述

举例

还是在我刚刚分析完源码的 JUnit 中找个例子吧。JUnit 中的 TestCase 以及它的子类就是一个模板方法模式的例子。在 TestCase 这个抽象类中将整个测试的流程设置好了,比如先执行 Setup 方法初始化测试前提,在运行测试方法,然后再 TearDown 来取消测试设置。但是你将在 Setup、TearDown 里面作些什么呢?鬼才知道呢!!因此,而这些步骤的具体实现都延迟到子类中去,也就是你实现的测试类中。

来看下相关的源代码吧。

这是 TestCase 中,执行测试的模板方法。你可以看到,里面正像前面定义中所说的那样,它制定了“算法”的框架——先执行 setUp 方法来做下初始化,然后执行测试方法,最后执行 tearDown 释放你得到的资源。

public void runBare() throws Throwable {
    
    setUp();
    try {
        runTest();
    }
    finally {
        tearDown();
    }
}

这就是上面使用的两个方法。与定义中不同的是,这两个方法并没有被实现为抽象方法,而是两个空的无为方法(被称为钩子方法)。这是因为在测试中,我们并不是必须要让测试程序使用这两个方法来初始化和释放资源的。如果是抽象方法,则子类们必须给它一个实现,不管用到用不到。这显然是不合理的。使用钩子方法,则你在需要的时候,可以在子类中重写这些方法。

protected void setUp() throws Exception {
}

protected void tearDown() throws Exception {
}

适用情况

根据上面对定义的分析,以及例子的说明,可以看出模板方法适用于以下情况:

  1. 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。

  2. 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。其实这可以说是一种好的编码习惯了。

  3. 控制子类扩展。模板方法只在特定点调用操作,这样就只允许在这些点进行扩展。比如上面 runBare()方法就只在 runTest 前面适用 setUp 方法。如果你不愿子类来修改你的模板方法定义的框架,你可以采用两种方式来做:一是在 API 中不体现出你的模板方法;或者将你的模板方法置为 final 就可以了。

可以看出,使用模板方法模式可以将代码的公共行为提取出来,达到复用的目的。而且,在模板方法模式中,是由父类的模板方法来控制子类中的具体实现。这样你在实现子类的时候,根本不需要对业务流程有太多的了解。

import java.util.*;
import junit.framework.*;

//访问者角色
interface Visitor {
    
    void visit(Gladiolus g);
    void visit(Runuculus r);
    void visit(Chrysanthemum c);
}

// The Flower hierarchy cannot be changed:
//元素角色
interface Flower {
    void accept(Visitor v);
}

//以下三个具体元素角色
class Gladiolus implements Flower {public void accept(Visitor v) { v.visit(this);}
}

class Runuculus implements Flower {
    public void accept(Visitor v) { v.visit(this);}
}

class Chrysanthemum implements Flower {
    public void accept(Visitor v) { v.visit(this);}
}

// Add the ability to produce a string:
//实现的具体访问者角色
class StringVal implements Visitor {
    
    String s;
    
    public String toString() { return s; }
    
    public void visit(Gladiolus g) {
        s = "Gladiolus";
    }
    
    public void visit(Runuculus r) {
        s = "Runuculus";
    }
    
    public void visit(Chrysanthemum c) {
        s = "Chrysanthemum";
    }
}

// Add the ability to do "Bee" activities:
//另一个具体访问者角色
class Bee implements Visitor {
    
    public void visit(Gladiolus g) {
        System.out.println("Bee and Gladiolus");
    }
    
    public void visit(Runuculus r) {
        System.out.println("Bee and Runuculus");
    }
    
    public void visit(Chrysanthemum c) {
        System.out.println("Bee and Chrysanthemum");
    }
}

//这是一个对象生成器
//这不是一个完整的对象结构,这里仅仅是模拟对象结构中的元素
class FlowerGenerator {
    
    private static Random rand = new Random();
    
    public static Flower newFlower() {
        
        switch(rand.nextInt(3)) {
            default:
            case 0: return new Gladiolus();
            case 1: return new Runuculus();
            case 2: return new Chrysanthemum();
        }
    }
}

//客户测试程序
public class BeeAndFlowers extends TestCase {
    /*
    在这里你能看到访问者模式执行的流程:
    首先在客户端先获得一个具体的访问者角色
    遍历对象结构
    对每一个元素调用 accept 方法,将具体访问者角色传入
    这样就完成了整个过程
    */
    //对象结构角色在这里才组装上
    List flowers = new ArrayList();
    
    public BeeAndFlowers() {
        for(int i = 0; i < 10; i++)
            flowers.add(FlowerGenerator.newFlower());
    }
    
    Visitor sval ;
    
    public void test() {
// It's almost as if I had a function to// produce a Flower string representation:
//这个地方你可以修改以便使用另外一个具体访问者角色
        sval = new StringVal();
        Iterator it = flowers.iterator();
        while(it.hasNext()) {
            ((Flower)it.next()).accept(sval);
            System.out.println(sval);
        }
    }
    
    public static void main(String args[]) {
        junit.textui.TestRunner.run(BeeAndFlowers.class);
    }
}

双重分派

对了,你在上面的例子中体会到双重分派的实现了没有?

首先在客户程序中将具体访问者模式作为参数传递给具体元素角色(加亮的地方所示)。这便完成了一次分派。

进入具体元素角色后,具体元素角色调用作为参数的具体访问者模式中的 visitor 方法,同时将自己(this)作为参数传递进去。具体访问者模式再根据参数的不同来选择方法来执行(加亮的地方所示)。这便完成了第二次分派。

优缺点及适用情况

先来看下访问者模式的使用能否避免引言中的痛苦。使用了访问者模式以后,对于原来的类层次增加新的操作,仅仅需要实现一个具体访问者角色就可以了,而不必修改整个类层次。而且这样符合“开闭原则”的要求。而且每个具体的访问者角色都对应于一个相关操作,因此如果一个操作的需求有变,那么仅仅修改一个具体访问者角色,而不用改动整个类层次。

看来访问者模式确实能够解决我们面临的一些问题。

而且由于访问者模式为我们的系统多提供了一层“访问者”,因此我们可以在访问者中添加一些对元素角色的额外操作。

但是“开闭原则”的遵循总是片面的。如果系统中的类层次发生了变化,会对访问者模式产生什么样的影响呢?你必须修改访问者角色和每一个具体访问者角色…

看来访问者角色不适合具体元素角色经常发生变化的情况。而且访问者角色要执行与元素角色相关的操作,就必须让元素角色将自己内部属性暴露出来,而在 java 中就意味着其它的对象也可以访问。这就破坏了元素角色的封装性。而且在访问者模式中,元素与访问者之间能够传递的信息有限,这往往也会限制访问者模式的使用。

《设计模式》一书中给出了访问者模式适用的情况:

  1. 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作。

  2. 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操作“污染”这些对象的类Visitor 使得你可以将相关的操作集中起来定义在一个类中。

  3. 当该对象结构被很多应用共享时,用 Visitor 模式让每个应用仅包含需要用到的操作。

  4. 定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。改变对象结构类需要重定义对所有访问者的接口,这可能需要很大的代价。如果对象结构类经常改变,那么可能还是在这些类中定义这些操作较好。你是否能很好的理解呢?

总结

这是一个巧妙而且复杂的模式,它的使用条件比较苛刻。当系统中存在着固定的数据结构(比如上面的类层次)
,而有着不同的行为,那么访问者模式也许是个不错的选择。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值