# 模板方法模式

本文作者:程序员飞云

本站地址:https://www.flycode.icu (opens new window)

# 模板方法模式的定义

模板方法(Template Method)模式,定义一个操作中的算法骨架,将一些步骤延迟到子类里面。模板方法可以是的子类不修改算法骨架即可重新定义改算法的特定步骤。

# 案例一—-问卷测试

我们平常经常做一些性格测试,能力测试,学生考试等等

我们现在有3道题目,下面是一个用户输出,假设现在我没有卷子发,需要用户自己手动抄写题目,现在有n个人在做这个问卷。

1. 以下各类中哪几个是线程安全的?( )
	A.ArrayList
	B.Vector
	C.Hashtable
	D.Stack
答案:B,C,D
	
2. 以下哪些继承自 Collection 接口()
     A.List
     B.Set
     C.Map
     D.Array
答案:A,B 
3. final、finally和finalize的区别中,下述说法正确的有()
    A.final用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。
    B.finally是异常处理语句结构的一部分,表示总是执行。
    C.finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源的回收,例如关闭文件等。
    D.引用变量被final修饰之后,不能再指向其他对象,它指向的对象的内容也是不可变的。
    
答案:A,B
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 基础

A抄写了题目以及自己的答案

public class TestPaperOperationA {
    void doWriteQuestion1() {
        System.out.println("1.以下各类中哪几个是线程安全的?( )\n" +
                "A.ArrayList\n" +
                "B.Vector\n" +
                "C.Hashtable\n" +
                "D.Stack");

        System.out.println("答案:A C D");
    }

    void doWriteQuestion2() {
        System.out.println("2.以下哪些继承自 Collection 接口()\n" +
                "A.List\n" +
                "B.Set\n" +
                "C.Map\n" +
                "D.Array");

        System.out.println("答案:D");
    }

