小酌重构系列[15]——策略模式代替分支

前言

在一些较为复杂的业务中,客户端须要依据条件,执行相应的行为或算法。在实现这些业务时,咱们可能会使用较多的分支语句(switch case或if else语句)。使用分支语句,意味着“变化”和“重复”,每一个分支条件都表明一个变化,每一个分支逻辑都是类似行为或算法的重复。
当追加新的条件时,咱们须要追加分支语句,并追加相应的行为或算法。html

上一篇文章“使用多态代替条件判断”中,咱们讲到它能够处理这些“变化”和“重复”,今天我将介绍一种新的方式——使用策略模式代替分支,它也能处理这些“变化”和“重复”。在讲这个策略以前,咱们先来看一则小故事。算法

小商城的运营

某小型在线商城,有3位核心成员,他们分别是CTO、COO和CEO。
CTO:小A,负责撸代码,以及维护商城系统。
COO:小B,负责吹牛忽悠,以及市场推广和运营。
CEO:小C,负责拉皮条,以及看着你俩干活。设计模式

在这个故事中,假定你就是小A,头衔CTO(谁让你既不会拉皮条,也不会吹牛忽悠呢)。工具

第一幕

某一天,小B策划了一个促销活动,免费给用户发放一些优惠券,用户在消费满必定金额后,可使用这些优惠券抵扣。
假定如今有两个优惠活动——“满99减20,满199减50”。网站

每一个用户要买的东西和花费的金额是不一样的,根据不一样的消费金额,系统须要断定使用什么优惠券。
面对这样一个场景,你说这不是忒简单了嘛,而后唰唰唰2分钟就撸完了这串代码。spa

public decimal CalculateAmount(decimal amount)
{
    if (amount < 99)
        return amount;
    else if (amount < 200)
        return amount - 20;
    else
        return amount - 50;
}

小B看了后,说道:“哇,这么快就弄完了,不愧是我们公司的CTO,赶忙上线吧!”。设计

第二幕

第一天,小B根据交易数据分析得知,自从上了优惠券后(我是优惠券,谁要上我?),商城的交易额增加了不少,并且有较多用户的订单金额居然超过了200。
为了回馈这部分“高端”用户的热情和贡献,商城决定加大优惠力度,因而小B追加了两项优惠活动:满299减80,满399减120。(好吧,这和街边卖场的大叔吆喝是同样样的,原价500多的真皮皮鞋、钱包,如今只要50元,全场50元,统统50元…!)code

看到这新出现的场景,你想这不是分分钟搞定的事儿?因而你修改了CalculateAmount()方法。htm

public decimal CalculateAmount(decimal amount)
{
    if (amount < 99)
        return amount;
    else if (amount < 200)
        return amount - 20;
    else if(amount < 300)
        return amount - 50;
    else if (amount < 400)
        return amount - 80;
    else
        return amount - 120;
}

第三幕

次日,小B又提了一个要求:“有些用户的会员等级比较高,为了给用户一种“老子是上帝”的感受,能够为这些高级会员打一些折扣。”blog

铜牌会员无折扣,银牌会员打98折,金牌会员打95折,砖石会员打9折。

这时,你内心嘀咕了一声,干吗不早说? 改吧,反正也不是啥难事儿。

public decimal CalculateAmount(Customer customer, decimal amount)
{
    // 优惠券减免
    if (amount < 99)
    {
    }
    else if (amount <200)
        amount -= 20;
    else if(amount <300)
        amount -= 50;
    else if (amount < 400)
        amount -= 80;
    else
        amount -= 120;

    // 会员等级减免
    switch (customer.MemberLevel)
    {
        case MemberLevel.Silver:
            amount *= 0.98m;
            break;
        case MemberLevel.Gold:
            amount *= 0.95m;
            break;
        case MemberLevel.Diamond:
            amount *= 0.90m;
            break;
    }
    return amount;
}

小B拍了拍你的肩膀,意味深长地说:“网站的维护就全靠你了,我们会好起来的,赚了钱你们一块儿分!”。

第四幕

三天以后,小B说这几天商城销量很是不错,我们应该赚了很多钱。可是用户如今的激情也降下去了,我们能够撤回这些优惠了,商品都按原价来卖吧,麻烦你把优惠政策给撤销吧。

