设计模式系列11--装饰器模式

大部分公司都有销售团队,假设老板给你布置了一个任务,让你按照下面的要求开发一套程序来计算销售团队每个月的工资。

  • 每个人当月业务奖金 = 当月销售额 * 3%
  • 每个人的累积奖金 = 总的回款额 * 0.1%
  • 销售经理的团队奖金 = 团队总销售额 * 1%

每个人的工资就是基本工资加上奖金,那么按照常规模式我们来看下如何让实现。

#import "calculateBonus.h"

@implementation calculateBonus

-(NSInteger)calculateSalary:(NSInteger)monthSales  sumSales:(NSInteger)sumSales isManager:(BOOL)manager{
    //基本工资都是8000
    NSInteger salary = 8000;
    salary += [self monthBonus:monthSales];
    salary += [self sumBonus:sumSales];
    if (manager) {
        salary += [self groupBonus];
    }

    return salary;

}


//当月奖金
-(NSInteger)monthBonus:(NSInteger)monthSales{
    return monthSales * 0.003;
}


//累积奖金
-(NSInteger)sumBonus:(NSInteger)sumSales{
    return  sumSales * 0.001;
}

//团队奖金
-(NSInteger)groupBonus{
    //简单起见,团队的销售总额设置为100000
    return 100000 * 0.01;
}
@end

测试下:

calculateBonus *calculate = [calculateBonus new];  
NSInteger salary1 = [calculate calculateSalary:12222 sumSales:12000 isManager:YES];  
NSLog(@"经理工资:%zd", salary1);

NSInteger salary2 = [calculate calculateSalary:21333 sumSales:23111 isManager:NO];  
NSLog(@"员工甲:%zd", salary2);

NSInteger salary3 = [calculate calculateSalary:22113 sumSales:11222 isManager:NO];  
NSLog(@"员工乙:%zd", salary3);

输出:

2016-12-14 08:57:58.600 装饰者模式[64313:1880733] 经理工资:9048  
2016-12-14 08:57:58.601 装饰者模式[64313:1880733] 员工甲:8086  
2016-12-14 08:57:58.601 装饰者模式[64313:1880733] 员工乙:8077  
Program ended with exit code: 0

看起来运行良好,好,该来的还是来了,改需求。

现在要增加一个环比奖金:就是本月销售额比上月又增加,然后达到一定比例,就会有奖金,增加越多,奖金比率越高。你说这还不简单,再加一个环比奖金计算方法不就是了。那么如果工资计算的方式也换了呢?不同级别的人员或者员工的计算奖金的方式也换了呢?

假设

  • 甲的总工资 = 基本工资 + 当月销售奖金 + 环比奖金

  • 乙的工资 = 基本工资 + 环比奖金

  • 丙的工资 = 基本工资 + 当月销售奖金

  • 丁的工资 = 基本工资 + 环比奖金 + 团队奖金

后面再给你制定十几种不同的计算方式,崩溃了没有?按照上面的写法,那么每一种总工资计算方式你都要写一种方法,再假设这些人的总工资计算方式每个季度还会有调整,你怎么办,接着改?

仔细分析下上面的需求,总工资的计算有两部分:基本工资加上各种奖金,基本工资是固定的,麻烦的地方就在于奖金的计算方式是动态变化的,有各种各样的组合方式。按照设计模式的思想:封装变化,这里变化的部分是奖金的组合方式,那么我们就把这部分封装起来。

我们可以采取这样的方式,把每种奖金的计算方式都封装成单独的类,然后需要用到哪种奖金计算方式,就把这个奖金计算方式和基本工资组合起来,需要多少种奖金计算方式,就组合多少种。这样实现起来,是不是非常灵活?以后你想修改或者增加减少奖金计算方式,只需要修改或者增加减少一个奖金计算方式就可以了,至于每个人的总工资计算方式各不相同,就更简单了,交给客户端自由组合。

总结起来就是如下三个要求:

  • 奖金计算方式要灵活,可以动态增加或者减少

  • 可以动态组合奖金计算方式

要实现上面的功能,就要清楚我们今天的猪脚:装饰器模式。

下面来认识下这位仁兄


定义

动态地给一个对象添加一些额外的职责。就增加功能来说, 装饰模式比生成子类更为灵活。

