[30天快速上手TDD][Day 17]Refactoring - Strategy Pattern

[30天快速上手TDD][Day 17]Refactoring - Strategy Pattern


前言

在上篇文章中,我们将各个物流商的对象,抽象化出来一个物流商的界面,这个界面提供了当下页面对象所需要的功能:

  1. 计算运费
  2. 取得运费结果
  3. 取得物流商名称

虽然页面对象仍与物流商对象直接相依,但在 context 端已经是“使用界面”,而不管各物流商对象背后的实践了。

这篇文章,标题虽然带着“Strategy Pattern”,也就是策略模式,但不熟 Design Patterns 的读者朋友不用担心,保持着心中无招即可。我们只需要把程序的坏味道用最自然的方式重构,您就会体会到 Strategy Pattern 的样子、目的、用法, Strategy Pattern 将会自动的浮现出来。

记得,虽是心中无招,但仍有心法,也就是 OO 的 SOLID 原则,是我们重构的底限。

只是重构一个判断式,把一样的东西留着,不一样的东西抽成 function ,我想... 3 分钟应该还是很够用了

目前的程序

为方便阅读重构前后的程序比较,这边先列出截至目前为止,我们的页面程序如下所示:


protected void btnCalculate_Click(object sender, EventArgs e)
{
    //若页面通过验证
    if (this.IsValid)
    {
        //取得画面数据
        var product = this.GetProduct();

        var companyName = "";
        double fee = 0;

        //选黑猫,计算出运费
        if (this.drpCompany.SelectedValue == "1")
        {
            //计算
            //BlackCat blackCat = new BlackCat() { ShipProduct = product };
            //blackCat.Calculate();
            //companyName = blackCat.GetsComapanyName();
            //fee = blackCat.GetsFee();
            ILogistics logistics = new BlackCat() { ShipProduct = product };
            logistics.Calculate();
            companyName = logistics.GetsComapanyName();
            fee = logistics.GetsFee();


        }
        //选新竹货运,计算出运费
        else if (this.drpCompany.SelectedValue == "2")
        {
            //计算
            //Hsinchu hsinchu = new Hsinchu() { ShipProduct = product };
            //hsinchu.Calculate();
            //companyName = hsinchu.GetsComapanyName();
            //fee = hsinchu.GetsFee();

            ILogistics logistics = new Hsinchu() { ShipProduct = product };
            logistics.Calculate();
            companyName = logistics.GetsComapanyName();
            fee = logistics.GetsFee();
        }
        //选邮局,计算出运费
        else if (this.drpCompany.SelectedValue == "3")
        {
            //计算
            //PostOffice postOffice = new PostOffice() { ShipProduct = product };
            //postOffice.Calculate();
            //companyName = postOffice.GetsComapanyName();
            //fee = postOffice.GetsFee();

            ILogistics logistics = new PostOffice() { ShipProduct = product };
            logistics.Calculate();
            companyName = logistics.GetsComapanyName();
            fee = logistics.GetsFee();
        }
        //发生预期以外的状况,呈现警告消息,回首页
        else
        {
            var js = "alert('发生不预期错误,请洽系统管理者');location.href='http://tw.yahoo.com/';";
            this.ClientScript.RegisterStartupScript(this.GetType(), "back", js, true);
        }

        //呈现结果
        this.SetResult(companyName, fee);
    }
}

回顾

重构到这,其实已经很充足了,职责已经分离,也透过界面来降低耦合,也有对应的整合测试与单元测试。

不过如同一开始重构的时机点所说,当我们为了需求或 bug 而修改功能时,其实可以再思考一下,这样类似的需求会不会再发生。这样的情况,有没有合适的 pattern 可以解决我们的需求与问题。

首先切换回人话模式,眼前的功能需求,用人话来描述就是:‘不同物流商,使用对应的计价方法’。用 Design Pattern 的用词来说,就是:‘根据条件,决定对应的算法’。也就是策略模式(strategy pattern)。

虽然提到了策略模式,但不熟 Design Patterns 的读者朋友不用担心,我们只需要把程序的坏味道用最自然的方式重构,您就会体会到 Strategy Pattern 的样子、目的、用法, Strategy Pattern 将会自动的浮现出来。

重构第九式:运用Design Pattern-策略模式

上面已经提到了,这段程序一言以蔽之,就是“不同物流商,使用对应的计价方法”,让我们回过头来看现在的程序,有哪些部分是相同的,哪些部分是不同的,如下图所示:

相同与不同的部分

可以看到经过抽象地使用界面之后,红色方块中的程序,已经是一模一样了。不同的部分,是黄色方块中的程序,也就是上面人话描述的“选择不同物流商时,要使用不同的计价方法”。

