--- title: 设计模式之模板方法模式 date: 2015-01-06 09:43:00 order: 14 categories: - 设计 - 设计模式 tags: - 设计 - 设计模式 permalink: /pages/e0f9d2ec/ --- # 设计模式之模板方法模式 ## 意图 **模板方法模式** (Template Method) 是一种行为设计模式, 它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。 模板方法模式是所有模式中最为常见的几个模式之一,是**基于继承**的**代码复用**的基本技术。,没有关联关系。 因此,在模板方法模式的类结构图中,**只有继承关系**。 模板方法模式需要开发抽象类和具体子类的设计师之间的协作。一个设计师负责给出一个算法的轮廓和骨架,另一些设计师则负责给出这个算法的各个逻辑步骤。 代表这些具体逻辑步骤的方法称做**基本方法(primitive method)**;而将这些基本方法汇总起来的方法叫做**模板方法(template method)**,这个设计模式的名字就是从此而来。 在模板方法模式中,首先父类会定义一个算法的框架,即实现算法所必须的所有方法。 - 其中,具有共性的代码放在父类的具体方法中。 - 各个子类特殊性的代码放在子类的具体方法中。但是父类中需要有对应抽象方法声明。 - 钩子方法可以让子类决定是否对算法的不同点进行挂钩。 ## 适用场景 - 当你只希望客户端扩展某个特定算法步骤, 而不是整个算法或其结构时, 可使用模板方法模式。 - 当多个类的算法除一些细微不同之外几乎完全一样时, 你可使用该模式。 但其后果就是, 只要算法发生变化, 你就可能需要修改所有的类。 ## 结构 ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210517201945.png) ### 结构说明 1. **抽象类** (Abstract­Class) 会声明作为算法步骤的方法, 以及依次调用它们的实际模板方法。 算法步骤可以被声明为 `抽象`类型, 也可以提供一些默认实现。 2. **具体类** (Concrete­Class) 可以重写所有步骤, 但不能重写模板方法自身。 ### 结构代码范式 **AbstractClass** : 抽象类,定义并实现一个模板方法。这个模板方法定义了算法的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类去实现。顶级逻辑也有可能调用一些具体方法。 ```java abstract class AbstractClass { public abstract void PrimitiveOperation1(); public abstract void PrimitiveOperation2(); public void TemplateMethod() { PrimitiveOperation1(); PrimitiveOperation2(); } } ``` **ConcreteClass** : 实现实现父类所定义的一个或多个抽象方法。 ```java class ConcreteClassA extends AbstractClass { @Override public void PrimitiveOperation1() { System.out.println("具体A类方法1"); } @Override public void PrimitiveOperation2() { System.out.println("具体A类方法2"); } } class ConcreteClassB extends AbstractClass { @Override public void PrimitiveOperation1() { System.out.println("具体B类方法1"); } @Override public void PrimitiveOperation2() { System.out.println("具体B类方法2"); } } ``` 客户端 ```java public class TemplateMethodPattern { public static void main(String[] args) { AbstractClass objA = new ConcreteClassA(); AbstractClass objB = new ConcreteClassB(); objA.TemplateMethod(); objB.TemplateMethod(); } } ``` ## 伪代码 本例中的**模板方法**模式为一款简单策略游戏中人工智能的不同分支提供 “框架”。 ![模板方法模式示例的结构](https://raw.githubusercontent.com/dunwu/images/master/snap/20210517202308.png) 一款简单游戏的 AI 类。 游戏中所有的种族都有几乎同类的单位和建筑。 因此你可以在不同的种族上复用相同的 AI 结构, 同时还需要具备重写一些细节的能力。 通过这种方式, 你可以重写半兽人的 AI 使其更富攻击性, 也可以让人类侧重防守, 还可以禁止怪物建造建筑。 在游戏中新增种族需要创建新的 AI 子类, 还需要重写 AI 基类中所声明的默认方法。 ```java // 抽象类定义了一个模板方法,其中通常会包含某个由抽象原语操作调用组成的算 // 法框架。具体子类会实现这些操作,但是不会对模板方法做出修改。 class GameAI is // 模板方法定义了某个算法的框架。 method turn() is collectResources() buildStructures() buildUnits() attack() // 某些步骤可在基类中直接实现。 method collectResources() is foreach (s in this.builtStructures) do s.collect() // 某些可定义为抽象类型。 abstract method buildStructures() abstract method buildUnits() // 一个类可包含多个模板方法。 method attack() is enemy = closestEnemy() if (enemy == null) sendScouts(map.center) else sendWarriors(enemy.position) abstract method sendScouts(position) abstract method sendWarriors(position) // 具体类必须实现基类中的所有抽象操作,但是它们不能重写模板方法自身。 class OrcsAI extends GameAI is method buildStructures() is if (there are some resources) then // 建造农场,接着是谷仓,然后是要塞。 method buildUnits() is if (there are plenty of resources) then if (there are no scouts) // 建造苦工,将其加入侦查编组。 else // 建造兽族步兵,将其加入战士编组。 // ... method sendScouts(position) is if (scouts.length > 0) then // 将侦查编组送到指定位置。 method sendWarriors(position) is if (warriors.length > 5) then // 将战斗编组送到指定位置。 // 子类可以重写部分默认的操作。 class MonstersAI extends GameAI is method collectResources() is // 怪物不会采集资源。 method buildStructures() is // 怪物不会建造建筑。 method buildUnits() is // 怪物不会建造单位。 ``` ## 案例 模板方法模式应用场景十分广泛。 在《Head First》的模板方法模式章节里列举了一个十分具有代表性的例子。 现实生活中,茶和咖啡是随处可见的饮料。冲泡一杯茶或冲泡一杯咖啡的过程是怎样的? 我们来整理一下流程。 - 泡茶: 烧开水 ==> 冲泡茶叶 ==> 倒入杯中 ==> 添加柠檬 - 泡咖啡: 烧开水 ==> 冲泡咖啡 ==> 倒入杯中 ==> 添加糖和牛奶 由以上处理步骤不难发现,准备这两种饮料的处理过程非常相似。我们可以使用模板类方法去限定制作饮料的算法框架。 其中相同的具有共性的步骤(如烧开水、倒入杯中),直接在抽象类中给出具体实现。 而对于有差异性的步骤,则在各自的具体类中给出实现。 【抽象类】 ```java abstract class Beverage { // 模板方法,决定了算法骨架。相当于TemplateMethod()方法 public void prepareBeverage() { boilWater(); brew(); pourInCup(); if (customWantsCondiments()) { addCondiments(); } } // 共性操作,直接在抽象类中定义 public void boilWater() { System.out.println("烧开水"); } // 共性操作,直接在抽象类中定义 public void pourInCup() { System.out.println("倒入杯中"); } // 钩子方法,决定某些算法步骤是否挂钩在算法中 public boolean customWantsCondiments() { return true; } // 特殊操作,在子类中具体实现 public abstract void brew(); // 特殊操作,在子类中具体实现 public abstract void addCondiments(); } ``` 【具体类】 ```java class Tea extends Beverage { @Override public void brew() { System.out.println("冲泡茶叶"); } @Override public void addCondiments() { System.out.println("添加柠檬"); } } class Coffee extends Beverage { @Override public void brew() { System.out.println("冲泡咖啡豆"); } @Override public void addCondiments() { System.out.println("添加糖和牛奶"); } } ``` 【客户端】 ```java public static void main(String[] args) { System.out.println("============= 准备茶 ============="); Beverage tea = new Tea(); tea.prepareBeverage(); System.out.println("============= 准备咖啡 ============="); Beverage coffee = new Coffee(); coffee.prepareBeverage(); } ``` 输出 ``` ============= 准备茶 ============= 烧开水 冲泡茶叶 倒入杯中 添加柠檬 ============= 准备咖啡 ============= 烧开水 冲泡咖啡豆 倒入杯中 添加糖和牛奶 ``` ## 案例 **使用示例:** 模版方法模式在 Java 框架中很常见。 开发者通常使用它来向框架用户提供通过继承实现的、 对标准功能进行扩展的简单方式。 这里是一些核心 Java 程序库中模版方法的示例: - [`java.io.InputStream`](http://docs.oracle.com/javase/8/docs/api/java/io/InputStream.html)、 [`java.io.OutputStream`](http://docs.oracle.com/javase/8/docs/api/java/io/OutputStream.html)、 [`java.io.Reader`](http://docs.oracle.com/javase/8/docs/api/java/io/Reader.html) 和 [`java.io.Writer`](http://docs.oracle.com/javase/8/docs/api/java/io/Writer.html) 的所有非抽象方法。 - [`java.util.AbstractList`](http://docs.oracle.com/javase/8/docs/api/java/util/AbstractList.html)、 [`java.util.AbstractSet`](http://docs.oracle.com/javase/8/docs/api/java/util/AbstractSet.html) 和 [`java.util.AbstractMap`](http://docs.oracle.com/javase/8/docs/api/java/util/AbstractMap.html) 的所有非抽象方法。 - [`javax.servlet.http.HttpServlet`](http://docs.oracle.com/javaee/7/api/javax/servlet/http/HttpServlet.html), 所有默认发送 HTTP 405 “方法不允许” 错误响应的 `do­XXX()`方法。 你可随时对其进行重写。 **识别方法:** 模版方法可以通过行为方法来识别, 该方法已有一个在基类中定义的 “默认” 行为。 ### 消除 if ... else 和重复代码 假设要开发一个购物车功能,针对不同用户进行不同的处理: - 普通用户需要收取运费,运费是商品价格的 10%,无商品折扣; - VIP 用户同样需要收取商品价格 10% 的快递费,但购买两件以上相同商品时,第三件开始享受一定折扣; - 内部用户可以免运费,无商品折扣。 #### 问题 1.0 版本 普通用户购物车 ```java public class NormalUserCart { public Cart process(long userId, Map items) { Cart cart = new Cart(); //把Map的购物车转换为Item列表 List itemList = new ArrayList<>(); items.entrySet().stream().forEach(entry -> { Item item = new Item(); item.setId(entry.getKey()); item.setPrice(Db.getItemPrice(entry.getKey())); item.setQuantity(entry.getValue()); itemList.add(item); }); cart.setItems(itemList); //处理运费和商品优惠 itemList.stream().forEach(item -> { //运费为商品总价的10% item.setDeliveryPrice( item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1"))); //无优惠 item.setCouponPrice(BigDecimal.ZERO); }); //计算纯商品总价 cart.setTotalItemPrice(cart.getItems() .stream() .map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add)); //计算运费总价 cart.setTotalDeliveryPrice( cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add)); //计算总优惠 cart.setTotalDiscount( cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add)); //应付总价=商品总价+运费总价-总优惠 cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount())); return cart; } } ``` VIP 用户购物车 ```java public class VipUserCart { public Cart process(long userId, Map items) { Cart cart = new Cart(); List itemList = new ArrayList<>(); items.entrySet().stream().forEach(entry -> { Item item = new Item(); item.setId(entry.getKey()); item.setPrice(Db.getItemPrice(entry.getKey())); item.setQuantity(entry.getValue()); itemList.add(item); }); cart.setItems(itemList); itemList.stream().forEach(item -> { //运费为商品总价的10% item.setDeliveryPrice( item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1"))); //购买两件以上相同商品,第三件开始享受一定折扣 if (item.getQuantity() > 2) { item.setCouponPrice(item.getPrice() .multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100"))) .multiply(BigDecimal.valueOf(item.getQuantity() - 2))); } else { item.setCouponPrice(BigDecimal.ZERO); } }); cart.setTotalItemPrice(cart.getItems() .stream() .map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add)); cart.setTotalDeliveryPrice( cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add)); cart.setTotalDiscount( cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add)); cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount())); return cart; } } ``` 内部用户购物车 ```java public class InternalUserCart { public Cart process(long userId, Map items) { Cart cart = new Cart(); List itemList = new ArrayList<>(); items.entrySet().stream().forEach(entry -> { Item item = new Item(); item.setId(entry.getKey()); item.setPrice(Db.getItemPrice(entry.getKey())); item.setQuantity(entry.getValue()); itemList.add(item); }); cart.setItems(itemList); itemList.stream().forEach(item -> { //免运费 item.setDeliveryPrice(BigDecimal.ZERO); //无优惠 item.setCouponPrice(BigDecimal.ZERO); }); cart.setTotalItemPrice(cart.getItems() .stream() .map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add)); cart.setTotalDeliveryPrice( cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add)); cart.setTotalDiscount( cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add)); cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount())); return cart; } } ``` 客户端 ```java @GetMapping("wrong") public Cart wrong(@RequestParam("userId") int userId) { String userCategory = Db.getUserCategory(userId); if (userCategory.equals("Normal")) { NormalUserCart normalUserCart = new NormalUserCart(); return normalUserCart.process(userId, items); } if (userCategory.equals("Vip")) { VipUserCart vipUserCart = new VipUserCart(); return vipUserCart.process(userId, items); } if (userCategory.equals("Internal")) { InternalUserCart internalUserCart = new InternalUserCart(); return internalUserCart.process(userId, items); } return null; } ``` > 对比一下代码量可以发现,三种购物车 70% 的代码是重复的。原因很简单,虽然不同类型用户计算运费和优惠的方式不同,但整个购物车的初始化、统计总价、总运费、总优惠和支付价格的逻辑都是一样的。 #### 修正版本 1.0 版本的问题在于:相同的代码应该只在一处出现。 如果我们熟记抽象类和抽象方法的定义的话,这时或许就会想到,是否可以把重复的逻辑定义在抽象类中,三个购物车只要分别实现不同的那份逻辑呢? 其实,这个模式就是模板方法模式。 > 下面展示基于 工厂模式+模板方法模式 优化重复代码。 【抽象类】 ```java public abstract class AbstractCart { public Cart process(long userId, Map items) { Cart cart = new Cart(); List itemList = new ArrayList<>(); items.entrySet().stream().forEach(entry -> { Item item = new Item(); item.setId(entry.getKey()); item.setPrice(Db.getItemPrice(entry.getKey())); item.setQuantity(entry.getValue()); itemList.add(item); }); cart.setItems(itemList); itemList.stream().forEach(item -> { processCouponPrice(userId, item); processDeliveryPrice(userId, item); }); cart.setTotalItemPrice(cart.getItems() .stream() .map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add)); cart.setTotalDeliveryPrice( cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add)); cart.setTotalDiscount( cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add)); cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount())); return cart; } protected abstract void processCouponPrice(long userId, Item item); protected abstract void processDeliveryPrice(long userId, Item item); } ``` 【普通用户购物车】 ```java @Service(value = "NormalUserCart") public class NormalUserCart extends AbstractCart { @Override protected void processCouponPrice(long userId, Item item) { item.setCouponPrice(BigDecimal.ZERO); } @Override protected void processDeliveryPrice(long userId, Item item) { item.setDeliveryPrice(item.getPrice() .multiply(BigDecimal.valueOf(item.getQuantity())) .multiply(new BigDecimal("0.1"))); } } ``` 【VIP 用户购物车】 ```java @Service(value = "VipUserCart") public class VipUserCart extends NormalUserCart { @Override protected void processCouponPrice(long userId, Item item) { if (item.getQuantity() > 2) { item.setCouponPrice(item.getPrice() .multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100"))) .multiply(BigDecimal.valueOf(item.getQuantity() - 2))); } else { item.setCouponPrice(BigDecimal.ZERO); } } } ``` 【内部用户购物车】 ```java @Service(value = "InternalUserCart") public class InternalUserCart extends AbstractCart { @Override protected void processCouponPrice(long userId, Item item) { item.setCouponPrice(BigDecimal.ZERO); } @Override protected void processDeliveryPrice(long userId, Item item) { item.setDeliveryPrice(BigDecimal.ZERO); } } ``` 【客户端】 ```java @GetMapping("right") public Cart right(@RequestParam("userId") int userId) { String userCategory = Db.getUserCategory(userId); AbstractCart cart = (AbstractCart) applicationContext.getBean(userCategory + "UserCart"); return cart.process(userId, items); } ``` ## 与其他模式的关系 - [工厂方法模式](https://refactoringguru.cn/design-patterns/factory-method)是[模板方法模式](https://refactoringguru.cn/design-patterns/template-method)的一种特殊形式。 同时, *工厂方法*可以作为一个大型*模板方法*中的一个步骤。 - [模板方法](https://refactoringguru.cn/design-patterns/template-method)基于继承机制: 它允许你通过扩展子类中的部分内容来改变部分算法。 [策略模式](https://refactoringguru.cn/design-patterns/strategy)基于组合机制: 你可以通过对相应行为提供不同的策略来改变对象的部分行为。 *模板方法*在类层次上运作, 因此它是静态的。 *策略*在对象层次上运作, 因此允许在运行时切换行为。 ## 参考资料 - [《Head First 设计模式》](https://book.douban.com/subject/2243615/) - [《大话设计模式》](https://book.douban.com/subject/2334288/) - [设计模式教程](https://refactoringguru.cn/design-patterns/catalog)