策略模式

大家一定都使用过电子地图。在地图中输入出发地和目的地,然后再选取你的出行方式,就可以计算出最优线路以及预估的时长。出行方式有驾车、公交、步行、骑行等。出行方式不同,计算的线路和时间当然也不同。
其实出行方式换个词就是出行策略。而策略模式就是针对此类问题的设计模式。生活中这种例子太多了,比如购物促销打折的策略、计算税费的策略等等。相应的策略模式也是一种常用的设计模式。本节我们会以电子地图为例,比较工厂模式和策略模式,讲解策略模式的原理。最后结合工厂模式改造策略模式的代码实现,以达到更高的设计目标。

1. 实现策略模式

接下来我们就以电子地图为例,讲解如何用策略模式实现。不过先别着急,上一节我们学习了工厂模式,看起来电子地图也可以用工厂模式来实现。所以我们先来看看用工厂模式如何实现。下面的例子为了方便展示,接口入参只有出行方式,省略了出发地和目的地。计算结果是预估时长。

1.1 工厂模式实现电子地图

首先我们需要一个策略接口,不同策略实现该接口。再搭配一个策略工厂。客户端代码只需要根据用户的出行方式,让工厂返回具体实现即可,由具体的实现来提供算法计算。以工厂模式实现的电子地图代码如下。
TravelStrategy接口代码:

public interface TravelStrategy {
    int calculateMinCost();
}

TravelStrategy接口的实现代码:

public class SelfDrivingStrategy implements TravelStrategy {
    @Override
    public int calculateMinCost() {
        return 30;
    }
}

TravelStrategyFactory代码:

public class TravelStrategyFactory {
    public TravelStrategy createTravelStrategy(String travelWay) {
        if ("selfDriving".equals(travelWay)) {
            return new SelfDrivingStrategy();
        } if ("bicycle".equals(travelWay)) {
            return new BicycleStrategy();
        } else {
            return new PublicTransportStrategy();
        }
    }
}

TravelService对外提供计算方法,通过工厂生成所需要的 strategy。代码如下:

public class TravelService {
    private TravelStrategyFactory travelStrategyFactory = new TravelStrategyFactory();

    public int calculateMinCost(String travelWay) {
        TravelStrategy travelStrategy = travelStrategyFactory.createTravelStrategy(travelWay);
        return travelStrategy.calculateMinCost();
    }
}

代码结构和我们上一节讲解的音乐推荐器几乎一模一样。看似也很好地解决了我们的设计问题。接下来我们看看如何用策略模式解决这个问题,然后我们再对两种模式做对比。

1.2 策略模式实现电子地图

使用策略模式,需要增加一个策略上下文类(Context)。Context类持有策略实现的引用,并且对外提供计算方法。Context类根据持有策略的不同,实现不同的计算逻辑。客户端代码只需要调用 Context 类的计算方法即可。如果想切换策略实现,那么只需要改变Context类持有的策略实现即可。
TravelStrategy 接口和实现的代码不变,请参照上面工厂模式中给出的代码。其他代码如下:
StrategyContext 类:

public class StrategyContext {
    private TravelStrategy strategy;

    public StrategyContext(TravelStrategy strategy) {
        this.strategy = strategy;
    }

    public int calculateMinCost(){
        return strategy.calculateMinCost();
    }

StrategyContext 持有某种 TravelStrategy 的实现,它对外提供的calculateMinCost 方法,实际是对 TravelStrategy 做了一层代理。想切换不同算法的时候,只需更改 StrategyContext 持有的 TravelStrategy 实现。

TravelService 对外提供计算方法,代码如下:

public class TravelService {
    private StrategyContext strategyContext;

    public int calculateMinCost(String travelWay){

        if ("selfDriving".equals(travelWay)) {
            strategyContext = new StrategyContext(new SelfDrivingStrategy());
        } if ("bicycle".equals(travelWay)) {
            strategyContext = new StrategyContext(new BicycleStrategy());
        } else {
            strategyContext = new StrategyContext(new PublicTransportStrategy());
        }

        return strategyContext.calculateMinCost();
    }
}

可以看到 TravelService 中只会和 Context 打交道,初始化 Context 时,根据不同的出行方式,设置不同的策略。看到这里你是不是会有疑问,使用工厂模式消除了客户端代码的条件语句。怎么使用策略模式,条件语句又回来了?别急,我们继续向下看。

最后我们看一下策略模式的类图:
图片描述

2. 策略模式优缺点

2.1 优点

