[30天快速上手TDD][Day 17]Refactoring - Strategy Pattern
前言
在上篇文章中,我们将各个物流商的对象,抽象化出来一个物流商的界面,这个界面提供了当下页面对象所需要的功能:
- 计算运费
- 取得运费结果
- 取得物流商名称
虽然页面对象仍与物流商对象直接相依,但在 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 ,使用场景端)所关心的职责,如下图所示:
读者朋友们,从程序去阅读这个计算运费按钮的逻辑,去体会一下,程序会说话的感觉:
- 如果页面 Validation 通过验证
- 取得页面上商品资讯
- 取得对应的物流商
- 请物流商计算运费
- 取得物流商名称
- 取得运费结果
- 将名称与运费结果呈现到页面上
不同的部分,则是要想办法限缩到最小范围,也就是:究竟这个条件,只会影响哪些东西不同。相同的部分,请放到判断式以外。如下图所示:
不同的部分,指的是“画面上选择哪一间物流商”,而这个判断,只会影响要使用哪一个物流商对象。而所有的物流商对象,都符合“物流商界面”(不论是继承或实践,都是 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 所设计。如下图所示:
小结
策略模式,难吗?如果您已经把程序重构成这副模样,相信我,你真的不必懂“策略模式”这四个字。因为我们重构用的就只是最基本的面向对象精神与设计原则。
但,这也不代表着开发人员就不需要了解或学习设计模式。设计模式,就像 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 ,只要记得:可以解决你的问题,满足使用者的需求就行了!
或许您会对下列培训课程感兴趣:
- 2019/7/27(六)~2019/7/28(日):演化式设计:测试驱动开发与持续重构 第六梯次(中国台北)
- 2019/8/16(五)~2019/8/18(日):【C#进阶设计-从重构学会高易用性与高弹性API设计】第二梯次(中国台北)
- 2019/9/21(六)~2019/9/22(日):Clean Coder:DI 与 AOP 进阶实战 第二梯次(中国台北)
- 2019/10/19(六):【针对遗留代码加入单元测试的艺术】第七梯次(中国台北)
- 2019/10/20(日):【极速开发】第八梯次(中国台北)
想收到第一手公开培训课程资讯,或想询问企业内训、顾问、教练、咨询服务的,请洽 Facebook 粉丝专页:91敏捷开发之路。 |
原文:大专栏 [30天快速上手TDD][Day 17]Refactoring - Strategy Pattern