在说策略之前,我们先来完成这样一项任务。
“做一个商场收银软件,营业员根据客户购买商品单价和数量,向客户收费”
这个不是很简单嘛,只要给我们几分钟就好了,之后就有了下面的代码:
商场收银系统v1.0关键代码如下:
//声明一个double变量total来计算总计
double total = 0.0d;
private void btnOk_Click(object sender, EventArgs e)
{
//声明一个double变量totalPrices来计算每个商品的
单价(txtPrice)*数量(txtNum)后的合计
double totalPrices = Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text);
//将每个商品合计计入总计
total = total + totalPrices;
//在列表框中显示信息
lbxList.Items.Add("单价:" + txtPrice.Text + " 数量:" + txtNum.Text + " 合计:" + totalPrices.ToString());
//在lblResult标签上显示总计数
lblResult.Text = total.ToString();
}
有些朋友可能因为上篇文章的影响,会觉得不可能这么简单吧,其实呢,就是这么简单。
但是呢,系统不是一尘不变的,客户的需求是多变得。
如果现在我要求商场对商品搞活动,所有的商品打8折。
那不就是在totalPrices后面乘以一个0.8吗?
商场收银系统v1.0关键代码如下:
//声明一个double变量total来计算总计
double total = 0.0d;
private void btnOk_Click(object sender, EventArgs e)
{
//声明一个double变量totalPrices来计算每个商品的
单价(txtPrice)*数量(txtNum)后的合计
double totalPrices = Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text);
//将每个商品合计计入总计
total = total + totalPrices*0.8;
//在列表框中显示信息
lbxList.Items.Add("单价:" + txtPrice.Text + " 数量:" + txtNum.Text + " 合计:" + totalPrices.ToString());
//在lblResult标签上显示总计数
lblResult.Text = total.ToString();
}
难道商场活动结束,不打折了,你还要再把程序改写代码再去把所有机器全部安装一次吗?再说,我现在还有可能因为周年庆,打五折的情况,你怎么办?
好吧,想得是简单了点。其实只要加一个下拉选择框就可以解决你说的问题。
商场收银系统v1.1关键代码如下:
商场收银系统v1.1关键代码如下:
double total = 0.0d;
private void btnOk_Click(object sender, EventArgs e)
{
double totalPrices = 0d;
//cbxType是一个下拉选择框,分别有“正常收费”、“打8折”、“打7折”和“打5折”
switch (cbxType.SelectedIndex)
{
case 0:
totalPrices =Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text);
break;
case 1:
totalPrices =Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text) * 0.8;
break;
case 2:
totalPrices = Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text) * 0.7;
break;
case 3:
totalPrices = Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text) * 0.5;
break;
}
total = total + totalPrices;
lbxList.Items.Add(
"单价:" + txtPrice.Text +
" 数量:" + txtNum.Text + " " + cbxType.SelectedItem +
" 合计:" + totalPrices.ToString()
);
lblResult.Text = total.ToString();
}
这比刚才灵活性上是好多了。
不过重复代码很多,像Convert.ToDouble(),你这里就写了8遍,
而且4个分支要执行的语句除了打折多少以外几乎没什么不同,应该考虑重构一下。
不过还不是最主要的,现在我的需求又来了,商场的活动加大,需要有满300返100的促销算法,你说怎么办?满300返100,那要是700就要返200了?这个必须要写函数了吧?”
用简单工厂模式,可以先写一个父类,再继承它实现多个打折和返利的子类,利用多态,完成这个代码。
打算写几个子类?根据需求呀,比如8折、7折、5折、满300送100、满200送50……要几个写几个。
面向对象的编程,并不是类越多越好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象集合才是类。
打一折和打九折只是形式的不同,抽象分析出来,所有的打折算法都是一样的,所以打折算法应该是一个类。
商场收银系统v1.3关键代码如下
//现金收取父类
abstract class CashSuper
{
//抽象方法:收取现金,参数为原价,返回为当前价
public abstract double acceptCash(double money);
}
//正常收费,继承CashSuper
class CashNormal : CashSuper
{
public override double acceptCash(double money)
{
return money;
}
}
//客户端窗体程序(主要部分)
CashSuper csuper;//声明一个父类对象
double total = 0.0d;
private void btnOk_Click(object sender, EventArgs e)
{
//利用简单工厂模式根据下拉选择框,生成相应的对象
csuper = CashFactory.createCashAccept(cbxType.SelectedItem.ToString());
double totalPrices = 0d;
//通过多态,可以得到收取费用的结果
totalPrices = csuper.acceptCash(
Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text)
);
total = total + totalPrices;
lbxList.Items.Add(
"单价:" + txtPrice.Text +
" 数量:" + txtNum.Text + " " + cbxType.SelectedItem +
" 合计:" + totalPrices.ToString()
);
lblResult.Text = total.ToString();
}
好了,使用简单工厂做完了,我们得到了下面的显示结果
要是需要打5折和满500送200的促销活动,如何办?
只要在收费对象生成工厂当中加两个条件,在界面的下拉选项框里加两项,就OK了。
如果现在需要增加一种商场促销手段,满100积分10点,以后积分到一定时候可以领取奖品如何做?
收费对象生成工厂加一个积分算法,构造方法有两个参数:条件和返点,让它继承CashSuper,再到收费对象生成工厂里加满100积分10点的分支条件,再到界面稍加改动
就行了。
如果商场现在需要拆迁,没办法,只能跳楼价销售,商场的所有商品都需要打8折,打折后的价钱再每种商品满300送50,最后计总价的时候,商场还满1000送200
搞没搞错哦,这商场不如白送得了,哪有这样促销的?
你是软件开发者,客户老是变动需求的确不爽,但你不能不让客户提需求呀,我不是说过吗,需求的变更是必然!所以开发者应该的是考虑如何让自己的程序更能适应变化,而不是抱怨客户的无理,客户不会管程序员加班时的汗水,也不相信程序员失业时的眼泪。
简单工厂模式虽然也能解决这个问题,但的确不是最好的办法,另外由于商场是可能经常性的更改打折额度和返利额度,每次更改都需要改写代码重新编译部署真的是很糟糕的处理方式,面对算法的时常变动,是不是应该有更好的办法。方法肯定是有的,利用『策略模式』就可以很好的解决这样的问题,下面就让我们先来看看什么是策略模式吧。
STRATEGY(策略)
1.意图
定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。
本模式使得算法可独立于使用它的客户而变化。
2.别名
政策(Policy)
3.动机
有许多算法可对一个正文流进行分行。将这些算法硬编进使用它们的类中是不可取的,其原因如下:
(1)需要换行功能的客户程序如果直接包含换行算法代码的话将会变得复杂,这使得客户程序庞大并且难以维护,尤其当其需要支持多种换行算法时问题会更加严重。
(2)不同的时候需要不同的算法,我们不想支持我们并不使用的换行算法。
(3)当换行功能是客户程序的一个难以分割的成分时,增加新的换行算法或改变现有算法将十分困难。
可以定义一些类来封装不同的换行算法,从而避免这些问题。一个以这种方法封装的算法称为一个策略(strategy)。
4.适用性
许多相关的类仅仅是行为有异。“策略”提供了一种用多个行为中的一个行为来配置一个类的方法。
需要使用一个算法的不同变体。例如,可能会定义一些反映不同的空间/时间权衡的算法。当这些变体实现为一个算法的类层次时,可以使用策略模式。
算法使用客户不应该知道的数据。可使用策略模式以避免暴露复杂的、与算法相关的数据结构。
一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现。将相关的条件分支移入它们各自的Strategy类中以代替这些条件语句。
5.结构
6.参与者
Strategy(策略,如Compositor)
—定义所有支持的算法的公共接口。Context使用这个接口来调用某ConcreteStrategy定义的算法。
ConcreteStrategy(具体策略,如SimpleCompositor)
—以Strategy接口实现某具体算法。
Context(上下文,如Composition)
—用一个ConcreteStrategy对象来配置。
—维护一个对Strategy对象的引用。
—可定义一个接口来让Stategy访问它的数据。
7.协作
Strategy和Context相互作用以实现选定的算法。当算法被调用时,Context可以将该算法所需要的所有数据都传递给该Stategy。或者,Context可以将自身作为一个参数传递给Strategy操作。这就让Strategy在需要时可以回调Context。
Context将它的客户的请求转发给它的Strategy。客户通常创建并传递一个ConcreteStrategy对象给Context;这样,客户仅与Context交互。通常有一系列的ConcreteStrategy类可供客户从中选择。
8.效果
Strategy模式有下面的一些优点和缺点:
1)相关算法系列
Strategy类层次为Context定义了一系列的可供重用的算法或行为。继承有助于析取出这些算法中的公共功能。
2)一个替代继承的方法
继承提供了另一种支持多种算法或行为的方法。可以直接生成一个Context类的子类,从而给它以不同的行为。
但这会将行为硬行编制到Context中,而将算法的实现与Context的实现混合起来,从而使Context难以理解、难以维护和难以扩展,而且还不能动态地改变算法。
最后得到一堆相关的类, 它们之间的唯一差别是它们所使用的算法或行为。
将算法封装在独立的Strategy类中使得你可以独立于其Context改变它,使它易于切换、易于理解、易于扩展。
3)消除了一些条件语句
Strategy模式提供了用条件语句选择所需的行为以外的另一种选择。
4)实现的选择
Strategy模式可以提供相同行为的不同实现。客户可以根据不同时间/空间权衡取舍要求从不同策略中进行选择。
5) 客户必须了解不同的Strategy
本模式有一个潜在的缺点,就是一个客户要选择一个合适的Strategy就必须知道这些Strategy到底有何不同。此时可能不得不向客户暴露具体的实现问题。因此仅当这些不同行为变体与客户相关的行为时, 才需要使用Strategy模式。
6) Strategy和Context之间的通信开销
无论各个ConcreteStrategy实现的算法是简单还是复杂, 它们都共享Strategy定义的接口。
因此很可能某些ConcreteStrategy不会都用到所有通过这个接口传递给它们的信息;简单的ConcreteStrategy可能不使用其中的任何信息!这就意味着有时Context会创建和初始化一些永远不会用到的参数。如果存在这样问题, 那么将需要在Strategy和Context之间进行更紧密的耦合。
7 ) 增加了对象的数目
Strategy增加了一个应用中的对象的数目。
有时你可以将Strategy实现为可供各Context共享的无状态的对象来减少这一开销。任何其余的状态都由Context维护。Context在每一次对Strategy对象的请求中都将这个状态传递过去。共享的Stragey不应在各次调用之间维护状态。
9.实现
1)定义Strategy和Context接口
Strategy和Context接口必须使得ConcreteStrategy能够有效的访问它所需要的Context中的任何数据,反之亦然。
一种办法是让Context将数据放在参数中传递给Strategy操作—也就是说, 将数据发送给Strategy。这使得Strategy和Context解耦。但另一方面, Context可能发送一些Strategy不需要的数据。
另一种办法是让Context将自身作为一个参数传递给Strategy, 该Strategy再显式地向该Context请求数据。或者, Strategy可以存储对它的Context的一个引用, 这样根本不再需要传递任何东西。
这两种情况下, Strategy都可以请求到它所需要的数据。但现在Context必须对它的数据定义一个更为精细的接口, 这将Strategy和Context更紧密地耦合在一起。
2)将Strategy作为模板参数
在C++中,可利用模板机制用一个Strategy来配置一个类。然而这种技术仅当下面条件满足时才可以使用
(1) 可以在编译时选择Strategy
(2) 它不需在运行时改变。
在这种情况下,要被配置的类(如,Context)被定义为以一个Strategy类作为一个参数的模板类:
使用模板不再需要定义给Strategy定义接口的抽象类。把Strategy作为一个模板参数也使得可以将一个Strategy和它的Context静态地绑定在一起,从而提高效率。
策略模式讲完了,那我们之前的商场收银软件怎么去改了,下面我们来改一改。
我们来做个策略Context类
//收费策略Context
class CashContext
{
//声明一个现金收费父类对象
private CashSuper cs;
//设置策略行为,参数为具体的现金收费子类(正常,打折或返利)
public void setBehavior(CashSuper csuper)
{
this.cs = csuper;
}
//得到现金促销计算结果(利用了多态机制,不同的策略行为导致不同的结果)
public double GetResult(double money)
{
return cs.acceptCash(money);
}
}
客户端主要代码如下:
double total = 0.0d;//用于总计
private void btnOk_Click(object sender, EventArgs e)
{
CashContext cc = new CashContext();
switch (cbxType.SelectedItem.ToString())
{
case "正常收费":
cc.setBehavior(new CashNormal());
break;
case "满300返100":
cc.setBehavior(new CashReturn("300", "100"));
break;
case "打8折":
cc.setBehavior(new CashRebate("0.8"));
break;
}
double totalPrices = 0d;
totalPrices =
cc.GetResult(Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text));
total = total + totalPrices;
lbxList.Items.Add(
"单价:" + txtPrice.Text +
" 数量:" + txtNum.Text + " " + cbxType.SelectedItem +
" 合计:" + totalPrices.ToString());
lblResult.Text = total.ToString();
}
好了,改完了。
用策略模式是实现了,但有些疑问,用了策略模式,则把分支判断又放回到客户端来了,这等于要改变需求算法时,还是要去更改客户端的程序?
最初的策略模式是有缺点的,客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。换言之,策略模式只适用于客户端知道所有的算法或行为的情况。
仔细观察过代码,不管是用工厂模式写的,还是用策略模式写的,那个分支的switch依然去不掉。原因在哪里?
要考虑的就是可不可以不在程序里写明‘如果是打折就去实例化CashRebate类,如果是返利就去实例化CashReturn类’这样的语句,而是在当用户做了下拉选择后,再根据用户的选择去某个地方找应该要实例化的类是哪一个。
下面我们就来介绍一种编程方式:依赖注入(Dependency Injection)
关键在于如何去用这种方法来解决我们的switch问题。本来依赖注入是需要专门的IoC容器提供,比如spring.net,显然当前这个程序不需要这么麻烦,只需要再了解一个简单的.net技术‘反射’就可以了。
请看下面的两个样例:
//实例化方法一
//原来把一个类实例化是这样的
Animal animal=new Cat();
//声明一个动物对象,名称叫animal,然后将animal实例化成猫类的对象
//实例化方法二
//还可以用反射的办法得到这个实例
using System.Reflection;//先引用System.Reflection
//假设当前程序集是AnimalSystem,名称空间
AnimalSystem Animal animal =
(Animal)Assembly.Load("AnimalSystem")
.CreateInstance("AnimalSystem.Cat");
利用Assembly.Load("程序集名称").CreateInstance("名称空间.类名称")去实例化类。
这样我们就可以将我们的软件再次进行改进:
让它去读XML的配置文件,来生成这个下拉列表框,然后再根据用户的选择,通过反射实时的实例化出相应的算法对象,最终利用策略模式计算最终的结果。
客户端主要代码:
using System.Reflection;
DataSet ds;//用于存放配置文件信息
double total = 0.0d;//用于总计
private void Form1_Load(object sender, EventArgs e)
{
//读配置文件
ds = new DataSet();
ds.ReadXml(Application.StartupPath + "\\CashAcceptType.xml");
//将读取到的记录绑定到下拉列表框中
foreach (DataRowView dr in ds.Tables[0].DefaultView)
{
cbxType.Items.Add(dr["name"].ToString());
}
cbxType.SelectedIndex = 0;
}
private void btnOk_Click(object sender, EventArgs e)
{
CashContext cc = new CashContext();
//根据用户的选项,查询用户选择项的相关行
DataRow dr = ((DataRow[])ds.Tables[0].Select("name='" + cbxType.SelectedItem.ToString() + "'"))[0];
//声明一个参数的对象数组
object[] args = null;
//若有参数,则将其分割成字符串数组,用于实例化时所用的参数
if (dr["para"].ToString() != "") args = dr["para"].ToString().Split(',');
//通过反射实例化出相应的算法对象
cc.setBehavior((CashSuper)Assembly.Load(“商场管理软件 ").CreateInstance("商场管理软件." + dr["class"].ToString(), false,BindingFlags.Default, null, args, null, null));
double totalPrices = 0d;
totalPrices = cc.GetResult(Convert.ToDouble(txtPrice.Text) * Convert.ToDouble(txtNum.Text));
total = total + totalPrices;
lbxList.Items.Add("单价:" + txtPrice.Text +" 数量:" + txtNum.Text + " " +cbxType.SelectedItem +" 合计:" + totalPrices.ToString());
lblResult.Text = total.ToString();
}
配置文件 CashAcceptType.xml 的代码
<?xml version="1.0" encoding="utf-8" ?>
<CashAcceptType>
<type>
<name>正常收费</name>
<class>CashNormal</class>
<para></para>
</type>
<type>
<name>满300返100</name>
<class>CashReturn</class>
<para>300,100</para>
</type>
<type>
<name>满200返50</name>
<class>CashReturn</class>
<para>200,50</para>
</type>
<type>
<name>打8折</name>
<class>CashRebate</class>
<para>0.8</para>
</type>
<type>
<name>打7折</name>
<class>CashRebate</class>
<para>0.7</para>
</type>
</CashAcceptType>
无论需求是什么,现在连程序都不动,只需要去改改XML文件就全部摆平。
比如,如果觉得现在满300送100太多,要改成送80,只需要去XML文件里改就行.
再比如,希望增加新的算法,如积分返点,那先写一个返点的算法类继承CashSuper,再去改一下XML文件,对过去的代码依然不动。
总之,现在是真的做到了程序易维护,可扩展。