在刚开始接触设计模式,经常弄混行为型模式中的策略模式和模板方法模式。最近工作之外的时间,重新学习了一下,总结出二者的区别,以及何时使用策略模式,何时使用模板方法模式,向大家分享出来。
首先大致讲一下二者的区别,后续具体讲解每种模式,大家再回头看下这部分,会对二者有更好的体会。
策略模式:定义一个算法的抽象,有一组算法实现这个抽象算法,它们可以相互替换,所以说策略模式指的是一组算法,它们之间可以互相替换,使用策略模式一般是为了解决代码中出现的又臭又长的if else分支,使代码满足开闭原则,有利于后续程序扩展。
模板方法模式:定义一个算法的操作框架(抽象类),框架是指算法第一步干嘛,第二步干嘛。。。(模板方法),某个步骤所有子类都一样,则将其在框架中进行实现(具体方法),某些步骤所有子类不一样,就延迟到子类各自去实现(抽象方法)。模板方法模式,也是针对一组算法,这些算法的框架都是一样的,即算法第一步做什么,第二步做什么。算法之间相同的步骤在框架中实现,不同部分由各自自己实现。使用模板方法模式是为了抽取程序中的公共代码,减少重复代码。
策略模式
1、策略模式实现原理:向上转型
2、策略模式的定义
策略模式,定义了一组算法,将每个算法都封装起来,并且使它们之间可以互换。UML结构图如图1所示,时序图如图2所示。
Context:策略上下文,通常策略上下文对象会持有一个真正的策略实现对象,负责调用具体策略上下文对象的方法。
IStrategy:策略抽象,算法的骨架。
ConcreteStrategy:具体策略类,算法的具体实现,IStrategy抽象的实现。
3、策略模式的目的
使程序遵循开闭原则,有利于程序的扩展,以及程序扩展不会影响现有的功能。
4、策略模式案例
商场打折案例,商场场往往根据不同的客户制定不同的报价策略,比如针对新客户不打折扣,针对老客户打9折,针对VIP客户打8折...现在我们要做一个报价管理的模块,针对不同的客户,提供不同的折扣报价,应该怎么做呢?
原始的if else写法
有人肯定会觉得so easy,写几个if else判断一下就可以了嘛。
// 程序1
/**
* 报价管理系统
*/
public class QuoteManager {
/**
* 报价方法
* @param originalPrice 商品原始价格
* @param customType 客户类型
* @return
*/
public BigDecimal quote(BigDecimal originalPrice, String customType){
if ("新客户".equals(customType)) {
System.out.println("抱歉!新客户没有折扣!");
return originalPrice;
}else if ("老客户".equals(customType)) {
System.out.println("恭喜你!老客户打9折!");
originalPrice = originalPrice.multiply(new BigDecimal(0.9)).setScale(2,BigDecimal.ROUND_HALF_UP);// 保留两位小数,四舍五入(若舍弃部分>=0.5,就进位)
return originalPrice;
}else if("VIP客户".equals(customType)){
System.out.println("恭喜你!VIP客户打8折!");
originalPrice = originalPrice.multiply(new BigDecimal(0.8)).setScale(2,BigDecimal.ROUND_HALF_UP);
return originalPrice;
}
//其他人员都是原价
return originalPrice;
}
}
/**
* 客户端
*/
public class Client {
public static void main(String[] args) {
QuoteManager quoteManager = new QuoteManager();
BigDecimal price = quoteManager.quote(new BigDecimal(100), "老客户");
System.out.println("报价为:" +price);
}
}
没有接触过策略模式的新手程序员基本都会想到这种写法,但是仔细观察,这种写法是存在一些问题的。当我们新增加一个客户类型的时候,需要修改源代码增加一个else if分支,一是会使QuoteManager的quote()方法变得越来越臃肿,二是违反了开闭原则,不利于程序的维护,对源代码的改动,可能不小心破坏现有运行正常的功能。
策略模式
// 程序2
/**
* 报价策略接口
*/
public interface IQuoteStrategy {
/**
* 获取最终报价
* @param originalPrice 商品原始价格
* @return
*/
BigDecimal getPrice(BigDecimal originalPrice);
}
/**
* 新客户 报价策略实现类
*/
public class NewCustomerQuoteStrategy implements IQuoteStrategy {
@Override
public BigDecimal getPrice(BigDecimal originalPrice) {
System.out.println("抱歉!新客户没有折扣!");
return originalPrice;
}
}
/**
* 老客户 报价策略实现类
*/
public class OldCustomerQuoteStrategy implements IQuoteStrategy {
@Override
public BigDecimal getPrice(BigDecimal originalPrice) {
System.out.println("恭喜!老客户享有9折优惠!");
originalPrice = originalPrice.multiply(new BigDecimal(0.9)).setScale(2,BigDecimal.ROUND_HALF_UP);
return originalPrice;
}
}
/**
* VIP客户 报价策略实现类
*/
public class VIPCustomerQuoteStrategy implements IQuoteStrategy {
@Override
public BigDecimal getPrice(BigDecimal originalPrice) {
System.out.println("恭喜!VIP客户享有8折优惠!");
originalPrice = originalPrice.multiply(new BigDecimal(0.8)).setScale(2,BigDecimal.ROUND_HALF_UP);
return originalPrice;
}
}
/**
*
* 报价上下文
*/
public class QuoteContext {
private IQuoteStrategy quoteStrategy;
//通过构造器注入具体的报价策略
public QuoteContext(IQuoteStrategy quoteStrategy){
this.quoteStrategy = quoteStrategy;
}
// 调用具体报价策略
public BigDecimal getPrice(BigDecimal originalPrice){
return quoteStrategy.getPrice(originalPrice);
}
}
/**
* 客户端
*/
public class Client {
public static void main(String[] args) {
//1.创建老客户的报价策略
IQuoteStrategy oldQuoteStrategy = new OldCustomerQuoteStrategy();
//2.创建报价上下文对象,并设置具体的报价策略
QuoteContext quoteContext = new QuoteContext(oldQuoteStrategy);
//3.调用报价上下文的方法
BigDecimal price = quoteContext.getPrice(new BigDecimal(100));
System.out.println("报价为:" +price);
}
}
此时,商场营销部新推出了一个客户类型--MVP用户,可以享受折扣7折优惠,我们只要新增一种MVP客户的报价策略,然后客户端调用的时候,创建这个新增的报价策略实现,并设置到策略上下文就可以了,对原来已经实现的代码没有任何的改动。
// 程序3
/**
* 新增MVP客户报价策略
* MVP客户 报价策略实现类
*/
public class MVPCustomerQuoteStrategy implements IQuoteStrategy {
@Override
public BigDecimal getPrice(BigDecimal originalPrice) {
System.out.println("哇偶!MVP客户享受7折优惠!!!");
originalPrice = originalPrice.multiply(new BigDecimal(0.7)).setScale(2,BigDecimal.ROUND_HALF_UP);
return originalPrice;
}
}
/**
* 客户端
*/
public class Client {
public static void main(String[] args) {
//创建MVP客户的报价策略
IQuoteStrategy mvpQuoteStrategy = new MVPCustomerQuoteStrategy();
//创建报价上下文对象,并设置具体的报价策略
QuoteContext quoteContext = new QuoteContext(mvpQuoteStrategy);
//调用报价上下文的方法
BigDecimal price = quoteContext.getPrice(new BigDecimal(100));
System.out.println("报价为:" +price);
}
}
5、策略模式优缺点
优点:
遵循开闭原则,一是程序简洁不臃肿,二是有利于程序的扩展,程序的扩展不会影响现有正常的功能。
缺点:
1、客户端必须知道所有的策略类,并自行决定使用哪一个策略类(见程序2,Client类的第3行代码)。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。换言之,策略模式只适用于客户端知道算法或行为的情况。
2、由于策略模式把每个具体的策略实现都单独封装成为类,如果备选的策略很多的话,那么对象的数目就会很可观,会造成类爆炸的问题。
模板方法模式
1、模板方法模式实现原理:继承
2、模板方法模式的定义
定义一个算法中的操作框架,将子类之间相同的步骤在框架中实现,不同的步骤交给子类自己去实现,使得子类可以不改变算法的结构即可重定义该算法的某些特定步骤。UML结构图如图3所示,模板方法模式代码结构如程序4所示。
// 程序4
/**
* 抽象类(算法的框架)
*/
public abstract class TemplateClass {
// 模板方法
public void templateMethod(){
// 算法第一步
abstractMethod();
// 算法第二步
concreteMethod();
// 算法第三步
abstractMethod();
}
// 具体方法
private final void concreteMethod(){
// do something
}
// 抽象方法
protected abstract void abstractMethod();
// 钩子方法
protected void hookMethod(){
}
}
/**
* 子类(具体的算法)
*/
public class ConcreteClass extends TemplateClass{
// 实现父类的抽象方法
@Override
protected void abstractMethod() {
// do something
}
// 实现父类的钩子方法
@Override
protected void hookMethod(){
// do something
}
}
TemplateClass:抽象类,算法的框架。
ConcreteClass:子类,具体的算法。
templateMethod():模板方法,定义在抽象类中,把基本方法组合在一起形成一个算法的框架,在抽象类中定义并实现,子类直接继承即可。
abstractMethod():抽象方法,在抽象类中声明,由具体子类实现。在Java语言里抽象方法以abstract关键字标示。
concreteMethod():具体方法,在抽象类中声明并实现。
hookMethod():钩子方法,在抽象类中声明并实现,子类会重写它。钩子方法的作用,可以让具体类灵活控制算法的执行(当我们不想让某个步骤执行的话,用钩子方法就可以达到这个目的,见下面的例子),使算法更加灵活。
3、模板方法模式的目的
减少重复代码,见定义中描述的,将子类之间相同的算法步骤在框架中实现,子类就不需要自己实现了,减少子类之间的重复代码。
4、模板方法模式案例
业务场景:银行计算存款利息场景,银行交易系统需要支持两种存款账号,活期存款(Demand Deposite)账号和定期存款(Time Deposite)账号,这两种账号的存款利息计算方式是不同的。
不使用模板方法策略模式的写法:
程序设计UML图如图4所示,代码见程序5。
// 程序5
/**
* 活期存款
*/
public class DemandDepositeAccount {
// 计算活期存款的利息总额
public double calculateDemandDepositeInterest(){
double demandDepositeAccount = getDemandDepositeAccount();
double demandDepositeInterestRate = getDemandDepositeInterestRate();
return demandDepositeAccount * demandDepositeInterestRate;
}
public double getDemandDepositeAccount() {
return 4000;// 活期存款总额是4000元。
}
public double getDemandDepositeInterestRate() {
return 0.04;// 活期存款利息率是0.04。
}
}
/**
* 定期存款
*/
public class TimeDepositeAccount {
// 计算定期存款的利息总额
public double calculateTimeDepositeInterest(){
double timeDepositeAccount = getTimeDepositeAccount();
double timeDepositeInterestRate = getTimeDepositeInterestRate();
return timeDepositeAccount * timeDepositeInterestRate;
}
public double getTimeDepositeAccount() {
return 6000;// 定期存款总额是6000元。
}
public double getTimeDepositeInterestRate() {
return 0.06;// 定期存款利息率是0.06。
}
}
/**
* 客户端
*/
public class Client {
public static void main(String[] args) {
// 计算活期存款的存款总额
DemandDepositeAccount demandDepositeAccount = new DemandDepositeAccount();
double demandDepositeInterest = demandDepositeAccount.calculateDemandDepositeInterest();
System.out.println("活期存款的利息总额:" + demandDepositeInterest + "元");
// 计算定期存款的存款总额
TimeDepositeAccount timeDepositeAccount = new TimeDepositeAccount();
double timeDepositeInterest = timeDepositeAccount.calculateTimeDepositeInterest();
System.out.println("定期存款的利息总额:" + timeDepositeInterest + "元");
}
}
使用模板方法策略模式的写法:
我发现原始写法,DemandDepositeAccount的calculateDemandDepositeInterest()方法和TimeDepositeAccount的calculateTimeDepositeInterest()大致逻辑是一样的,都是先获取存款总额,然后获取利息率,最后存款总额乘以利息率得到利息总额,有没有什么办法来解决这个代码重复的问题呢?
使用模板方法模式进行优化,将利息计算这个算法的框架提取出来作为一个模板,两种存款模式直接继承这个模板,无需自己再写一遍了。程序设计UML图如图5所示,代码见程序6。
// 程序6
/**
* 抽象类
*/
public abstract class Account {
// 计算利息总额,算法框架
public double calculateInterest(){
double depositAccount = getDepositAccount();
double interestRate = getInterestRate();
return depositAccount * interestRate;
}
// 获取存款总额
public abstract double getDepositAccount();
// 获取利息率
public abstract double getInterestRate();
}
/**
* 子类1,表示活期存款
*/
public class DemandDepositeAccount extends Account {
@Override
public double getDepositAccount() {
return 4000;// 活期存款总额是4000元。
}
@Override
public double getInterestRate() {
return 0.04;// 活期存款利息率是0.04。
}
}
/**
* 子类2,表示定期存款
*/
public class TimeDepositeAccount extends Account {
@Override
public double getDepositAccount() {
return 6000;// 定期存款总额是6000元。
}
@Override
public double getInterestRate() {
return 0.06;// 定期存款利息率是0.06。
}
}
/**
* 客户端
*/
public class Client {
public static void main(String[] args) {
// 计算活期存款的存款总额
Account demandDepositeAccount = new DemandDepositeAccount();
double demandDepositeInterest = demandDepositeAccount.calculateInterest();
System.out.println("活期存款的利息总额:" + demandDepositeInterest + "元");
// 计算定期存款的存款总额
Account timeDepositeAccount = new TimeDepositeAccount();
double timeDepositeInterest = timeDepositeAccount.calculateInterest();
System.out.println("定期存款的利息总额:" + timeDepositeInterest + "元");
}
}
5、钩子方法
钩子就是给子类一个授权,让子类来决定模板方法的逻辑执行,有一些子类不想执行模板方法中的某一步,就可以使用钩子进行控制。拿做西红柿炒蛋这个例子来讲解一下钩子方法,比如在炒西红柿鸡蛋的时候,由子类去决定是否要加调料。见程序7所示。
// 程序7
/**
* 抽象类
*/
public abstract class Cook {
// 钩子方法,让子类决定是否放油,默认放油
public boolean isAddOil(){
return true;
}
// 做西红柿炒蛋的步骤封装成一个模板
public final void cook(){
if (isAddOil()){
this.addOil();
}
this.addEgg();
this.addTomato();
}
// 放油
public abstract void addOil();
// 放鸡蛋
public abstract void addEgg();
// 放西红柿
public abstract void addTomato();
}
/**
* 菜鸟厨师做西红柿炒鸡蛋
*/
public class NoviceCook extends Cook {
private boolean addOilFlag = true; // 默认放油
/**
* 由子类决定是否放油
* @param addOilFlag
*/
public void setAddOilFlag(boolean addOilFlag){
this.addOilFlag = addOilFlag;
}
@Override
public boolean isAddOil(){
return this.addOilFlag;
}
@Override
public void addOil() {
System.out.println("菜鸟:放十斤油");
}
@Override
public void addEgg() {
System.out.println("菜鸟:鸡蛋壳掉到锅里了");
}
@Override
public void addTomato() {
System.out.println("菜鸟:西红柿没有切块");
}
}
/**
* 大厨做西红柿炒蛋
*/
public class ChefCook extends Cook {
private boolean addOilFlag = true;
public void setAddOilFlag(boolean addOilFlag){
this.addOilFlag = addOilFlag;
}
@Override
public boolean isAddOil(){
return this.addOilFlag;
}
@Override
public void addOil() {
System.out.println("大厨:放适量油");
}
@Override
public void addEgg() {
System.out.println("大厨:放适量鸡蛋");
}
@Override
public void addTomato() {
System.out.println("大厨:放适量西红柿");
}
}
/**
* 客户端
*/
public class Client {
public static void main(String[] args) {
System.out.println("-----菜鸟厨师做西红柿炒蛋-----");
NoviceCook noviceCook = new NoviceCook();
// 菜鸟厨师第一次做西红柿炒蛋,没放油
noviceCook.setAddOilFlag(false);
noviceCook.cook();
System.out.println("-----大厨做西红柿炒蛋-----");
ChefCook chefCook = new ChefCook();
chefCook.cook();
}
}
打印结果:
-----菜鸟厨师做西红柿炒蛋-----
菜鸟:鸡蛋壳掉到锅里了
菜鸟:西红柿没有切块
-----大厨做西红柿炒蛋-----
大厨:放适量油
大厨:放适量鸡蛋
大厨:放适量西红柿
6、模板方法模式优缺点
优点:
1、将多个子类中,重复的代码抽取成模板方法放在抽象类中,子类只需要实现彼此不同的方法。以此来实现代码复用,减少重复代码。
2、封装不变部分,扩展可变部分。把认为不变部分的算法封装到抽象类中实现,而可变部分则通过继承来让子类扩展。
缺点:
1、算法骨架需要改变时需要修改抽象类。
2、按照设计习惯,抽象类负责声明最抽象、最一般的事物属性和方法,实现类负责完成具体的事物属性和方法,但是模板方式正好相反,子类执行的结果影响了父类的结果,会增加代码阅读的难度。
总结
何时使用策略模式?何时使用模板方法模式?
程序在最开始设计阶段,基本很少用到设计模式,设计模式一般会在程序出现一些问题时,比如重复代码太多、增加新的代码很容易影响线运行正常的功能,才会考虑使用设计模式对程序做重构。策略模式和模板方法模式一般在程序重构过程中应用,那么二者的应用场景分别是什么呢?
当我们为了程序有利于以后的扩展,扩展不会影响现有的功能,此时考虑使用策略模式。
当我们是为了抽离程序的公共代码,减少代码重复,此时考虑使用模板方法模式。