定义

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





图纸

设计模式——2_9 模版方法(Template Method)-LMLPHP




一个例子:从文件中获取信息分几步?

假定在你们公司的对外网站上有一个允许用户上传文件的接口,你会通过这个接口解析用户上传的文件,并把解析到的数据存到数据库中,用于共享供应商和你们自己的信息。但是由于经理坚信客户就是上帝,于是乎诡异的需求出现了,你需要从 word、excel还有xml文件中读取数据

准备好了吗?这次的例子开始了:



Reader

看到这样的题目你肯定会说,那很简单啊。肯定是要建一个 Reader 类簇,搞一个 WordReaderExcelReaderXMLReader。然后根据需要解析哪种文件去调用对应的 Reader 不就万事大吉了吗?


非常好,赶紧去吧 Reader 建出来,就像这样:

设计模式——2_9 模版方法(Template Method)-LMLPHP

Reader
/**
 * 读取器
 */
public interface Reader<E> {

    List<E> read(File file) throws IOException;
}

/**
 * word 文件的信息读取器
 */
public class WordReader<E> implements Reader<E> {

    @Override
    public List<E> read(File file) throws IOException {
        if (file.exists()) {
            //文件必须存在 打开流
            try (FileInputStream is = new FileInputStream(file)) {
                System.out.println("使用 is 进行word信息读取");

                return data;//返回最终用户要的数据
            }
        }

        return null;
    }
}

/**
 * excel 文件的信息读取器
 */
public class ExcelReader<E> implements Reader<E> {

    @Override
    public List<E> read(File file) throws IOException {
        if (file.exists()) {
            //文件必须存在 打开流
            try (FileInputStream is = new FileInputStream(file)) {
                System.out.println("使用 is 进行excel信息读取");

                return data;//返回最终用户要的数据
            }
        }

        return null;
    }
}

/**
 * xml 文件的信息读取器
 */
public class XMLReader<E> implements Reader<E> {

    @Override
    public List<E> read(File file) throws IOException {
        if (file.exists()) {
            //文件必须存在 打开流
            try (FileInputStream is = new FileInputStream(file)) {
                System.out.println("使用 is 进行xml信息读取");

                return data;//返回最终用户要的数据
            }
        }

        return null;
    }
}

然后我们的问题才刚刚开始

我们发现这个 Reader 里面大量的代码都是重复的,我们判断要读取的文件是否存在,然后需要开启一个文件流,并且保证无论如何他都会被正确关闭,而这些操作 无论我将来读取任何类型的文件,他们都应该是不变的


在我们的实现里出现了重复,那他们一定可以像被提取公因式一样被提取出来简化



读取一个文件分几步?

问:把大象塞冰箱分几步?

答:三步,打开冰箱门、把大象塞进去、关上冰箱门


其实这个脑筋急转弯是个标准的偷换概念,如果你不了解他的套路,一定会纠结答案里的第二步。正确答案其实并没有解决问题,而是给正确答案加了层包装,事实上无论你塞任何东西进冰箱,都需要打开和关上冰箱门

沿着这个思路,我们再来看看上面那个问题

读取一个文件分几步?

其实也就三步,打开文件流,读出数据,关闭文件流


也就是说我们可以考虑拆分刚刚的实现,就像这样:

设计模式——2_9 模版方法(Template Method)-LMLPHP

Reader
/**
 * 读取器
 */
public abstract class Reader<E> {

    public List<E> read(File file) throws IOException {
        if (haveFile(file)) {
            FileInputStream fis = getFileInStream(file);

            try {
                return resolution(fis);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                endWork(fis);
            }
        }

        return null;
    }

    protected boolean haveFile(File file) {
        return file.exists();
    }

    protected FileInputStream getFileInStream(File file) throws FileNotFoundException {
        return new FileInputStream(file);
    }

    protected abstract List<E> resolution(FileInputStream fis) throws IOException;

