10.考题抄错会做也白搭 - 模板方法模式(Template Method)

  时间: 3月27日19点   地点: 小菜大鸟住所的客厅   人物: 大鸟, 小菜.

     “小菜, 今天面试的情况如何?” 大鸟刚下班, 回来就敲开了小菜房门.

     “嗨,” 小菜叹了口气, “书到用时方恨少呀, 英语太烂, 没办法.”
     “是让你英语对话还是让你做英语题目了?”
     “要是英语对话, 我可能马上就跟他们说拜拜了. 是做编程的英语题, 因为平时英语文章看得少, 所以好多单词都是似曾相识, 总之猜不出意思, 造成我不等不瞎蒙. 还好都是选择题, 一百道蒙起来也不断太困难.”
     ……
     “考试题目抄错了, 那就不是考试了, 而考试试卷最大的好处就是, 大家都是一样的题目, 特别是标准化的考试, 比如全是选择或判断的题目, 那就最大化的限制了答题者的发挥, 大家都是 ABCD 或打勾打叉, 非对即错的结果.”
     “说得好, 这其实就是一个典型的设计模式, 不过为了讲解这个设计模式, 你先把抄题目的程序写给我看看.”
     “好的.”

10.1 重复=易错+难改

     二十分钟后, 小菜的第一份作业.



  • 学生甲抄的试卷类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 学生甲抄的试卷
    public class TestPaperA {
    // 试题1
    public void testQuestion1() {
    System.out.println("杨过得到, 后来给了郭靖.炼成倚天剑屠龙刀的玄铁可能是[]
    a.球磨铸铁 b.马口铁. c.高速合金钢. d.碳素纤维 ");
    System.out.println("答案: b");
    }
    // 试题2
    public void testQuestion2() {
    System.out.println("杨过程英,陆无双铲除了情花, 造成[]
    a.使这种植物不再害人 b.使一种珍稀植物灭绝 c.破坏了那个生物圈的生态平衡 d.造成该地区沙漠化");
    System.out.println("答案: a");
    }
    // 试题3
    public void testQuestion3() {
    System.out.println("蓝凤凰致使华山师,徒桃谷六仙呕吐不止, 如果你是大夫,你会给他开什么药[]
    a.阿司匹林 b.牛黄解毒片 c.氟哌酸 d.让他们喝大量生牛奶 e.以上全不对");
    System.out.println("答案: e");
    }
    }
  • 学生乙抄的试卷类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 学生乙抄的试卷
    public class TestPaperB {
    // 试题1
    public void testQuestion1() {
    System.out.println("杨过得到, 后来给了郭靖.炼成倚天剑屠龙刀的玄铁可能是[]
    a.球磨铸铁 b.马口铁. c.高速合金钢. d.碳素纤维 ");
    System.out.println("答案: d");
    }
    // 试题2
    public void testQuestion2() {
    System.out.println("杨过程英,陆无双铲除了情花, 造成[]
    a.使这种植物不再害人 b.使一种珍稀植物灭绝 c.破坏了那个生物圈的生态平衡 d.造成该地区沙漠化");
    System.out.println("答案: b");
    }
    // 试题3
    public void testQuestion3() {
    System.out.println("蓝凤凰致使华山师,徒桃谷六仙呕吐不止, 如果你是大夫,你会给他开什么药[]
    a.阿司匹林 b.牛黄解毒片 c.氟哌酸 d.让他们喝大量生牛奶 e.以上全不对");
    System.out.println("答案: a");
    }
    }
  • 客户端代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public static void main(String[] args) {
    System.out.println("学生甲抄的试卷: ");
    TestPaperA studentA = new TestPaperA();
    studentA.testQuestion1();
    studentA.testQuestion2();
    studentA.testQuestion3();
    System.out.println("学生乙抄的试卷: ");
    TestPaperB studentB = new TestPaperB();
    studentB.testQuestion1();
    studentB.testQuestion2();
    studentB.testQuestion3();
    }

10.3 提炼代码

     “大鸟, 我自己都感觉到了, 学生甲和学生乙两个抄试卷类非常类似, 除了答案不同, 没什么不一样, 这样写又容易写错, 又难以维护.”
     “说得对, 如果老师突然要改题目, 那两个人的代码都需要改, 如果某人抄错了, 那真是糟糕之极, 那你说怎么办?”
     “老师出一份试卷, 打印多份, 然学生填写答案就可以了. 在这里应该把试题和答案分享, 抽象出一个父类, 让两个子类继承于它, 公共的试题代码写到父类中就可以了.”
     “好的, 写写看.”
     十分钟后, 小菜的第二份作业.

  • 试卷父类代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class TestPaper {
    // 试题1
    public void testQuestion1() {
    System.out.println("杨过得到, 后来给了郭靖.炼成倚天剑屠龙刀的玄铁可能是[]
    a.球磨铸铁 b.马口铁. c.高速合金钢. d.碳素纤维 ");
    }
    // 试题2
    public void testQuestion2() {
    System.out.println("杨过程英,陆无双铲除了情花, 造成[]
    a.使这种植物不再害人 b.使一种珍稀植物灭绝 c.破坏了那个生物圈的生态平衡 d.造成该地区沙漠化");
    }
    // 试题3
    public void testQuestion3() {
    System.out.println("蓝凤凰致使华山师,徒桃谷六仙呕吐不止, 如果你是大夫,你会给他开什么药[]
    a.阿司匹林 b.牛黄解毒片 c.氟哌酸 d.让他们喝大量生牛奶 e.以上全不对");
    }
    }
  • 学生子类代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 学生甲抄的试卷
    public class TestPaperA extends TestPaper {
    @Override
    public void testQuestion1() {
    System.out.println("答案: b");
    }
    @Override
    public void testQuestion2() {
    System.out.println("答案: b");
    }
    @Override
    public void testQuestion3() {
    System.out.println("答案: b");
    }
    }

     客户端代码完全相同, 略.
     “大鸟, 这下子类就非常简单了, 只要填写答案就可以了.”
     “这还只是初步的泛化, 你仔细看看, 两个学生的类里面, 还有没有类似的代码.”
     “啊, 感觉相同的东西还是有的, 比如试题都有 super.testQuestion1(). 还有sysotem.out.println("答案: ") , 感觉除了选项abcd, 其他都是重复的.”
     “说得好, 我们既然用了继承, 并且肯定这个继承有意义, 就应该要称为子类的模板, 所有重复的代码都要上升到父类中去, 而不是让每个子类都去重复.
     “那应该怎么做呢? 我想不出来了.” 小菜缴械投降.
     “哈, 模板方法登场了, 当我们完成在某一细节层次的一个过程或一系列步骤, 但其个别步骤在更详细的层次上的实现可能不同时, 我们通常考虑用模板方法模式来处理. 现在来研究研究我们最初的试题方法.”

1
2
3
4
5
// 试题1
public void testQuestion1() {
System.out.println("杨过得到, 后来给了郭靖.炼成倚天剑屠龙刀的玄铁可能是[] a.球磨铸铁 b.马口铁. c.高速合金钢. d.碳素纤维 ");
System.out.println("答案: d"); // 只有答案学生会有不一样的结果, 其他全部是一样的.
}
1
2
3
4
5
6
7
8
// 试题1
public void testQuestion1() {
System.out.println("杨过得到, 后来给了郭靖.炼成倚天剑屠龙刀的玄铁可能是[] a.球磨铸铁 b.马口铁. c.高速合金钢. d.碳素纤维 ");
System.out.println("答案: " + answer1()); // 改成一个虚方法
}
// 此方法的目的就是让子类重写, 因为这里每个人的答案都是不同的.
protected abstract String answer1();

     “其余两个题目也用相同的方法”
     “然后子类就非常简单了, 重写抽象方法后, 把答案填上, 其他什么都不用做. 因为父类简历了所有的重复模板.”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 学生甲抄的试卷
public class TestPaperA extends TestPaper {
@Override
protected String answer1() {
return "b";
}
@Override
protected String answer2() {
return "b";
}
@Override
protected String answer3() {
return "b";
}
}


     “客户端需要改动一个小地方, 集本来是子类变量的声明, 改成了父类, 这样就可以利用多态性实现代码的复用了.”

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
System.out.println("学生甲抄的试卷: ");
TestPaper studentA = new TestPaperA(); // 将子类的声明改成父类
studentA.testQuestion1();
studentA.testQuestion2();
studentA.testQuestion3();
System.out.println("学生乙抄的试卷: ");
TestPaper studentB = new TestPaperB();
studentB.testQuestion1();
studentB.testQuestion2();
studentB.testQuestion3();
}

     “此时需要更多的学生来答试卷, 只不过是是在试卷的模板上填写选择题的答案, 这是每个人的试卷唯一的不同.” 大鸟说道.
     “大鸟太绝了吧, 还有姓名是不同的吧.”
     “哈, 小菜说得对, 除了题目答案, 每个人的姓名也是不同的. 但这样的做法的确是对试卷的最大复用.”

10.4 模板方法模式

     “而这其实就是最典型的模板方法模式.”

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



     AbstractClass 是抽象类, 其实也就是一抽象模板, 定义并实现了一个模板方法. 这个模板方法一般是一个具体的方法, 他给出了一个顶级逻辑的骨架, 而逻辑的组成步骤在相应的抽象类操作中, 推迟到子类实现. 顶级逻辑也有可能调用一些具体的方法.

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class AbstractClass {
// 一些抽象行为, 放到子类去实现
public abstract void primitiveOperation1();
public abstract void primitiveOperation2();
// 模板方法, 给出了逻辑的骨架, 而逻辑的组成是一组相应的抽象操作
// 它们都推迟到子类实现
public void templateMethod() {
primitiveOperation1();
primitiveOperation2();
}
}

     ConcreteClass, 实现父类所定义的一个获多个抽象方法. 每一个 AbstractClass 都可以有任意多个 ConcreteClass 与之对应, 而每一个 ConcreteClass 都可以给出这些抽象方法(也就是顶级逻辑的组成步骤) 的不同实现, 从而使得顶级逻辑的实现各不相同.

1
2
3
4
5
6
7
8
9
10
11
public class ConcreteClassA extends AbstractClass {
@Override
public void primitiveOperation1() {
System.out.println("具体类A实现方法1");
}
@Override
public void primitiveOperation2() {
System.out.println("具体类A实现方法2");
}
}
  • 客户端调用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public static void main(String[] args) {
    AbstractClass c;
    c = new ConcreteClassA();
    c.templateMethod();
    c = new ConcreteClassB();
    c.templateMethod();
    }

10.5 模板方法的特点

     “大鸟, 是不是可以这么说, 模板方法方式是通过把不变的行为搬移到超类, 去除子类中的重复代码, 来体现它的优势.
     “对的, 模板方法模式模式就是提供了一个很好的代码复用的平台. 因为有时候, 我们会遇到有一系列步骤构成的过程需要执行. 这个过程从高层次上看是相通的, 但有些步骤的实现可能不同. 这时候, 我们通常就应该用模板方法模式了”
     “你的意思是说, 碰到这个情况, 当不变的和可变的行为在方法的子类实现中混合在一起的时候, 不变的行为就会在子类中重复出现. 我们通过模板方法模式把这些行为搬移到单一的地方, 这样就帮助子类拜托重复的不变的行为的纠缠.
     “总结的好. 看来这省心的事你总是学得最快.”
     “哪里哪里, 这还不是大鸟教得好啊.” 小菜也不忘记谦虚两句, “不过老实讲, 这模板方法实在不算难, 我早就用过了, 只不过以前不知道这也算是一个设计模式.”
     “是呀, 模板方法模式是很常用的设计模式, 对继承和多态玩儿的好的人几乎都会在继承体系中多多少少的用到它. 比如在 .net 或 java 的类库设计中, 通常都会利用模板方法模式提取类库中的公共行为到抽象类中.”

10.6 主观题, 看你怎么蒙

     此时小菜的手机响了.
     “请问是菜遥先生吗?” 手机那边一女士的声音.
     “我是, 请问您是?” 小菜不认识这手机号.
     “我是您今天面试XX公司的人事部的经理……”

~感谢捧场,您的支持将鼓励我继续创作~