看图识模式

今天从收银台的例子说起(不要嫌弃~)

商场需要一套收银台系统,这套系统除了基本的计算商品价格增减外,还要应付各种节假日的促销活动:

clipboard.png

相信即使一个编程的新手也一样能胜任这套系统的开发,实在没有太多难度~
可如果想让它的代码设计合理、易于拓展、利于维护、便于测试。这可就有难度了。

高手和新手的差别除了知识面广、专业度深之外就是代码的设计能力。其实绝大部分的任务新手也能完全搞定,无非就是多改改bug,多加加班而已。但是新手价格经济实惠,不挑剔,不怕被坑,多好,咳咳...跑题了

方案一:
上面的每种售价方案其实对应一种算法,把三个算法写在调用类中,通过if-else判断类型调用对应的算法生成价格即可。
这是对简单的写法,谈不上设计,但是也完成了功能。

不足之处是: 客户端调用的地方冗余了大量的逻辑,导致系统臃肿,难以维护且无法
拓展,算法也无法复用。这肯定是不行的

方案二:
其实三个算法是相同的功能,只是执行了不同的策略,我们可以把这三个算法封装到一个生成价格类中,客户端只需要传递一个类型,调用这个类生成价格即可。生成价格类通过类型判断调用对应的算法。

价格计算逻辑和调用端分离解耦。但是还有一些其它问题: 如果新增促销策略还
需要增加生成价格类中的if-else类型判断,而且添加一个新算法。这不符合
开放-封闭原则。而且算法只被价格计算类使用,也不能达到复用的效果。

第二个方案中解决了价格计算逻辑和调用端之间的耦合问题,剩下的问题改如何解决呢?其实当我们看到这种大量堆砌的if-else的语句时就读到了一种坏坏的味道。

下面介绍的这种设计模式有利于我们解决这种问题。

模式定义

定义

策略模式(Strategy Pattern):定义一系列算法,把它们一个一个封装起来,并且使它们可以相互替换。策略模式让算法可独立于使用它的客户而变化。

结构图

clipboard.png

责任链模式包含如下角色:

  • 环境类(Context): 维护一个对Strategy对象的引用,可避免高层模块对策略的直接调用。
  • 抽象策略类(Strategy): 定义所有支持的算法的公共接口。
  • 具体策略类(ConcreteStrategy): 以Strategy接口实现某具体算法。

在策略模式中,调用算法的主体封装到了封装类Context中,抽象策略Strategy一般是一个接口,目的只是为了定义规范,里面一般不包含逻辑。

代码

通过上面的定义,下面来设计一下代码实现:

clipboard.png

价格类对应的是context,context类引用了一个具体的价格算法(策略),代码如下:

CashScheme价格计算方法抽象类,对应抽象策略类

@interface CashScheme : NSObject

- (double)acceptCash:(double)number;

@end

@implementation CashScheme

- (double)acceptCash:(double)number
{
    return 0;
}

@end

CashRebate折扣计算方法类,对应具体策略类

@interface CashRebate : CashScheme

- (instancetype)initWithMoneyRebate:(float)moneyRebate;

@end

@interface CashRebate ()

@property (nonatomic, assign) float moneyRebate;

@end

@implementation CashRebate

- (instancetype)initWithMoneyRebate:(float)moneyRebate
{
    self = [super init];
    if (self) {
        _moneyRebate = moneyRebate;
    }
    return self;
}

- (double)acceptCash:(double)number
{
    return (self.moneyRebate * number);
}

@end

CashReturn返现计算方法类,对应具体策略类

@interface CashReturn : CashScheme

- (instancetype)initWithCondition:(double)condition returnCount:(double)count;

@end

@interface CashReturn ()

@property (nonatomic, assign) double condition;    // 条件值(达到多少可以参加返现)
@property (nonatomic, assign) double returnCount;  // 返现值

@end

@implementation CashReturn

- (instancetype)initWithCondition:(double)condition returnCount:(double)count;
{
    self = [super init];
    if (self) {
        _condition = condition;
        _returnCount = count;
    }
    return self;
}

- (double)acceptCash:(double)number
{
    double result = number;
    if (number >= self.condition) {
        result = number - self.returnCount;
    }
    return result;
}

@end

CashNormal无优惠(日常)计算方法类,对应具体策略类

@interface CashNormal : CashScheme

@end

@implementation CashNormal

- (double)acceptCash:(double)number
{
    return number;
}

@end

CashContext生成价格类,对应context

@interface CashContext : NSObject

@property (nonatomic, strong) CashScheme *cashScheme;

- (double)getResult:(double)number;

@end

@implementation CashContext

- (double)getResult:(double)number
{
    double result = number;
    if (self.cashScheme) {
        result = [self.cashScheme acceptCash:number];
    }
    return result;
}