  1. 使用策略模式,可以根据策略接口,定义一系列可供复用的算法或者行为;
  2. 调用方只需要持有Context的引用即可。而无需知道具体的策略实现。满足迪米特法则;
  3. Context 在策略的方法之外可以做一些通用的切面逻辑。

GOF的《设计模式》著作中认为策略模式可以消除一些条件语句,我对此持怀疑态度。正如上面的例子,虽然由于Context在初始化的时候已经指定了策略实现,在计算逻辑中不需要根据条件选择逻辑分支。但是,客户端代码在初始化Context的时候,如何判断应该传入哪个策略实现呢?其实在客户端代码或者别的地方还是缺少不了条件判断。所以这里消除条件语句,只是针对算法逻辑的条件判断。

第一个优点是策略模式解决的核心问题。但其实工厂模式也是可以做到的。第二点,我认为很重要,客户端代码只需要和 Context 打交道即可,避免了和不同策略类、工厂类的接触。工厂模式中,客户端代码需要知道工厂类和产品类,两个类。正好复习一下迪米特法则,如果两个类没有必要直接通信,那么两个类就没有必要相互作用。可以通过第三方来间接调用。

2.2 缺点

  1. 客户端代码需要知道不同的策略以及如何选择策略。因此可以看到上面的客户端代码有着丑陋的条件判断;
  2. 由于策略类实现同样的接口,所以参数列表要保持一致,但可能并不是所有的策略都需要全部参数。

3. 策略模式与工厂模式结合使用

针对第一个缺点。我们可以通过策略模式与工厂模式结合使用来改进。通过进一步封装,消除客户端代码的条件选择。

我们修改一下StrategyContext类,代码如下:

public class StrategyContext {
    private TravelStrategy strategy;

    public StrategyContext(String travelWay) {
        if ("selfDriving".equals(travelWay)) {
            strategy = new SelfDrivingStrategy();
        } if ("bicycle".equals(travelWay)) {
            strategy = new BicycleStrategy();
        } else {
            strategy = new PublicTransportStrategy();
        }
    }

    public int calculateMinCost(){
        return strategy.calculateMinCost();
    }
}

可以看到我们初始化的逻辑和工厂的逻辑很相似。这样条件判断就提炼到 Context 类中了。而客户端代码将会简洁很多,只需要在初始化 StrategyContext 时,传入相应的出行方式即可。代码如下:

public class TravelService {
    private StrategyContext strategyContext;

    public int calculateMinCost(String travelWay){

        strategyContext = new StrategyContext(travelWay);

        return strategyContext.calculateMinCost();
    }
}

改进后,客户端代码现在已经完全不知道策略对象的存在了。条件判断也被消除了。其实很多时候我们都是通过搭配不同设计模式来达到我们的设计目标的。

策略+工厂模式类图如下:
图片描述

4. 策略模式适用场景

当存在多种逻辑不同,但属于同一类型的行为或者算法时,可以考虑使用策略模式。以此来消除你算法代码中的条件判断。同时让你的代码满足多种设计原则。

很多时候,工厂模式和策略模式都可以为你解决同类问题。但你要想清楚,你想要的是一个对象,还是仅仅想要一个计算结果。如果你需要的是一个对象,并且想用它做很多事情。那么请使用工厂模式。而你仅仅想要一个特定算法的计算结果,那么请使用策略模式。

策略模式属于对象行为模式,而工厂属于创建型模式。策略模式和工厂模式对比如下:
图片描述

5. 小结

策略模式解决的问题是如何封装可供复用的算法或者行为。策略模式满足了单一职责、开闭、迪米特法则、依赖倒转等原则。我们一定想清楚策略模式的适用场景,否则某些时候你会搞不清到底用工厂模式还是策略模式。最后提醒大家,设计模式很多时候都是混合使用,我们不应该局限于使用某一种设计模式来解决问题。