    protected void endWork(FileInputStream fis) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

/**
 * word 文件的信息读取器
 */
public class WordReader<E> extends Reader<E> {

    @Override
    public List<E> resolution(FileInputStream fis) throws IOException {
        //word 文件的读取方式
    }
}

/**
 * excel 文件的信息读取器
 */
public class ExcelReader<E> extends Reader<E> {

    @Override
    public List<E> resolution(FileInputStream fis) throws IOException {
        //excel 文件的读取方式
    }
}

/**
 * xml 文件的信息读取器
 */
public class XMLReader<E> extends Reader<E> {

    @Override
    public List<E> resolution(FileInputStream fis) throws IOException {
        //xml 文件的读取方式
    }
}

在这个实现中,我们将读取文件的流程分为 getFileInStream (打开文件流) -> resolution (读出文件信息)-> endWork(关闭文件流),最后把他们集成到 read 方法中并提供给外部代码调用


那你会说,就这?这不就是继承的标准用法吗?

整个模式确实是通过继承来实现的,但是他的核心是定义了骨架的 read 方法。在顶层的 read 方法中,他定义了 Reader 的工作流程,而且他调用了尚未被实现的方法 resolution,而这个方法恰恰是整个 Reader 中最核心的方法,他决定了这个 Reader 的具体工作内容

也就是说,这个实现完成了这样一个壮举,即:由父类(上层)决定调用方式,让子类(下层)决定具体实现

而这正是一个标准的模板方法实现




碎碎念

模板方法和好莱坞原则

好莱坞原则


据说模板方法的诞生是受到了好莱坞的运作模式的启发(Head First 设计模式 里写的,不管你信不信,反正我信了 ),书里是这样说的:

依赖腐败

在书上他提出了一个新概念:依赖腐败。这种腐败可不是我们平时说的 权力导致腐败,绝对的权力导致绝对的腐败。恰恰相反,依赖腐败 是上下层之间过于“亲密”导致的,上下层互相依赖,最终导致整个系统纠缠在一起,就像一团打结的毛线球一样

为了解决这种 依赖腐败 ,我们考虑让依赖尽可能变成单向的,更具体一点,让下层组件挂载到上层组件的结构中(在上层组件提前预留出位置的情况下)。上层组件只会知道在某个位置一定有某个下层组件存在,在某时某刻我可以调用他,至于他具体是如何实现的,上层组件从来是漠不关心的。这就是好莱坞原则

就像开车,我只需要知道车有行驶的功能,踩了油门他会走,踩住刹车他要停。至于他烧的是92还是95,用的电池还是油箱,跟我没关系的



模板方法和钩子

钩子,英文名叫 hook

还有个东西叫 钩子方法,比如上例中的 resolution

简单来说他就是指那种 下层可以提供实现,而且一定会在上层实现某种条件的情况下被调用 的抽象方法(空实现也算)


在JavaScript中钩子方法更是随处可见,只不过在那边叫回调函数,其实本质上两者是一样的

更进一步,使用回调函数的 JavaScript 函数其实自身也是一个模板方法的实现

当我使用带有回调函数的函数时,这个具体函数的执行骨架我是无法修改的,我只需要关注于我传进去的回调函数会在符合什么状态下被调用,以及应该执行什么



模板方法和框架

理论上来说,在创建框架的时候,模板方法总是你的好帮手,典型的比如Android里面的几大组件,servlet里面的请求处理流程,甚至是古老的 applet


并不是说这些框架都肯定用了模板方法,模板方法提供的是一种思路,即 上层制定规则,下层具体实现。对于框架来说,其他程序员就是他的客户,他必须保证在每个客户都拥有足够高自由度的情况下,整个框架可以按照预设的方式运作

上层决定出入口,整个框架的起承转合,因为这个执行方式是永远不会变的,这是框架的灵魂。至于具体要怎么起,怎么承,你来决定框架的血肉



模板方法和策略

对模板方法来说,由于上层指定规则,下层具体实现。但是这个“下层”可没有人规定一定是子类来实现的


譬如说在 Reader 的实现中,我完全可以把 resolution 的实现委托出去,创建一个对应的类簇,比如说 Handler 吧,让这个类簇可以专注于根据不同的文件类型读取不同的信息。这时候 ReaderHandler 之间,就会形成一个类似策略模式的实现,Handler 是作为可插拔的 Reader 部分算法来实现的,就像这样:

设计模式——2_9 模版方法(Template Method)-LMLPHP

Handler
/**
 * 读取器
 */
public class Reader<E> {

