[设计模式]之二:策略模式
需求情景
比如现在需要做一个收银软件,要根据用户所买商品的单价和数量进行计算。
很简单,用“单价 * 数量”即可。
但如果某天需要打折呢?
也很简单,同一个方法,把折扣作为一个参数,默认值为1,代码改为“单价 * 数量 * 折扣”即可。
恩,看起来都很美好。现在又要加需求,我要满300减100,我还要满200送50...
OK,现在就得回到面向对象上来了。向上次简单工厂一样,把所有计算价格可能的方法封装成一个个类。比如一个打一折类,一个打两折类...唉等等,这可不对。上次加、减、乘、除分别封装是因为他们属于同一种类型,但是有不同的实现方法。而这次,对于打折来说,不论打几折,打折的计算方式都是一样的,只是形式不同,但本质是一样的。同理,满减和返利也是两种类型,但各自有多种实现。
面向对象的编程,并不是类越多越好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象集合才是类
所以可以开始编码,先抽象一个计算收款的类,抽象一个收钱的方法,然后根据不同打折类型实现不同的收钱方法。
@interface Cash : NSObject
- (CGFloat)acceptOriginCash: (CGFloat)money;
@end
@implementation Cash
- (CGFloat)acceptOriginCash: (CGFloat)money {
return money;
}
@end
///正常价钱
@implementation CashNormal
- (CGFloat)acceptOriginCash:(CGFloat)money {
return money;
}
@end
///折扣
@interface CashRebate : Cash
@property (nonatomic, assign) CGFloat rebate;
@end
@implementation CashRebate
- (instancetype)init {
self = [super init];
if (self) {
_rebate = 1.0; //默认不打折
}
return self;
}
- (CGFloat)acceptOriginCash:(CGFloat)money {
return money * _rebate;
}
@end
///满返
@interface CashReturn : Cash
@property (nonatomic, assign) CGFloat moneyCondition;
@property (nonatomic, assign) CGFloat moneyReturn;
@end
@implementation CashReturn
- (instancetype)init {
self = [super init];
if (self) {
_moneyReturn = 0;
_moneyCondition = 0;
}
return self;
}
- (CGFloat)acceptOriginCash:(CGFloat)money {
if (_moneyCondition == 0 || _moneyReturn == 0 || money < _moneyCondition) {
return money; //没有返现
} else {
int returnCount = floorf(money / _moneyCondition);
money -= returnCount * _moneyReturn;
return money;
}
}
@end
创建好以上几种收费类型,设想一下,一般打折时都会列出相应的打折商品,也就是说平时不是所有的商品都打折,这时候假设我们专门写好一个折扣日的类,类中包含了打折商品列表,当然也包含了打折的方式等其他信息。继续用面向对象的思想去思考,折扣日应该也分好几种,比如周末,五一,工作日等等,所以折扣日也可以抽象一个基类出来,这个基类就应该包含返回折扣结果的抽象方法。
OK,到这里问题就来了,不同的折扣日都有相同的获取最终价钱的方法,而对于价钱的计算策略却完全不同,也就是每个具体的折扣日实现这个返回折扣结果的抽象方法都不一样。那该怎么做?
设计原则:找到系统中变化的部分,将变化的部分同其它稳定的部分隔开。换句话说就是:"找到变化并且把它封装起来,稍后你就可以在不影响其它部分的情况下修改或扩展被封装的变化部分。"尽管这个概念很简单,但是它几乎是所有设计模式的基础,所有模式都提供了使系统里变化的部分独立于其它部分的方法。
可以看出,每个折扣日都要实现基类返回折扣结果的方法,但实现的方法不一样。而计算方法都是经过了封装的,保证计算方法不被改变,也保证改变一个不会影响到其他计算方法。在这种情况下,就可以考虑使用策略模式。
策略模式
策略模式定义了算法家族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化不会影响到使用算法的客户。
以上几种收钱方式都是一些算法,算法本身只是一种策略,最重要的是这些算法是随时都可能且可以互相替换的,这就是变化点,而封装变化点是我们面向对象很重要的思维方式。
所以这里的思路是创建一个上下文类,用策略对象作为构造参数,来维护一个对策略对象的引用。同时这样也不会受到拓展的影响。
@interface CashContext : NSObject
- (instancetype)initWithCash: (Cash *)cash;
- (CGFloat)getResult: (CGFloat)money;
@end
///
@interface CashContext()
{
Cash *_cash;
}
@end
@implementation CashContext
- (instancetype)initWithCash: (Cash *)cash {
self = [super init];
if (self) {
_cash = cash;
}
return self;
}
- (CGFloat)getResult: (CGFloat)money {
return [_cash acceptOriginCash:money];
}
@end
创建好Context类,就可以通过构造方法选择不同的策略来实现计算:
CashContext *context = [[CashContext alloc] initWithCash:[[CashRebate alloc] initWithRebate:0.8]];//打8折
CGFloat value = [context getResult:400]]//原价400
UML类图
应用场景和优缺点
应用
- 多个类只区别在表现行为不同,可以使用Strategy模式,在运行时动态选择具体要执行的行为。
- 需要在不同情况下使用不同的策略(算法),或者策略还可能在未来用其它方式来实现。
- 对客户隐藏具体策略(算法)的实现细节,彼此完全独立。(你只要知道Context类的接口,不必知道折扣算法内部是怎么实现的)
优点
- 提供了一种替代继承的方法,而且既保持了继承的优点(代码重用)还比继承更灵活(算法独立,可以任意扩展)。
- 避免程序中使用多重条件转移语句,使系统更灵活,并易于扩展。
- 遵守大部分GRASP原则和常用设计原则,高内聚、低偶合。
缺点
- 因为每个具体策略类都会产生一个新类,所以会增加系统需要维护的类的数量。
参考
鸭子-策略模式(Strategy)
这篇文章更深入形象,推荐阅读