    void doWriteQuestion3() {
        System.out.println("3.final、finally和finalize的区别中,下述说法正确的有?\n" +
                "A.final用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。\n" +
                "\n" +
                "B.finally是异常处理语句结构的一部分,表示总是执行。\n" +
                "\n" +
                "C.finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源的回收,例如关闭文件等。\n" +
                "\n" +
                "D.引用变量被final修饰之后,不能再指向其他对象,它指向的对象的内容也是不可变的。");

        System.out.println("答案:A B");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

B也是这样编写代码

public class TestPaperOperationB {
    void doWriteQuestion1() {
        System.out.println("以下各类中哪几个是线程安全的?( )  正确答案: B C D\n" +
                "A.ArrayList\n" +
                "B.Vector\n" +
                "C.Hashtable\n" +
                "D.Stack");

        System.out.println("答案:A");
    }

    void doWriteQuestion2() {
        System.out.println("以下哪些继承自 Collection 接口()正确答案: A B\n" +
                "A.List\n" +
                "B.Set\n" +
                "C.Map\n" +
                "D.Array");

        System.out.println("答案:A");
    }

    void doWriteQuestion3() {
        System.out.println("final、finally和finalize的区别中,下述说法正确的有?正确答案: A B\n" +
                "A.final用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。\n" +
                "\n" +
                "B.finally是异常处理语句结构的一部分,表示总是执行。\n" +
                "\n" +
                "C.finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源的回收,例如关闭文件等。\n" +
                "\n" +
                "D.引用变量被final修饰之后,不能再指向其他对象,它指向的对象的内容也是不可变的。");

        System.out.println("答案:D");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

进行测试

public class Main {
    public static void main(String[] args) {
        TestPaperOperationA testPaperOperationA = new TestPaperOperationA();
        testPaperOperationA.doWriteQuestion1();
        testPaperOperationA.doWriteQuestion2();
        testPaperOperationA.doWriteQuestion3();
        
        TestPaperOperationB testPaperOperationB = new TestPaperOperationB();
        testPaperOperationB.doWriteQuestion1();
        testPaperOperationB.doWriteQuestion2();
        testPaperOperationB.doWriteQuestion3();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

代码比对

看了上面的代码第一个感觉就是繁琐,明明都是一样的题目却要编写多次,浪费时间,一旦问题多了用户还可能写错题目,如果我现在要修改某个题目,那么A和B都要去修改对应的题目,但是A和B只需要给出答案就可以。那么我们为什么不先复印好题目,用户直接提交答案。

上面这个特征是否和继承有点相似,父类统一规范,子类可以自己进行扩展,例如上面的父类可以将题目写好,然后子类继承只需要修改自己的答案就可以,这样是否就能省事,减少麻烦了呢,模板方法就此产生。

# 模板方法模式

首先需要定义好一个模板,用户只需要填入对应的答案就可以。

# 模板类

需要使用abstract进行修饰的原因是我们只需要让用户填入选择就好,需要编写一个抽象方法,而这个选择的答案是交给子类来重新编写的。

public abstract class TestPaperTemplate {

    void doWriteQuestion1() {
        System.out.println("1.以下各类中哪几个是线程安全的?( )\n" +
                "A.ArrayList\n" +
                "B.Vector\n" +
                "C.Hashtable\n" +
                "D.Stack");

        System.out.println("答案:" + this.answer1());
    }

    protected abstract String answer1();

    void doWriteQuestion2() {
        System.out.println("2.以下哪些继承自 Collection 接口()\n" +
                "A.List\n" +
                "B.Set\n" +
                "C.Map\n" +
                "D.Array");

        System.out.println("答案: " + this.answer2());
    }

    protected abstract String answer2();

    void doWriteQuestion3() {
        System.out.println("3.final、finally和finalize的区别中,下述说法正确的有?\n" +
                "A.final用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。\n" +
                "\n" +
                "B.finally是异常处理语句结构的一部分,表示总是执行。\n" +
                "\n" +
                "C.finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源的回收,例如关闭文件等。\n" +
                "\n" +
                "D.引用变量被final修饰之后,不能再指向其他对象,它指向的对象的内容也是不可变的。");

        System.out.println("答案:" + this.answer3());
    }

    protected abstract String answer3();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 答案A类

public class TestPaperA extends TestPaperTemplate{
    @Override
    protected String answer1() {
        return "A";
    }

    @Override
    protected String answer2() {
        return "B";
    }

    @Override
    protected String answer3() {
        return "C";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 答案B类

public class TestPaperB extends TestPaperTemplate{
    @Override
    protected String answer1() {
        return "C";
    }

    @Override
    protected String answer2() {
        return "A";
    }

    @Override
    protected String answer3() {
        return "B";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 测试类

public static void main(String[] args) {
    System.out.println("A做题");
    TestPaperA testPaperA = new TestPaperA();
    testPaperA.doWriteQuestion1();
    testPaperA.doWriteQuestion2();
    testPaperA.doWriteQuestion3();

    System.out.println("B做题");
    TestPaperB testPaperB = new TestPaperB();
    testPaperB.doWriteQuestion1();
    testPaperB.doWriteQuestion2();
    testPaperB.doWriteQuestion3();
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 结构图

image-20240103171636689

整体看下来用户除了写答案,就没有其他操作了,这正好符合我们的需求

# 案例二—-爬取商城信息

此案例改编自https://bugstack.cn/md/develop/design-pattern/2020-07-07-%E9%87%8D%E5%AD%A6%20Java%20%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E3%80%8A%E5%AE%9E%E6%88%98%E6%A8%A1%E6%9D%BF%E6%A8%A1%E5%BC%8F%E3%80%8B.html (opens new window)

假设我们现在目前需要爬取京东,淘宝商城里面的商品信息,比如优惠价格,数量等等,然后生成对应的海报,我这里就只保留了登录和爬取信息,返回爬取信息。

爬取信息对于这两家都是通用的。不登录有些优惠信息商品是看不到了,所以需要登录方法,登录成功开始爬取信息,爬取成功后生成爬取信息,所以这个模板总共两个方法,这两个方法会根据不同的商城进行修改。

# 模板类

只有登录和爬取信息功能

public abstract class NetMall {
    String uid;

    String uPwd;

    public NetMall(String uid, String uPwd) {
        this.uid = uid;
        this.uPwd = uPwd;
    }

    /**
     * 根据对应的url来进行操作
     *
     * @param url 对应商城的url
     * @return
     */
    public void generateGoodsPoster(String url) {
        // 未登录
        if (!login(uid, uPwd)) throw new RuntimeException("登录失败");
        // 登录成功,爬取数据
        Map<String, String> res = reptile(url);

        res.forEach((k, v) -> System.out.println(k + " : " + v));
        System.out.println("爬取成功");
    }

    // 1. 模拟登陆
    protected abstract Boolean login(String uId, String uPwd);

    // 2. 爬取信息
    protected abstract Map<String, String> reptile(String url);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 具体实现类JD

假设JD无法登录,爬取信息失败

public class JDNetMall extends NetMall {
    public JDNetMall(String account, String password) {
        super(account, password);
    }

    @Override
    protected Boolean login(String uId, String uPwd) {
        System.out.println("uid---" + uId + "---upwd---" + uPwd);
        System.out.println("京东登录失败");
        return false;
    }

    @Override
    protected Map<String, String> reptile(String url) {
        System.out.println("京东爬取失败");
        return null;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 具体实现类TaoBao

淘宝登录成功,爬取数据,然后返回数据

public class TaoBaoNetMall extends NetMall {
    public TaoBaoNetMall(String uid, String uPwd) {
        super(uid, uPwd);
    }

    @Override
    protected Boolean login(String uId, String uPwd) {
        System.out.println("uid---" + uId + "---upwd---" + uPwd);
        System.out.println("淘宝网登录成功");
        return true;
    }

    @Override
    protected Map<String, String> reptile(String url) {
        System.out.println("爬取淘宝网数据");
        Map<String, String> map = new HashMap<>();
        map.put("name", "华为手机");
        map.put("price", "2999");
        return map;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 测试类

public class Main {
    public static void main(String[] args) {
//        JDNetMall jdNetMall = new JDNetMall("123","123");
//        jdNetMall.generateGoodsPoster("test");

        TaoBaoNetMall taoBaoNetMall = new TaoBaoNetMall("234","234");
        taoBaoNetMall.generateGoodsPoster("test");
    }
}
1
2
3
4
5
6
7
8
9

万一以后要爬取pdd,天猫等等,都是一样的操作,扩展性很强,也易于维护。

# 优点

  1. 去除子类中的重复代码,达到代码复用
  2. 有一点解耦包含在里面,那就是不变的放在模板里面,变化的放在子类里面,这个子类出现问题了不会影响到其他的类

# 使用场景

  1. AQS里面的tryAcquire0
  2. Spring源码里面大部分使用到了,之后会整理出来
  3. 个人飞云代码生成平台,里面对于代码生成器制作的时候步骤总共分为6步,编写模板,读取信息,生成文件,构建jar,编写脚本,精简程序,这几个步骤在不同的场景下需要进行重写,例如用户只需要简单的使用命令生成代码,不需要知道这个是如何写的,那么这时候只需要给用户精简版的命令运行就可以,但是开发者可能需要完整代码,那么可以同时保留精简程序和完整代码
最近更新: 1/14/2025, 1:13:36 AM
飞云编程   |