	public List<E> read(File file,Handler<E> handler) throws IOException {
		if (haveFile(file)) {
			FileInputStream fis = getFileInStream(file);

			try {
				return handler.resolution(fis);
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				endWork(fis);
			}
		}

		return null;
	}

	protected boolean haveFile(File file) {
		return file.exists();
	}

	protected FileInputStream getFileInStream(File file) throws FileNotFoundException {
		return new FileInputStream(file);
	}


	protected void endWork(FileInputStream fis) {
		try {
			fis.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

public interface Handler<E> {

	List<E> resolution(FileInputStream fis) throws IOException;
}

/**
 * word 文件的信息读取器
 */
public class WordHandler<E> implements Handler<E> {

    @Override
    public List<E> resolution(FileInputStream fis) throws IOException {
        //word 文件的读取方式
    }
}

/**
 * excel 文件的信息读取器
 */
public class ExcelHandler<E> implements Handler<E> {

    @Override
    public List<E> resolution(FileInputStream fis) throws IOException {
        //excel 文件的读取方式
    }
}

/**
 * xml 文件的信息读取器
 */
public class XMLHandler<E> implements Handler<E> {

    @Override
    public List<E> resolution(FileInputStream fis) throws IOException {
        //xml 文件的读取方式
    }
}

那你会说,不对啊,这种写法可不就是把变化的部分提取出来的策略模式吗?模板方式的优良传统呢?通过继承修改原有部分实现呢?想了这么久想出来一个违背祖宗的决定是吧?

首先我承认,这就是策略的实现,但不妨碍他也用了模板方法呀。我们把一定会发生变化的部分独立出来,形成策略簇。但是我们原有的 Reader 依然保留拓展的能力呀,假设以后我要包装我的流,或者在读取前或读取后做一些操作,完全可以通过创建 Reader 子类的方式来实现


这种写法在实战中很常见,比如说在 迭代器 一章中就有一个标准的例子,我们讲的外部迭代,其实就是通过这种方式来实现的



模板方法和生成器

你发现了吗?模板方法模式和生成器模式他们的思路是一样的,只是最终的目的不同而已。


模板方法 关注行为,他讲究某个行为必须执行的步骤和顺序,把这些不变的内容固定好后,由子类去确定具体的算法,从而实现算法和执行流程之间的解耦

生成器 就像流水线,流水线上的各个工位要做什么事情在最开始就设计好了,你只需要提供物料,不同的工位就会根据自己的职能对物料进行组装或加工,这是对一个对象不同创建流程的抽象。本质上讲这也是一种对算法的抽象——对一个对象的创建流程的抽象



写在后面

总是有人在纠结,自由到底有没有边界。窃以为,自由一定是有限度的

自由不是为所欲为,而是在一定限制内随心所欲。真正自由是需要对自己所做的事情负责的,只有一个人在有担当所做的事情造成的后果的能力后,才有权利去讲自己的自由

这就像模板方法委托给子类进行实现的那个钩子一样,我给你自由,但是你需要在我制定的框架下。就像做饭,模板方法不管你是做佛跳墙还是蛋炒饭,这是你的自由,但是做完都得把火关上,否则家里会着火,那是你承担不起的后果





万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容

04-17 08:35