你幽幽地叹了一口气:“好吧,如今改(说好的赚钱你们一块儿分的呢,这茬子事儿你咋不提?一万匹草泥马疯狂地踏过)。”

因而你删除了调用这个方法的代码。

第五幕

一个月后,小B又来找你了:“如今又到了购物的旺季,淘宝京东开始作活动了,优惠力度还挺大,我们也在这股购物潮里凑个热闹吧。这一次,咱们有如下几项业务规则,和上次的不一样,也比上次的复杂一些,你听我向你娓娓道来啊。”

1. aaa规则
2. bbb规则
3. ccc规则
4. ddd规则

10. xxx规则

听完这些后,你崩溃了,你这个小B(一语双关),怎么一会儿提出这么多业务,还不带重样的,我从何改起啊?

设计模式简介

听完这个故过后,你能了解到什么呢?咱们用两个词来归纳,也就是本文开头提到的“变化”和“重复”。
大多数的“变化”都会伴随着“重复”,这些“重复”的表现形式可能不同,但它们的本质是相似的。

不管是生活仍是工做,变化是和重复都是无处不在的。
在工做中,咱们处于某一个岗位,咱们天天的工做任务都会有变化,咱们使用近乎相同的方式处理这些工做任务。

代码中也会出现不少“变化”和“重复”,咱们该如何应对呢?。
你能够借用一些设计模式,怎么用设计模式咱先不说,咱们先粗略地解一下设计模式。

设计模式是对软件设计中广泛存在(反复出现)的各类问题,所提出的解决方案。

设计模式咱们把它拆分红两个来看,“设计”和“模式”。
“设计”就是设计,对于软件系统来讲,即分析问题并解决问题的过程。
“模式”是指事物的标准。在软件领域,每一个人面临的问题是不一样的,虽然不一样的问题有不一样的标准,但不少问题本质上是相似的。

设计模式更多的是软件层面的,而非业务层面的。
即便你用了设计模式,你也不必定能解决业务上的问题。
即便你不用设计模式,业务上的问题你也许能经过其余途径解决。

设计模式怎么用,在这里我也没法给一个肯定的答案。
我我的的见解是,尽可能作到“心中无模式”。最关键的是,你应该直面问题的本质,寻找问题最有效的解决方式,不要一遇到问题,就夸夸其谈地使用某某设计模式。真切地从用户角度去出发,去剖析问题的本质,并提出合理的设计和解决方案。当问题得以解决,你回顾这个过程时,你会发现不少模式你是天然而然地用到了。不要特别在乎设计模式,这可能会让你忽视问题的本质,即便你把GOF的23种设计模式滚瓜烂熟,你解决问题的能力也不会有所提高。

PS:为了描述“变化”和“重复”,我使用了小商城运营这个故事。这个故事里面有些不恰当的地方,搞在线购物的是不会这么去设计优惠券和折扣的。

策略模式

正式进入今天的主题吧,这篇文章要提到的设计模式是“策略模式”。

定义

策略模式是设计模式里面较为简单的一种,它的定义以下:

The Strategy Pattern defines a family of algorithms,encapsulates each one,and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

策略模式定义了一系列的算法,并将每个算法封装起来,并且使它们还能够相互替换。
在策略模式中,算法是其中的“变化点”,策略模式让算法独立于使用它的客户而独立变化。

组成部分

策略模式有4个部分组成:

1. 客户端:指定使用哪一种策略,它依赖于环境

2. 环境:依赖于算法接口,并为客户端提供算法接口的实例

3. 抽象策略:定义公共的算法接口

4. 策略实现:算法接口的具体实现

下面这幅图诠释了这4个组成部分:

image

注意:在策略模式中,策略是由用户选择的,这意味着具体策略可能都要暴露给客户端,可是咱们能够经过“分解依赖”来隐藏策略细节。

示例

重构前

该示例是一家物流公司根据State计算物流运费的场景,ShippingInfo类的CalculateShippingAmount()方法,会按照不一样的State计算出运输费用。物流公司最开始只处理3个State的运输业务,分别是Alaska, NewYork和Florida。

image

public class ClientCode
{
    public decimal CalculateShipping()
    {
        ShippingInfo shippingInfo = new ShippingInfo();
        return shippingInfo.CalculateShippingAmount(State.Alaska);
    }
}

