考虑一个需求:
做一个商场收银软件,营业员根据客户所购买商品的单价和数量,向客户收费。
好像很简单,两个文本框,输入单价和数量,再用个列表框来记录商品的合计,最终用一个按钮来算出总额就可以了,对,还需要一个重置按钮来重新开始,不就行了?
代码如下:
//声明一个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();
}
嗯,很不错,现在考虑,
商场要对商品搞活动,所有商品打八折,怎么办?
心想,那不就是在totalPrices后面乘以一个0.8吗?可是,难道商场活动结束,不打折了,还要再把程序改写代码再去把所有机器全部安装一次吗?再说,现在还有可能因为周年庆,打五折的情况,怎么办 ?
其实,可以加一个下拉框解决问题:
double total = 0.0d;
private void Form1_Load(object sender, EventArgs e){
// 在ComboBox中加下拉选项
cbxType.Items.AddRange(new objecct[]{"正常收费","打八折","打七折","打五折"});
cbxType.SelectedIndex=0;
}
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();
}
这样,只要事先把商场可能的打折都做成下拉选择框的项,要变化的可能性就小多了。
这时候,需求又来了:
商场的活动加大,需要有满300返100的促销算法,怎么办?
满300返100,那要是700就要返200了 ,该写一个函数吗?
仔细想想,我们上一章学了什么?对,简单工厂模式!我们可以先写一个父类,再继承它实现多个打折和返利的子类,利用多态,完成这个代码。
那么,写几个子类呢?8折、7折、5折、满300送100、满200送50……要几个写几个吗?
其实,这里打折基本都是一样的,只要有个初始化参数就可以了。满几送几的,需要两个参数才行。
其实,面向对象的编程,并不是类越多越好。类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象集合才是类。打一折和打九折只是形式的不同,抽象分析出来,所有的打折算法都是一样的,所以打折算法应该是一个类。
代码结构图:
现金收费抽象类:
abstract class CashSuper{
// 现金收取超类的抽象方法,收取现金,参数为原价,返回为当前价
public abstract double acceptCash(double money);
}
正常收费子类:
class CashNormal:CashSuper{
public override double acceptCash(double money){ // 正常收费,原价返回
return money;
}
}
打折收费子类:
class CashRebate:CashSuper{
private double moneyRebate = 1d;
// 打折收费,初始化时,必须要输入折扣率,如八折,就是0.8
public CashRebate(string moneyRebate){
this.moneyRebate=double.Parse(moneyRebate);
}
public override double acceptCash(double money){
return money * moneyRebate;
}
}
返利收费子类:
class CashReturn:CashSuper{
private double moneyCondition =0.0d;
private double moneyReturn =0.0d;
public CashReturn(string moneyCondition, string moneyReturn){
this.moneyCondition=double.Parse(moneyCondition);
this.moneyReturn=double.Parse(moneyReturn);
}
public override double acceptCash(double money){
double result=money;
if(money>=moneyCondition)
// 若大于返利条件,则需要减去返利值
result=money-Math.Floor(money/moneyCondition)*moneyReturn;
return result;
}
}
现金收费工厂类:
class CashFactory{
// 现金收取工厂
public static CashSuper createCashAccept(string type){
CashSuper cs=null;
// 根据条件返回相应的对象
switch(type){
case "正常收费":
cs=new CashNormal();
break;
case "满300返100":
CashReturn cr1 = newCashreturn("300","100");
cs=cr1;
break;
case "打8折":
CashRebate cr2 = new CashRebate("0.8");
cs=cr2;
break;
}
return cs;
}
}
客户端程序主要部分:
// 客户端窗体程序(主要部分)
double total = 0.0d;
private void btnOk_Click(object sender, EventArgs e){
// 利用简单工厂模式根据下拉选择框,生成相应的对象
CashSuper csuper = CashFactory.createCashAccept(cbxType.SelectedItem.ToString());
double totalPrices=0d;
totalPrices=csuper.acceptCash(Convert.ToDouble)(txtPrice.Text) *Convert.ToDouble(txtNum.Text);
total+=totalPrices;
// 通过多态,可以得到收取费用的效果
lbxList.Items.Add("单价:" + txtPrices.Text+"数量:" + txtNum.Text + " "
+ cbxType.SelectedItem + " 合计:" +totalPrices.ToString());
lblResult.text=total.Tostring();
}
这样一来,对于活动的更改就比较简单了。
但是,简单工厂模式只是解决对象的创建问题,而且由于工厂本身包括了所有的收费方式,商场是可能经常性地更改打折额度和返利额度,每次维护或扩展收费方式都要改动这个工厂,以致代码需重新编译部署,不是最好的办法。
其实,面对算法的时常变动,还有更好的办法——
策略模式
策略模式:它定义了算法家族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化不会影响到使用算法的客户。
商场收银时如何促销,用打折还是返利,其实都是一些算法,用工厂来生成算法对象,这没有错,但算法本身只是一种策略,最重要的是这些算法是随时都可能互相替换的,这是变化点。而封装变化点是我们面向对象的一种很重要的思维方式。来看看策略模式的结构图和基本代码:
Strategy类,定义所有支持的算法的公共接口:
// 抽象算法类
abstract class Strategy{
// 算法方法
public abstract void AlgorithmInterface();
}
ConcreteStrategy,封装来了具体的算法或行为,继承于Strategy:
// 具体算法A
class ConcreteStrategyA : Strategy{
// 算法A实现方法
public override void AlgorithmInterface(){
Console.WriteLine("算法A实现");
}
}
// 具体算法B
class ConcreteStrategyB : Strategy{
// 算法B实现方法
public override void AlgorithmInterface(){
Console.WriteLine("算法B实现");
}
}
// 具体算法C
class ConcreteStrategyC : Strategy{
// 算法C实现方法
public override void AlgorithmInterface(){
Console.WriteLine("算法C实现");
}
}
Context,用一个ConcreteStrategy来配置,维护一个对Strategy对象的引用。
// 上下文
class Context{
Strategy strategy;
public Context(Strategy strategy){
this.strategy=strategy;
}
// 上下文接口
public void ContextInterface(){
// 根据具体的策略对象,调用其算法的方法
strategy.AlgorithmInterface();
}
}
客户端代码:
static void Main (string[] args){
Context context;
// 由于实例化不同的策略,所以最终在调用context.ContextInterface();时,
// 所获得的结果就不尽相同
context=new Context(new ConcreteStrategyA());
context.ContexrInterface();
context=new Context(new ConcreteStrategyB());
context.ContexrInterface();
context=new Context(new ConcreteStrategyC());
context.ContexrInterface();
Console.Read();
}
了解了基本框架后,回到我们之前的商场收费需求。其中,CashSuper就是抽象策略,而正常收费CashNormal、打折收费CashRebate和返利收费CashReturn就是三个具体策略,也就是策略模式中的具体算法。
基于此,我们来重构代码:
CashContext类:
class CashContext{
// 声明一个CashSuper对象
private CashSuper cs;
// 通过构造方法,传入具体的收费策略
public CashContext(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();
// 根据下拉选择框,将相应的策略对象作为参数传入CashContext的对象中
switch (cbxType.SelectedItem.ToString())
{
case "正常收费":
cc=new CashNormal();
break;
case "满300返100":
cc=new CashReturn("300", "100");
break;
case "打8折":
cc=new CashRebate("0.8");
break;
}
double totalPrices = 0d;
// 通过对Context的GetResult方法的调用,可以得到收取费用的结果,
// 并让具体算法与客户进行了隔离
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();
}
写到这里,可能会有些疑问:策略模式是实现了,但分支判断又放回到客户端来了,这等于要改变需求算法时,还是要去更改客户端的程序。如何把这个判断的过程从客户端程序转移走呢?
策略与简单工厂结合
改造后的CashContext:
class CashContext{
// 声明一个CashSuper对象
private CashSuper cs = null;
// 注意参数不是具体的收费策略对象,而是一个字符串,表示收费类型
public CashContext(string type){
// 将实例化具体策略的过程由客户端转移到Context类中。简单工厂的应用。
switch (type){
case "正常收费":
CashNormal cs0=new CashNormal();
cs=cs0;
break;
case "满300返100":
CashReturn cr1=new CashReturn("300","100");
cs=cr1;
break;
case "打8折":
CashRebate cr2=new CashRebate("0.8");
cs=cr2;
break;
}
}
public double GetResult(double money){
return cs.acceptCash(money);
}
}
客户端代码:
double total = 0.0d;//用于总计
private void btnOk_Click(object sender, EventArgs e)
{
// 根据下拉选择框,将相应的算法类型字符串传入CashContext的对象中
CashContext cc = new CashContext(cbxType.SelectedItem.ToString());
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();
}
写完这些,是不是对简单工厂模式理解更深刻了呢?
下面来看看上面的客户端代码与简单工厂的客户端代码的对比:
// 简单工厂模式的用法
CashSuper csuper = CashFactory.createCashAccept(cbxType.SelectedItem.Tostring());
...=csuper.acceptCash(...)
// 策略模式与简单工厂结合的用法
CashContext csuper = new CashContext(cbxType.SelectedItem.Tostring());
...=csuper.getResult(...)
发现不同了么——简单工厂模式,需要让客户端认识CashFactory和CashSuper两个类;而策略模式与简单工厂结合的用法,客户端只需要认识一个类CashContext就可以了,耦合度更低。
回过头来反思一下策略模式。它是一种定义一系列算法的方法,从概念上来看,所有这些算法完成的都是相同的工作,只是实现不同,它可以以相同的方式调用所有的算法,减少了各种算法类与使用算法类之间的耦合。
策略模式的Strategy类层次为Context定义了一系列的可供重用的算法或行为。继承有助于析取出这些算法中的公共功能。
策略模式的优点是简化了单元测试,因为每个算法都有自己的类,可以通过自己的接口单独测试。
当不同的行为堆砌在一个类中时,就很难避免使用条件语句来选择合适的行为。将这些行为封装在一个个独立的Strategy类中,可以在使用这些行为的类中消除条件语句。
即:
策略模式封装了变化。
策略模式就是用来封装算法的,但在实践中,我们发现可以用它来封装几乎任何类型的规则,只要在分析过程中听到需要在不同时间应用不同的业务规则,就可以考虑使用策略模式处理这种变化的可能性。
在基本的策略模式中,选择所用具体实现的指责由客户端对象承担,并转给策略模式的Context对象。
本章完。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
本文是作者阅读《大话设计模式》后,根据书中内容总结出的个人感悟。在此感谢程杰先生及其著作,本人受益匪浅。
本文是连载文章,此为第二章,学习封装变化点的策略模式。
上一章:https://blog.youkuaiyun.com/qq_36770641/article/details/82712283 简单工厂模式
下一章:https://blog.youkuaiyun.com/qq_36770641/article/details/82750708 单一指责原则、开放-封闭原则、依赖倒转原则
(本文遵循《大话设计模式》中的讲述顺序和方法,代码部分使用C#)
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------