设计模式(二)策略模式

考虑一个需求:

做一个商场收银软件,营业员根据客户所购买商品的单价和数量,向客户收费。

好像很简单,两个文本框,输入单价和数量,再用个列表框来记录商品的合计,最终用一个按钮来算出总额就可以了,对,还需要一个重置按钮来重新开始,不就行了?

代码如下:

//声明一个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.csdn.net/qq_36770641/article/details/82712283  简单工厂模式

下一章:https://blog.csdn.net/qq_36770641/article/details/82750708  单一指责原则、开放-封闭原则、依赖倒转原则

(本文遵循《大话设计模式》中的讲述顺序和方法,代码部分使用C#)

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值