public enum State
{
    Alaska,
    NewYork,
    Florida
}

public class ShippingInfo
{
    public decimal CalculateShippingAmount(State shipToState)
    {
        switch (shipToState)
        {
            case State.Alaska:
                return GetAlaskaShippingAmount();
            case State.NewYork:
                return GetNewYorkShippingAmount();
            case State.Florida:
                return GetFloridaShippingAmount();
            default:
                return 0m;   
        }
    }

    private decimal GetAlaskaShippingAmount()
    {
        return 15m;
    }

    private decimal GetNewYorkShippingAmount()
    {
        return 10m;
    }

    private decimal GetFloridaShippingAmount()
    {
        return 3m;
    }
}

这段代码使用了switch case分支语句,每一个State都有相应的运费算法。当物流公司业务扩大,追加新的State时,咱们不得不追加switch case分支,并提供新的State的运费算法。

在不远的未来,ShippingInfo类将变成这样:

  • CalculateShippingAmount()方法中包含了大量的switch case分支
  • 大量的运费算法使得ShippingInfo变得很是臃肿

从职责角度看,运费算法是另一个层面的职责,咱们也理应将运费算法从ShippingInfo中剥离出来。

重构后

为了演示策略模式的各个组成部分,我将重构后的代码拆分为4个部分,下图是重构后的UML图示。

image

抽象策略

计算运费的策略接口,在接口中定义了State属性和Calculate()计算方法。

public interface IShippingCalculation
{
    State State { get; }
    decimal Calculate();
}

策略实现

计算运费的策略实现,分别实现了Alask、NewYork和Florida三个州的运算策略。

public class AlaskShippingCalculation : IShippingCalculation
{
    public State State { get { return State.Alaska; } }

    public decimal Calculate()
    {
        return 15m;
    }
}

public class NewYorkShippingCalculation : IShippingCalculation
{
    public State State { get { return State.NewYork; } }

    public decimal Calculate()
    {
        return 10m;
    }
}

public class FloridaShippingCalculation : IShippingCalculation
{
    public State State { get { return State.Florida; } }

    public decimal Calculate()
    {
        return 3m;
    }
}

环境

IShippingInfo接口至关于环境接口,ShippingInfo至关于环境具体实现,ShippingInfo知道全部的运算策略。

public interface IShippingInfo
{
    decimal CalculateShippingAmount(State state);
}

public class ShippingInfo : IShippingInfo
{
    private IDictionary<State, IShippingCalculation> ShippingCalculations { get; set; }

    public ShippingInfo(IEnumerable<IShippingCalculation> shippingCalculations)
    {
        ShippingCalculations = shippingCalculations.ToDictionary(calc => calc.State);
    }

    public decimal CalculateShippingAmount(State state)
    {
        return ShippingCalculations[state].Calculate();
    }
}

客户端

ClientCode表示客户端,由客户端指定运输目的地,它经过IShippingInfo获取运费计算结果。

客户端依赖于IShippingInfo接口,这使运费计算策略得以隐藏,并解除了客户端对具体环境的依赖性。

public class ClientCode
{
    public IShippingInfo ShippingInfo { get; set; }

    public decimal CalculateShipping()
    {
        return ShippingInfo.CalculateShippingAmount(State.Alaska);
    }
}

使用分支仍是策略模式?

经过上面这个示例,你们能够清晰地看到,重构后的代码比重构前复杂的多。出现新的State时,虽然咱们能够方便地扩展新的策略,可是会致使策略类愈来愈多,这意味着咱们可能须要维护大量的策略类。

有些人会以为重构前的代码会比较实用,虽然耦合性高,无扩展性,但代码也比较好改——想使用哪一种方式彻底取决于你。
(在实际的应用中,运费计算远比示例中的代码要复杂的多。好比:须要依据当前的油价、运输路线、运输工具、运输时间等各类条件来计算。)

另外,咱们不该该一遇到分支语句,就想着把它改形成策略模式,这是设计模式的滥用。
若是分支条件是比较固定的,并且每一个分支处理逻辑较为简单,咱们就不必使用设计模式。

总的来讲,使用分支判断仍是策略模式?答案是:It depends on you.

相关文章
相关标签/搜索