一般我们在给一个原本的类添加功能的时候,都会想到使用继承,在原有的类上扩展新的功能,但是继承有一个非常大的缺点:和父类耦合性太高。如果后续需要添加或者减少功能,就不得不每次都要修改子类,而且如果修改了父类,对子类的影响也非常大。

所以我们一般优先考虑使用组合来实现功能的扩展,这也是设计模式的一个原则:多用组合,少用继承。装饰器模式就是实现组合功能的一种方式,它可以透明的给原本的类增加或者减少功能,而且可以把多个功能组合在一起,他不会改变原有类的功能,只是在原来功能的基础上加上一些新功能,而这些操作被装饰对象是毫不知情的。

比如上面的计算总工资,原有的对象是基本工资,但是需要在基本工资的基础上加上各种奖金,也就是给基本工资扩展了功能,但是基本工资这个原有功能是不会改变的,只是给他加上了各种各样的奖金,丰富了它的功能,最后算出来的还是工资,也就是保持原有的类型(整数型)不改变,这点要切记。


UML结构如及说明

image

实现装饰器模式要注意如下几点:

  1. 接 口 的 一 致 性

    装饰对象的接口必须与它所装饰的 C o m p o n e n t 的 接 口 是 一 致 的 , 因 此 , 所有的 concreteDecorator 类必须有一个公共的父类

  2. 省略抽象的 D e c o r a t o r 类

    当你仅需要添加一个职责时,没有必要定义抽象 D e c o r a t o r 类。 你常常需要处理现存的类层次结构而不是设计一个新系统,这时你可以把 D e c o r a t o r 向 C o m p o n e n t 转发请求的职责合并到 C o n c r e t e D e c o r a t o r 中。

  3. 保持 C o m p o n e n t 类 的 简 单 性

    为 了 保 证 接 口 的 一 致 性 , 组 件 和 装 饰 必 须 有 一 个 公 共 的 C o m p o n e n t 父类。因此保持这个类的简单性是很重要的;即,它应集中于定义接口而不是存储数据。对数据表示的定义应延迟到子类中,否则 C o m p o n e n t 类 会 变 得 过 于 复 杂 和 庞 大 , 因 而难以大量使用。赋予 C o m p o n e n t太 多 的 功 能 也 使 得 , 具 体 的 子 类 有 一 些 它 们 并 不 需 要 的 功 能的可能性大大增加。

  4. 改 变 对 象 外 壳 与 改 变 对 象 内 核

    我们可以将 D e c o r a t o r 看 作 一 个 对 象 的 外 壳 , 它 可 以 改 变这个对象的行为。另外一种方法是改变对象的内核。例如, S t r a t e g y 模 式 就 是 一 个 用 于 改变内核的很好的模式。 当 C o m p o n e n t 类 原 本 就 很 庞 大 时 , 使 用 D e c o r a t o r 模 式 代 价 太 高 , S t r a t e g y模 式 相 对 更 好 一 些。在 S t r a t e g y 模式中,组件将它的一些行为转发给一个独立的策略对象,我们可以替换s t r a t e g y 对象,从而改变或扩充组件的功能。

image


代码实现

1、定义抽象基类

先定义一个抽象基类,工资类和奖金计算方式类都继承自这个类,该类定义了一个公开接口,用于计算奖金

#import <Foundation/Foundation.h>

@interface component : NSObject
-(NSInteger)calculateSalary:(NSInteger)monthSales  sumSales:(NSInteger)sumSales;
@end

=================

2、定义工资类(被装饰对象)

#import "component.h"

@interface concreteComponent : component

@end

======================

//被装饰对象,基本工资

#import "concreteComponent.h"

@implementation concreteComponent

-(NSInteger)calculateSalary:(NSInteger)monthSales  sumSales:(NSInteger)sumSales{
    //基本工资8000
    return 8000;
}

@end

3、定义抽象装饰器

定义一个抽象装饰器,继承自抽象基类component,每个具体的装饰器继承自该类,该类主要做一些初始化工作

#import "component.h"

@interface Decorator : component
@property(strong,nonatomic)component *components;
- (instancetype)initWithComponet:(component *)component;
@end

=================

#import "Decorator.h"

@implementation Decorator
- (instancetype)initWithComponet:(component *)component
{
    self = [super init];
    if (self) {
        self.components = component;
    }
    return self;
}

-(NSInteger)calculateSalary:(NSInteger)monthSales sumSales:(NSInteger)sumSales{
    return [self.components calculateSalary:monthSales sumSales:sumSales];
}
@end

