设计模式之模板方法模式 意图 模板方法模式 (Template Method) 是一种行为设计模式, 它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。
模板方法模式是所有模式中最为常见的几个模式之一,是基于继承 的代码复用 的基本技术。,没有关联关系。 因此,在模板方法模式的类结构图中,只有继承关系 。
模板方法模式需要开发抽象类和具体子类的设计师之间的协作。一个设计师负责给出一个算法的轮廓和骨架,另一些设计师则负责给出这个算法的各个逻辑步骤。
代表这些具体逻辑步骤的方法称做**基本方法(primitive method);而将这些基本方法汇总起来的方法叫做 模板方法(template method)**,这个设计模式的名字就是从此而来。
在模板方法模式中,首先父类会定义一个算法的框架,即实现算法所必须的所有方法。
适用场景
当你只希望客户端扩展某个特定算法步骤, 而不是整个算法或其结构时, 可使用模板方法模式。
当多个类的算法除一些细微不同之外几乎完全一样时, 你可使用该模式。 但其后果就是, 只要算法发生变化, 你就可能需要修改所有的类。
结构
结构说明
抽象类 (AbstractClass) 会声明作为算法步骤的方法, 以及依次调用它们的实际模板方法。 算法步骤可以被声明为 抽象
类型, 也可以提供一些默认实现。
具体类 (ConcreteClass) 可以重写所有步骤, 但不能重写模板方法自身。
结构代码范式 AbstractClass : 抽象类,定义并实现一个模板方法。这个模板方法定义了算法的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类去实现。顶级逻辑也有可能调用一些具体方法。
1 2 3 4 5 6 7 8 9 abstract class AbstractClass { public abstract void PrimitiveOperation1 () ; public abstract void PrimitiveOperation2 () ; public void TemplateMethod () { PrimitiveOperation1(); PrimitiveOperation2(); } }
ConcreteClass : 实现实现父类所定义的一个或多个抽象方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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" ); } }
客户端
1 2 3 4 5 6 7 8 public class TemplateMethodPattern { public static void main (String[] args) { AbstractClass objA = new ConcreteClassA (); AbstractClass objB = new ConcreteClassB (); objA.TemplateMethod(); objB.TemplateMethod(); } }
伪代码 本例中的模板方法 模式为一款简单策略游戏中人工智能的不同分支提供 “框架”。
一款简单游戏的 AI 类。
游戏中所有的种族都有几乎同类的单位和建筑。 因此你可以在不同的种族上复用相同的 AI 结构, 同时还需要具备重写一些细节的能力。 通过这种方式, 你可以重写半兽人的 AI 使其更富攻击性, 也可以让人类侧重防守, 还可以禁止怪物建造建筑。 在游戏中新增种族需要创建新的 AI 子类, 还需要重写 AI 基类中所声明的默认方法。
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 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》的模板方法模式章节里列举了一个十分具有代表性的例子。
现实生活中,茶和咖啡是随处可见的饮料。冲泡一杯茶或冲泡一杯咖啡的过程是怎样的?
我们来整理一下流程。
泡茶: 烧开水 ==> 冲泡茶叶 ==> 倒入杯中 ==> 添加柠檬
泡咖啡: 烧开水 ==> 冲泡咖啡 ==> 倒入杯中 ==> 添加糖和牛奶
由以上处理步骤不难发现,准备这两种饮料的处理过程非常相似。我们可以使用模板类方法去限定制作饮料的算法框架。
其中相同的具有共性的步骤(如烧开水、倒入杯中),直接在抽象类中给出具体实现。
而对于有差异性的步骤,则在各自的具体类中给出实现。
【抽象类】
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 abstract class Beverage { 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 () ; }
【具体类】
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 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("添加糖和牛奶" ); } }
【客户端】
1 2 3 4 5 6 7 8 9 10 11 public static void main (String[] args) { System.out.println("============= 准备茶 =============" ); Beverage tea = new Tea (); tea.prepareBeverage(); System.out.println("============= 准备咖啡 =============" ); Beverage coffee = new Coffee (); coffee.prepareBeverage(); }
输出
1 2 3 4 5 6 7 8 9 10 = = = = = = = = = = = = = 准备茶 = = = = = = = = = = = = = 烧开水 冲泡茶叶 倒入杯中 添加柠檬 = = = = = = = = = = = = = 准备咖啡 = = = = = = = = = = = = = 烧开水 冲泡咖啡豆 倒入杯中 添加糖和牛奶
案例 使用示例: 模版方法模式在 Java 框架中很常见。 开发者通常使用它来向框架用户提供通过继承实现的、 对标准功能进行扩展的简单方式。
这里是一些核心 Java 程序库中模版方法的示例:
识别方法: 模版方法可以通过行为方法来识别, 该方法已有一个在基类中定义的 “默认” 行为。
消除 if … else 和重复代码 假设要开发一个购物车功能,针对不同用户进行不同的处理:
普通用户需要收取运费,运费是商品价格的 10%,无商品折扣;
VIP 用户同样需要收取商品价格 10% 的快递费,但购买两件以上相同商品时,第三件开始享受一定折扣;
内部用户可以免运费,无商品折扣。
问题 1.0 版本 普通用户购物车
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 42 public class NormalUserCart { public Cart process (long userId, Map<Long, Integer> items) { Cart cart = new Cart (); List<Item> 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( 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 用户购物车
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 42 public class VipUserCart { public Cart process (long userId, Map<Long, Integer> items) { Cart cart = new Cart (); List<Item> 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( 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; } }
内部用户购物车
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 public class InternalUserCart { public Cart process (long userId, Map<Long, Integer> items) { Cart cart = new Cart (); List<Item> 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; } }
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @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 版本的问题在于:相同的代码应该只在一处出现。
如果我们熟记抽象类和抽象方法的定义的话,这时或许就会想到,是否可以把重复的逻辑定义在抽象类中,三个购物车只要分别实现不同的那份逻辑呢?
其实,这个模式就是模板方法模式。
下面展示基于 工厂模式+模板方法模式 优化重复代码。
【抽象类】
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 public abstract class AbstractCart { public Cart process (long userId, Map<Long, Integer> items) { Cart cart = new Cart (); List<Item> 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) ; }
【普通用户购物车】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @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 用户购物车】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @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); } } }
【内部用户购物车】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @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); } }
【客户端】
1 2 3 4 5 6 @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); }
与其他模式的关系
工厂方法模式 是模板方法模式 的一种特殊形式。 同时, 工厂方法 可以作为一个大型模板方法 中的一个步骤。
模板方法 基于继承机制: 它允许你通过扩展子类中的部分内容来改变部分算法。 策略模式 基于组合机制: 你可以通过对相应行为提供不同的策略来改变对象的部分行为。 模板方法 在类层次上运作, 因此它是静态的。 策略 在对象层次上运作, 因此允许在运行时切换行为。
参考资料