如同 DRY (Don't Repeat Yourself) 设计原则所说,在设计系统时,应避免同样一样事,却有着重复的程序的情况。一式多份,代表需求异动时,需要变更多份,代表不符合单一职责原则(SRP),也代表着可能会有漏改的情况。

以这例子来说,聪明如各位读者,肯定知道,怎么把相同的部分与不同的部分,抽到一个 function 中,只需要让不同的部分变成参数传入即可。

重构后的程序如下所示:


protected void btnCalculate_Click(object sender, EventArgs e)
{
    //若页面通过验证
    if (this.IsValid)
    {
        //取得画面数据
        var product = this.GetProduct();

        var companyName = "";
        double fee = 0;

        ////选黑猫,计算出运费
        //if (this.drpCompany.SelectedValue == "1")
        //{
        //    //计算
        //    //BlackCat blackCat = new BlackCat() { ShipProduct = product };
        //    //blackCat.Calculate();
        //    //companyName = blackCat.GetsComapanyName();
        //    //fee = blackCat.GetsFee();
        //    ILogistics logistics = new BlackCat() { ShipProduct = product };
        //    logistics.Calculate();
        //    companyName = logistics.GetsComapanyName();
        //    fee = logistics.GetsFee();


        //}
        ////选新竹货运,计算出运费
        //else if (this.drpCompany.SelectedValue == "2")
        //{
        //    //计算
        //    //Hsinchu hsinchu = new Hsinchu() { ShipProduct = product };
        //    //hsinchu.Calculate();
        //    //companyName = hsinchu.GetsComapanyName();
        //    //fee = hsinchu.GetsFee();

        //    ILogistics logistics = new Hsinchu() { ShipProduct = product };
        //    logistics.Calculate();
        //    companyName = logistics.GetsComapanyName();
        //    fee = logistics.GetsFee();
        //}
        ////选邮局,计算出运费
        //else if (this.drpCompany.SelectedValue == "3")
        //{
        //    //计算
        //    //PostOffice postOffice = new PostOffice() { ShipProduct = product };
        //    //postOffice.Calculate();
        //    //companyName = postOffice.GetsComapanyName();
        //    //fee = postOffice.GetsFee();

        //    ILogistics logistics = new PostOffice() { ShipProduct = product };
        //    logistics.Calculate();
        //    companyName = logistics.GetsComapanyName();
        //    fee = logistics.GetsFee();
        //}
        ////发生预期以外的状况,呈现警告消息,回首页
        //else
        //{
        //    var js = "alert('发生不预期错误,请洽系统管理者');location.href='http://tw.yahoo.com/';";
        //    this.ClientScript.RegisterStartupScript(this.GetType(), "back", js, true);
        //}

        ILogistics logistics = this.GetILogistics(this.drpCompany.SelectedValue, product);
        if (logistics != null)
        {
            logistics.Calculate();
            companyName = logistics.GetsComapanyName();
            fee = logistics.GetsFee();

            //呈现结果
            this.SetResult(companyName, fee);
        }
        else
        {
            var js = "alert('发生不预期错误,请洽系统管理者');location.href='http://tw.yahoo.com/';";
            this.ClientScript.RegisterStartupScript(this.GetType(), "back", js, true);
        }            
    }
}

/// 
/// 将ILogistics的instance,交给工厂来决定
/// 
/// 
/// 
/// 
  
  
private ILogistics GetILogistics(string company, Product product)
{
    if (company == "1")
    {
        return new BlackCat() { ShipProduct = product };
    }
    else if (company == "2")
    {
        return new Hsinchu() { ShipProduct = product };
    }
    else if (company == "3")
    {
        return new PostOffice() { ShipProduct = product };
    }
    else
    {            
        return null;
    }
}

相同的部分,也就是页面(在这为 context ,使用场景端)所关心的职责,如下图所示:

context

读者朋友们,从程序去阅读这个计算运费按钮的逻辑,去体会一下,程序会说话的感觉:

  1. 如果页面 Validation 通过验证
  2. 取得页面上商品资讯
  3. 取得对应的物流商
  4. 请物流商计算运费
  5. 取得物流商名称
  6. 取得运费结果
  7. 将名称与运费结果呈现到页面上

不同的部分,则是要想办法限缩到最小范围,也就是:究竟这个条件,只会影响哪些东西不同。相同的部分,请放到判断式以外。如下图所示:

不同的部分

不同的部分,指的是“画面上选择哪一间物流商”,而这个判断,只会影响要使用哪一个物流商对象。而所有的物流商对象,都符合“物流商界面”(不论是继承或实践,都是 Is-A 的关系)。

到这边,就只是透过一个 function ,将不同的部分放到参数中,以决定回传哪一个物流商对象。相同的部分,则放到判断式之外,用来描述 context 的流程与商业逻辑。

恭喜您,这就是策略模式。

如 wiki 上的描述:

the strategy pattern (also known as the policy pattern) is a particular software design pattern, whereby algorithms can be selected at runtime.

也就是,在执行阶段时,可以依据不同情况选择不同的算法。

来看一下 wiki 上 strategy pattern 的 class diagram :

在这个例子里,我们的程序若画成 class diagram ,就是按照这样的 pattern 所设计。如下图所示:

strategy pattern class diagram

小结

策略模式,难吗?如果您已经把程序重构成这副模样,相信我,你真的不必懂“策略模式”这四个字。因为我们重构用的就只是最基本的面向对象精神与设计原则。

但,这也不代表着开发人员就不需要了解或学习设计模式。设计模式,就像 UML 一样,除了可以拿来当作特定类型问题的 guidance 蓝图,也很常拿来沟通。当开发人员或分析设计人员,针对某一个情境、需求或问题时,可能只需要用“策略模式”四个字,就可以让每个人心里面有着基本的 class model ,并快速的 mapping 到眼前的情境。

想像一下,以这例子,每个人眼前面对的是重构前的程序,一个人提出:我们可以透过“策略模式”来重构,来把重复的程序降到最低,职责分离,并且对扩充开放,对修改封闭。这时,如果学习并了解过策略模式,大家脑袋里基本上就会把页面放到 context ,把抽象职责相同的部分淬练出一个界面,让每个对象不同的实践细节封装起来,页面只需要透过界面,就能保持一致。

心中无招,就能不被设计模式的框框给设限住。但无招不代表乱七八糟,而是掌握最基本的精神、原则,针对眼前的问题,使用者的需求来解决。

建议

读者朋友可以试试,当碰到一个问题或需求时,先别去寻找哪一个 pattern 适用,而是透过这一系列的方式,先动手重构。直到您觉得重构完成了,接着去看这样的问题,适合用哪一种 pattern ,接着比对您的设计与 GoF 原生的 design pattern ,有何异同。

接着用心去体会,不同的地方,是否属于自己情境或问题下,需客制化或变形的部分。还是单纯设计的冗赘,不够精简、精准。

如果是后者,恭喜你,你趁机学到了自己之前的盲点,再下一次的需求,您就更能使出 pattern 中的精妙之处。

如果是前者,恭喜你,您可以理解在自己的问题领域中,除了最原生的问题解决了,还更弹性地符合了使用者的需求。

去体会个中差异,才能活学活用。设计模式,只是一些常见的问题领域,所衍生出常见的模式解决方式,它是一种最普遍、最抽象、最基础的解决方式,不要去强求自己的设计所产生出来的 class diagram 一定要跟原著或 wiki 上图形一模一样,但绝对要能清楚说出来,为何不一样。

最后,在重构中设计模式的确是一种很方便、快速、好用的手法,但这边要强调的是,开发人员应该要能由需求、问题、 legacy code 当出发点,在重构的过程中,实践并体会出,由原始程序演变成某一种或多种设计模式所搭配设计的最终结果。如此一来,您才真的能体会到设计模式的髓。(因为设计模式的演变过程,绝大部分也正是从重构而来)

当您已经能完全体会且累积了许多相同问题领域的重构手法后,面对这篇文章范例这类的问题,心中无想,就会自然而然的使出策略模式来解决。


最后搞笑一下,下面是大家很熟稔的一段台词:

张三丰:无忌,你有九阳神功护体,学什么武功都特别快,太极拳只重其义,不重其招,你忘记所有招式,就练成太极拳了!

张三丰:你记住了没有?
张无忌:没记住!
张三丰:这套叫什么拳?
张无忌:不知道!
张三丰:你老爸姓什么呢?
张无忌:我忘了!
张三丰:好!你只要记住把这两浑蛋打成废人就行了!

忘记了
(图片来源:youtube: http://www.youtube.com/watch?v=FaGUA-hUsys)

基本上就是这样,面向对象的基本意义、目的、精神与原则,就像这边的九阳神功,有九阳神功护体,学什么 pattern 都快。

不必强记这是什么 pattern ,只要记得:可以解决你的问题,满足使用者的需求就行了!


或许您会对下列培训课程感兴趣:

  1. 2019/7/27(六)~2019/7/28(日):演化式设计:测试驱动开发与持续重构 第六梯次(中国台北)
  2. 2019/8/16(五)~2019/8/18(日):【C#进阶设计-从重构学会高易用性与高弹性API设计】第二梯次(中国台北)
  3. 2019/9/21(六)~2019/9/22(日):Clean Coder:DI 与 AOP 进阶实战 第二梯次(中国台北)
  4. 2019/10/19(六):【针对遗留代码加入单元测试的艺术】第七梯次(中国台北)
  5. 2019/10/20(日):【极速开发】第八梯次(中国台北)

想收到第一手公开培训课程资讯,或想询问企业内训、顾问、教练、咨询服务的,请洽 Facebook 粉丝专页:91敏捷开发之路。

原文:大专栏  [30天快速上手TDD][Day 17]Refactoring - Strategy Pattern


猜你喜欢

转载自www.cnblogs.com/petewell/p/11516448.html