4、具体装饰器

每月销售奖金装饰器

#import "Decorator.h"

@interface monthBonusDecorator : Decorator

@end

==================


#import "monthBonusDecorator.h"

@implementation monthBonusDecorator

-(NSInteger)calculateSalary:(NSInteger)monthSales sumSales:(NSInteger)sumSales{
    NSInteger salary = [self.components calculateSalary:monthSales sumSales:sumSales];
    NSInteger bonus = monthSales * 0.03;
    NSLog(@"当月销售奖金:%zd", bonus);
    return salary += bonus;
}
@end

累积奖金装饰器

#import "Decorator.h"

@interface sumBonusDecatorator : Decorator

@end

================

#import "sumBonusDecatorator.h"

@implementation sumBonusDecatorator

-(NSInteger)calculateSalary:(NSInteger)monthSales sumSales:(NSInteger)sumSales{
    NSInteger salary = [self.components calculateSalary:monthSales sumSales:sumSales];
    NSInteger bonus = sumSales * 0.01;
    NSLog(@"累积销售奖金:%zd", bonus);
    return salary += bonus;
}

@end

团队奖金装饰器

#import "Decorator.h"

@interface groupBonusDecorator : Decorator

@end

=================

#import "groupBonusDecorator.h"

@implementation groupBonusDecorator
-(NSInteger)calculateSalary:(NSInteger)monthSales sumSales:(NSInteger)sumSales{
    NSInteger salary = [self.components calculateSalary:monthSales sumSales:sumSales];
    NSInteger bonus = 100000 * 0.01;
    NSLog(@"团队奖金:%zd", bonus);
    return salary += bonus;
}
@end

5、测试

 //基本工资,被装饰对象
        component *c1 = [concreteComponent new];

        //装饰器
        Decorator *d1 = [[monthBonusDecorator alloc]initWithComponet:c1];
        Decorator *d2 = [[sumBonusDecatorator alloc]initWithComponet:d1];
        NSInteger salary1 = [d2 calculateSalary:10000 sumSales:12212];
        NSLog(@"\n奖金组合方式:当月销售奖金 + 累积销售奖金 \n 总工资 = %zd", salary1);

        NSLog(@"\n=============================================================================");

        Decorator *d3 = [[monthBonusDecorator alloc]initWithComponet:c1];
        Decorator *d4 = [[sumBonusDecatorator alloc]initWithComponet:d3];
        Decorator *d5 = [[groupBonusDecorator alloc]initWithComponet:d4];
        NSInteger salary2 = [d5 calculateSalary:12100 sumSales:12232];
        NSLog(@"\n奖金组合方式:当月销售奖金 + 累积销售奖金 + 团队奖金 \n 总工资 = %zd", salary2);


        NSLog(@"\n=============================================================================");

        Decorator *d6 = [[monthBonusDecorator alloc]initWithComponet:c1];
        Decorator *d7 = [[groupBonusDecorator alloc]initWithComponet:d6];
        NSInteger salary3 = [d7 calculateSalary:23111 sumSales:231111];
        NSLog(@"\n奖金组合方式:当月销售奖金 + 团队奖金 \n 总工资 = %zd", salary3);

输出如下

2016-12-14 10:34:31.280 装饰者模式[64586:1944336] 当月销售奖金:300  
2016-12-14 10:34:31.280 装饰者模式[64586:1944336] 累积销售奖金:122  
2016-12-14 10:34:31.280 装饰者模式[64586:1944336]  
奖金组合方式:当月销售奖金 + 累积销售奖金 
 总工资 = 8422

=============================================================================
2016-12-14 10:34:31.280 装饰者模式[64586:1944336] 当月销售奖金:363  
2016-12-14 10:34:31.280 装饰者模式[64586:1944336] 累积销售奖金:122  
2016-12-14 10:34:31.280 装饰者模式[64586:1944336] 团队奖金:1000  
2016-12-14 10:34:31.280 装饰者模式[64586:1944336]  
奖金组合方式:当月销售奖金 + 累积销售奖金 + 团队奖金 
 总工资 = 9485

=============================================================================
2016-12-14 10:34:31.281 装饰者模式[64586:1944336] 当月销售奖金:693  
2016-12-14 10:34:31.281 装饰者模式[64586:1944336] 团队奖金:1000  
2016-12-14 10:34:31.281 装饰者模式[64586:1944336]  
奖金组合方式:当月销售奖金 + 团队奖金 
 总工资 = 9693