@end

客户端调用

        CashContext *cashContext = [[CashContext alloc] init];
        CashScheme *cashScheme = nil;
        int type = 3;
        switch (type) {
            case 1:
            {
                cashScheme = [[CashNormal alloc] init];
            }
                break;
            case 2:
            {
                cashScheme = [[CashRebate alloc] initWithMoneyRebate:0.5];
            }
                break;
            case 3:
            {
                cashScheme = [[CashReturn alloc] initWithCondition:1000 returnCount:200];
            }
                break;
                
            default:
                cashScheme = [[CashNormal alloc] init];
                break;
        }
        cashContext.cashScheme = cashScheme;
        int result = [cashContext getResult:1000];
        NSLog(@"当前活动后的价格是: %@", @(result));

结果

clipboard.png

现在这样的设计很好的解决了我上面提出的问题,增加新的促销活动时只需新增一个继承至CashScheme的类,在acceptCash方法里实现自己的策略,而不用修改原有的系统,也不会影响以前的算法策略。客户端指定对应的算法策略给context就完成了对新算法的调用。这完全符合开放-封闭原则。

由于每个算法都有自己单独的类,简化了单元测试。而且算法也可以被复用。

客户端调用中的type是由运行时外部条件决定的,我这里假设为3。这也是这个模式的一个缺点,客户端需要知道不同算法策略的区别,在不同的条件下组织不同的算法策略。

这可缺点可以通过和简单工厂模式相结合来解决,这个大家自己思考一下!由于简单工厂模式很简单,不清楚的可以抽一点点时间看一下~

特征

动机

  • 完成一项任务,往往可以有多种不同的方式,每一种方式称为一个策略,我们可以根据环境或者条件的不同选择不同的策略来完成该项任务。
  • 在软件开发中也常常遇到类似的情况,实现某一个功能有多个途径,此时可以使用一种设计模式来使得系统可以灵活地选择解决途径,也能够方便地增加新的解决途径。
  • 在软件系统中,有许多算法可以实现某一功能,如查找、排序等,一种常用的方法是硬编码(Hard Coding)在一个类中,如需要提供多种查找算法,可以将这些算法写到一个类中,在该类中提供多个方法,每一个方法对应一个具体的查找算法;当然也可以将这些查找算法封装在一个统一的方法中,通过if…else…等条件判断语句来进行选择。这两种实现方法我们都可以称之为硬编码,如果需要增加一种新的查找算法,需要修改封装算法类的源代码;更换查找算法,也需要修改客户端调用代码。在这个算法类中封装了大量查找算法,该类代码将较复杂,维护较为困难。
  • 除了提供专门的查找算法类之外,还可以在客户端程序中直接包含算法代码,这种做法更不可取,将导致客户端程序庞大而且难以维护,如果存在大量可供选择的算法时问题将变得更加严重。
  • 为了解决这些问题,可以定义一些独立的类来封装不同的算法,每一个类封装一个具体的算法,在这里,每一个封装算法的类我们都可以称之为策略(Strategy),为了保证这些策略的一致性,一般会用一个抽象的策略类来做算法的定义,而具体每种算法则对应于一个具体策略类。

使用场景

在以下情况下可以使用策略模式:

  • 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
  • 一个系统需要动态地在几种算法中选择一种。
  • 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
  • 不希望客户端知道复杂的、与算法相关的数据结构,在具体策略类中封装算法和相关的数据结构,提高算法的保密性与安全性。

优缺点

优点

策略模式的优点

  • 策略模式提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。
  • 策略模式提供了管理相关的算法族的办法。
  • 策略模式提供了可以替换继承关系的办法。

     继承提供了另一种支持多种算法或行为的方法。你可以直接生成一个Context类的子
     类,从而给它以不同的行为。但这会将行为硬行编制到 Context中,而将算法的实
     现与Context的实现混合起来,从而使Context难以理解、难以维护和难以扩展,而
     且还不能动态地改变算法。最后你得到一堆相关的类 , 它们之间的唯一差别是它们
     所使用的算法或行为。 将算法封装在独立的Strategy类中使得你可以独立于其
     Context改变它,使它易于切换、易于理解、易于扩展。
  • 使用策略模式可以避免使用多重条件转移语句。

    Strategy模式提供了用条件语句选择所需的行为以外的另一种选择。当不同的行为堆
    砌在一个类中时 ,很难避免使用条件语句来选择合适的行为。将行为封装在一个个独
    立的Strategy类中消除了这些条件语句。含有许多条件语句的代码通常意味着需要使
    用Strategy模式。
    

缺点

策略模式的缺点

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类(可以通过简单工厂模式来弥补)。
  • 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。

danielmea
28 声望8 粉丝

程序猿的逗比日常-------