说到模板方法模式,它可能是一个让我们深入骨髓而又不自知的模式了,因为它在我们开发过程中会经常遇到,并且也非常简单。只不过,很多时候我们并不知道它就是模板方法模式而已。不负责任的说,当我们用到override关键字重写父类方法的时候,十有八九就跟模板方法模式有关了。

定义

先看一下模板方法模式的定义,模板方法模式定义了一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些步骤。

这里延迟到子类说的玄乎,其实就是子类继承并实现父类中的抽象方法(abstract),而重定义该算法的某些步骤指的就是子类重写父类的虚方法(virtual)。不过,不管是哪一个,子类都需要用到override

实例

我们还是通过一个例子来解释模板方法模式,先来一个经典的脑筋急转弯。

把一个大象装进冰箱要几个步骤?

答案是三步:
- 第一步,把冰箱门打开
- 第二步,把大象放进去
- 第三步,把冰箱门关上

对应到前面的定义,这里把大象装进冰箱的步骤就是算法的框架,而其中的每一步就是算法的具体步骤。我们用代码实现看看:

public abstract class AnimalToFridge
{
    public void Do()
    {
        OpenFridge();

        PutIntoFridge();

        CloseFridge();
    }

    private void OpenFridge()
    {
        Console.WriteLine("把冰箱门打开");
    }

    public abstract void PutIntoFridge();

    private void CloseFridge()
    {
        Console.WriteLine("把冰箱门关上");
    }
}

上面定义了一个把动物放进冰箱的基类,Do()方法定义了把大象装进冰箱的算法骨架,其中,打开冰箱和关闭冰箱两个步骤是固定不变的,变化只是把什么动物放进去。

再定义一个把大象放冰箱的子类,继承自上面的基类:

public class ElephantToFridge:AnimalToFridge
{
    public override void PutIntoFridge()
    {
        Console.WriteLine("把大象放进去");
    }
}

使用时,我们只需要调用Do()方法就可以完成把大象放冰箱的动作了:

static void Main(string[] args)
{
    AnimalToFridge elephantToFridge = new ElephantToFridge();
    elephantToFridge.Do();
}

这时候如果我们要把其他动物放进去,只需要继承AnimalToFridge就可以了,例如,我们把狗放进冰箱:

public class DogToFridge: AnimalToFridge
{
    public override void PutIntoFridge()
    {
        Console.WriteLine("把狗放进去");
    }
}

但是你以为这么简单就结束了吗?知道这个脑筋急转的朋友应该都知道它还有第二问。

然后把一个长颈鹿装进冰箱要几个步骤?

答案是四步:
- 第一步,把冰箱门打开
- 第二步,把大象弄出来
- 第三步,把长颈鹿放进去
- 第四步,把冰箱门关上

我们可以分析一下需求,也就是说,把大象放进之前不需要先把什么拿出来,但是放长颈鹿需要先把大象弄出来。再进一步分析的话,可以推测把鸡蛋、蚂蚁这样的小东西放进去,即使里面有大象,应该也不需要先把大象拿出来,而放狮子、老虎这样的大型动物就需要清空冰箱。为了满足这样的需求,我们的虚方法就登场了,代码可以做如下改进:

public abstract class AnimalToFridge
{
    public void Do()
    {
        OpenFridge();

        BeforePutIntoFridge();

        PutIntoFridge();

        CloseFridge();
    }

    private void OpenFridge()
    {
        Console.WriteLine("把冰箱门打开");
    }

    protected virtual void BeforePutIntoFridge() { }

    protected abstract void PutIntoFridge();

    private void CloseFridge()
    {
        Console.WriteLine("把冰箱门关上");
    }
}

基类中增加了一个BeforePutIntoFridge()的虚方法,方法只有一个空的实现(当然,如果需要的话,也可以添加具体内容),除此之外,我把虚方法和抽象方法的访问修饰符都改成protected了,因为,算法的单个步骤不应该被客户端直接调用,调用了也没有任何意义。这样,我们的大象和长颈鹿子类就可以如下实现了:

public class ElephantToFridge : AnimalToFridge
{
    protected override void PutIntoFridge()
    {
        Console.WriteLine("把大象放进去");
    }
}

public class GiraffeToFridge : AnimalToFridge
{
    protected override void BeforePutIntoFridge()
    {
        Console.WriteLine("把大象弄出来");
    }

    protected override void PutIntoFridge()
    {
        Console.WriteLine("把长颈鹿放进去");
    }
}

ElephantToFridge类不重写父类的BeforePutIntoFridge()方法,而GiraffeToFridge类重写了,也就是定义中所说的重定义了该算法的某些步骤了。

好了,这样就改造完成并满足需求了,我们再来看一下最终的整体类图:
设计模式-模板方法模式-LMLPHP

这就是模板方法模式,其实就是对继承加抽象方法和虚方法的使用,这可能算是继承的巅峰时刻了吧,在其他模式中只有被吐槽的命。

UML类图

再抽象一下就可以得到模板方法模式的UML类图了:
设计模式-模板方法模式-LMLPHP

钩子函数

在学习模板方法模式的时候,我们可能会经常听到钩子函数这个概念。钩子就是给子类一个授权,让子类来可重定义模板方法的某些步骤,听着高大上,说白了就是虚方法而已。

优缺点

优点

  • 封装了算法骨架,提高了代码复用性,简化了使用难度;
  • 封装不变部分,扩展可变部分,满足开闭原则。

缺点

  • 算法骨架不易更改,也就是原先定义的算法步骤如果需要变化,就不得不修改源代码了;
  • 扩展时,可能会产生很多子类,这是继承不可避免的缺陷。

跟建造者模式的异同

建造者模式很多地方跟模板方法模式是很相似的,例如,他们都是通过继承实现,都会把易变化的部分延迟到子类实现,并且都有一个方法封装骨架,只不过,建造者模式延迟到子类的是各部件的创建,封装的是最后的构建流程。而模板方法模式延迟到子类实现的是算法的某些步骤,封装的是算法骨架。也就是说如果你承认创建对象也是一种算法的话,那二者其实就差不多了。不过呢?他们也是有区别的,因为建造者模式中,各部件的建造需要客户端配合完成,因此,建造各部件的方法需要是public的,而模板方法模式中,各单独的算法步骤不应该被客户端直接调用,因此通常是protected的。不过,尽管如此,他们的设计思想确实是大同小异的。

说到这里,还记得建造者模式是如何通过使用委托来缓解子类过多的问题的吗?既然模板方法模式与建造者模式相似,那么处理方式也应该相似了,我们看看最终实现效果:

public class AnimalToFridge
{
    public void Do(Action beforePutIntoFridge,Action putIntoFridge)
    {
        OpenFridge();

        beforePutIntoFridge?.Invoke();

        putIntoFridge?.Invoke();

        CloseFridge();
    }

    private void OpenFridge()
    {
        Console.WriteLine("把冰箱门打开");
    }

    private void CloseFridge()
    {
        Console.WriteLine("把冰箱门关上");
    }
}

直接把抽象方法和虚方法都去掉了,换成了委托,只保留了算法骨架。这样做好处很明显,不需要子类了,无论多少动物,全部都通过委托搞定了。不过缺点也很明显,算法的实现交给了客户端,给客户端的使用带来了不小的负担,并且如果调用位置很多,还会导致大量代码重复,难以维护。

因此,模板方法模式具体该如何使用还得视情况而定。

源码链接

09-08 09:34