6、小结

从上面的测试可以看出,不管是使用何种奖金组合方式,只需要调用对应的装饰器即可,非常灵活。通过上面的代码我们看到,装饰器是一层层包裹的,基本工资被月工资装饰器包裹,月工资装饰器被累积奖金装饰器包裹,累积装饰器被团队奖金装饰器包裹,当调用计算奖金的公式的时候,就会按照顺序层层递归调用每个装饰器的功能,到最后算出总工资,我们来用示意图看看调用过程。

由于每个装饰器之间是完全独立的,所以我们可以使用任何我们想要的方式去组合这些装饰器,比如多次重复调用同一个装饰器,调换装饰器的顺序等等。

image


适用性

在如下情况可以考虑使用对象组合

  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。

  • 处理那些可以撤消的职责。

  • 当不能采用生成子类的方法进行扩充时 一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。


优缺点

  1. 比继 承 更 灵 活

    与 对 象 的 静 态 继 承 ( 多 重 继 承 ) 相 比 , D e c o r a t o r 模 式 提 供 了 更 加 灵活的向对象添加职责的方式。可以用添加和分离的方法,用装饰在运行时刻增加和删除职 责。相比之下,继承机制要求为每个添加的职责创建一个新的子类。这会产生许多新的类,并且会增加系统的复杂度。此外,为一 个特定的 C o m p o n e n t 类 提 供 多 个 不 同 的 D e c o r a t o r 类 , 这 就 使 得 你 可 以 对 一 些 职 责 进 行 混 合 和 匹配。 使用 D e c o r a t o r 模式可以很容易地重复添加一个特性。

  2. 避 免 了高层次类 有 太 多 的 特 征

    D e c o r a t o r模 式 提 供 了 一 种 “ 即 用 即 付 ” 的 方 法来添加职责。它并不试图在一个复杂的可定制的类中支持所有可预见的特征,相反,你可 以定义一个简单的类,并且用 D e c o r a t o r 类 给 它 逐 渐 地 添 加 功 能 。 可 以 从 简 单 的 部 件 组 合 出 复 杂的功能。这样,应用程序不必为不需要的特征付出代价。同时也更易于不依赖于 D e c o r a t o r 所扩展(甚至是不可预知的扩展)的类而独立地定义新类型的 D e c o r a t o r 。 扩 展 一 个 复 杂 类 的 时候,很可能会暴露与添加的职责无关的细节。

  3. 产生 许 多 小 对 象

    采用 D e c o r a t o r 模 式 进 行 系 统 设 计 往 往 会 产 生 许 多 看 上 去 类 似 的 小 对 象 , 这些对象仅仅在他们相互连接的方式上有所不同,而不是它们的类或是它们的属性值有所不 同。尽管对于那些了解这些系统的人来说,很容易对它们进行定制,但是很难学习这些系统, 排错也很困难。


装饰器和AOP

关于面向切换编程的具体解释看这里

百度百科AOP解释

AOP一般用来实现如下功能:日志记录,性能统计,安全控制,事务处理,异常处理等等。将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

看上面的描述就知道,装饰器就是一个很好实现AOP的方式,因为装饰器就是在不改变原有功能的前提下对其进行透明扩展的。

我们目前有一个鸭子类,实现呱呱叫的一个方法,现在我希望在不改变原有功能的情况下,统计鸭子叫了多少次,这就是AOP中的日志记录功能,我们可以使用装饰器模式来实现,具体代码我就不贴了,直接看最后的demo。


装饰器在iOS中的运用

我们应该用过一些图片处理app,可以给图片加上各种各样的滤镜或者裁剪旋转图片等等功能,其实这些也可以使用装饰器来实现,可以把每个功能都实现为一个装饰器,然后用户选择使用什么功能,就给图片加上对应的装饰器去做处理,这样做是不是非常灵活?

其实在iOS里面已经为我们提供了类似装饰器模式的功能的方法:category。category也可以透明的为一个类添加方法,下面我就使用一个小demo来演示如何使用category和装饰者模式分别来实现图片的选择和阴影效果。具体见demo。


demo下载

装饰者模式Demo

鸭子叫声计数器Demo

装饰者模式和category Demo

comments powered by Disqus