iOS设计模式-装饰者模式
问题:
某煎饼店现有煎饼、手抓饼和烧饼三种商品出售,每种商品都可以另外加鸡蛋或者香肠,现要开发一套商品结算系统,那么如何快速灵活的获取商品信息和价格并且利于以后增加新产品的拓展降低维护成本是开发人员必须面对的一个问题。
不太优雅的实现
针对这个问题开发人员提出了一个基于集成复用的设计方案,其基本结构如下图:
图一.png
上图中在抽象类Product中声明了抽象方法price和descriptionString,其子类Battercake、Handcake、Shaobing分别实现了price和descriptionString方法,返回自己的价格和描述,然后再通过它们的子类去拓展加鸡蛋和加香肠的情况。分析这个设计方案,不难发现有如下几个问题:
1.系统拓展麻烦
如果有人点了即加鸡蛋又加香肠的煎饼,在上图中通过增加一个新类EggSausageBattercake来实现,该类既是 的子类,又是 的子类;但是在很多面向对象的编程语言中,如Java,Objective-C等都不支持多继承,因此这些语言不能通过多继承来实现对来自多个父类的重用。
2.代码重复
从上图可以看出来,不只是煎饼可以加鸡蛋,手抓饼和烧饼都可以加鸡蛋,因此在EggBattercake、EggHandcake、EggShaobing中都有addEgg方法。该方法的具体实现基本相同,代码重复,不利于对系统进行修改和维护。
3.系统庞大
类的数目非常多,如果增加新的商品或者新的品类系统都需要增加大量的具体类,这将导致系统变得非常庞大。
总之如上图不是一个很好的设计方案,那么怎么办呢?如何让系统中的类可以进行拓展有不会导致类的数目急剧增加?这个设计方案之所以有这么多问题主要原因在于复用机制不合理,上图采用了继承复用,在复用父类的方法后再增加新的方法来拓展功能。根据“合成复用原则”,在实现功能复用时,我们要多用关联,少用继承,因此我们可以换个角度来考虑,将,addEgg方法抽取出来,封装在一个独立的类中,在这个类中定义一个Product类型的对象,通过调用Product的price方法来获取产品的基本价格,在通过addEgg方法计算最新的价格。由于Battercake、Handcake、Shaobing都是Product的子类,根据“里氏代换原则”,程序在运行时我们只要想这个独立的类中注入具体的Product子类的对象即可实现功能的拓展。这个独立的类一般成为装饰器(Decorator),它的作用就是对原有对象进行装饰,通过装饰来拓展原有对象的功能。
装饰类的引用大大简化了本系统的设计,它也是装饰模式的核心,下面我们将要对装饰模式进行介绍。
装饰者模式
装饰者模式(Decorator Pattern):动态的个一个对象增加一些额外的职责,就增加对象功能来说,装饰者模式比生成子类实现更为灵活,装饰者模式是一种对象结构型模式。
类图
图二 装饰者模式.jpg角色
- Component(抽象构件): 它是具体构件和抽象装饰类的共同父类或者是Protocol,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。
@protocol BattercakeProtocol <NSObject>
- (NSString *)descriptionString;
- (float)price;
@end
- ConcreteComponent(具体构件): 它是抽象构件类的子类,或者遵循协议。用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)。
@interface Battercake : NSObject<BattercakeProtocol>
@end
@implementation Battercake
- (NSString *)descriptionString {
return @"煎饼";
}
- (float)price {
return 8;
}
@end
- Decorator(抽象装饰类): 它也是抽象构件类的子类,或者遵循协议,用于给具体构件增加职责,但是具体职责在其子类中实现。它维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。
//在抽象装饰类Decorator中定义了一个Component类型的对象component,维持一个对抽象构件对象的引用,并可以通过构造方法或Setter方法将一个Component类型的对象注入进来,同时由于Decorator类实现了抽象构件Component接口,因此需要实现在其中声明的业务方法operation(),需要注意的是在Decorator中并未真正实现operation方法,而只是调用原有component对象的operation方法,它没有真正实施装饰,而是提供一个统一的接口,将具体装饰过程交给子类完成。
//装饰模式的核心在于抽象装饰类的设计,其典型的代码如下:
@interface AbstractDecorator : NSObject<BattercakeProtocol>
@property (nonatomic, strong) id<BattercakeProtocol>concreteComponent;
+ (instancetype)decoratorFor:(id<BattercakeProtocol>)concrete;
@end
@implementation AbstractDecorator
+ (instancetype)decoratorFor:(id<BattercakeProtocol>)concrete {
AbstractDecorator *decorator = [self new];
decorator.concreteComponent = concrete;
return decorator;
}
- (NSString *)descriptionString {
return [self.concreteComponent descriptionString];
}
- (float)price {
return [self.concreteComponent price];
}
@end
- ConcreteDecorator(具体装饰类):它是抽象装饰类的子类,负责向构件添加新的职责。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。
@interface EggDecorator : AbstractDecorator
@end
@implementation EggDecorator
- (NSString *)descriptionString {
return [NSString stringWithFormat:@"%@ %@", [super descriptionString], @"加一个鸡蛋"];
}
- (float)price {
return [super price] + 1;
}
@end
- 客户端调用
id<BattercakeProtocol> battercake = [Battercake new];
battercake = [EggDecorator decoratorFor:battercake];
battercake = [EggDecorator decoratorFor:battercake];
battercake = [SausageDecorator decoratorFor:battercake];
NSLog(@"%@, 价格:%@", [battercake descriptionString], @([battercake price]));
Log: 煎饼 加一个鸡蛋 加一个鸡蛋 加一跟香肠, 价格:12
当测试运行的时候会按照装饰器的组合顺序,依次调用相应的装饰器来执行业务功能,是一个递归的调用方法,以煎饼加鸡蛋加培根加香肠做例子,画个图来说明煎饼价格的计算过程吧,看看是如何调用的,如下图所示:
图三.png
这个图很好的揭示了装饰模式的组合和调用过程,请仔细体会一下。
如同上面的示例,对于基本的Battercake的对象而言,由于可以增加多种类别的辅料,为了灵活性,
把多种辅料的价格分散到不同的装饰器对象里面,采用动态组合的方式,来给基本的Battercake对象增添计算价格的功能,每个装饰器相当于价格计算的一个部分。
这种方式明显比为Battercake对象增加子类来得更灵活,因为装饰模式的起源点是采用对象组合的方式,然后在组合的时候顺便增加些功能。为了达到一层一层组装的效果,装饰模式还要求装饰器要实现与被装饰对象相同的业务接口,这样才能以同一种方式依次组合下去。
灵活性还体现在动态上,如果是继承的方式,那么所有的类实例都有这个功能了,而采用装饰模式,可以动态的为某几个对象实例添加功能,而不是对整个类添加功能。比如加鸡蛋的煎饼和加香肠的煎饼还有加鸡蛋和香肠的煎饼都使用的是Battercake类,只是动态的为它增加的功能不同而已。
模式讲解
1.本质
装饰模式的本质:功能细化,动态组合。动态是手段,组合才是目的。这里的组合有两个意思,一个是动态功能的组合,也就是动态进行装饰器的组合;另外一个是指对象组合,通过对象组合来实现为被装饰对象透明的增加功能。
但是要注意,装饰模式不仅仅可以增加功能,也可以控制功能的访问,可以完全实现新的功能,还可以控制装饰的功能是在被装饰功能之前还是之后来运行等。
总之,装饰模式是通过把复杂功能简单化,分散化,然后在运行期间,根据需要来动态组合的这么一个模式。
2. 模式功能
装饰模式能够实现动态的为对象添加功能,是从一个对象外部来给对象增加功能,相当于是改变了对象的外观。当装饰过后,从外部使用系统的角度看,就不再是使用原始的那个对象了,而是使用被一系列的装饰器装饰过后的对象。
这样就能够灵活的改变一个对象的功能,只要动态组合的装饰器发生了改变,那么最终所得到的对象的功能也就发生了改变。
变相的还得到了另外一个好处,那就是装饰器功能的复用,可以给一个对象多次增加同一个装饰器,也可以用同一个装饰器装饰不同的对象。
3. 对象组合
前面已经讲到了,一个类的功能的扩展方式,可以是继承,也可以是功能更强大、更灵活的对象组合的方式。
其实,现在在面向对象设计中,有一条很基本的规则就是“尽量使用对象组合,而不是对象继承”来扩展和复用功能。装饰模式的思考起点就是这个规则。
4. 装饰器
装饰器实现了对被装饰对象的某些装饰功能,可以在装饰器里面调用被装饰对象的功能,获取相应的值,这其实是一种递归调用。
在装饰器里不仅仅是可以给被装饰对象增加功能,还可以根据需要选择是否调用被装饰对象的功能,如果不调用被装饰对象的功能,那就变成完全重新实现了,相当于动态修改了被装饰对象的功能。
另外一点,各个装饰器之间最好是完全独立的功能,不要有依赖,这样在进行装饰组合的时候,才没有先后顺序的限制,也就是先装饰谁和后装饰谁都应该是一样的,否则会大大降低装饰器组合的灵活性。
5. 装饰器和组件类的关系
装饰器是用来装饰组件的,装饰器一定要实现和组件类一致的接口,保证它们是同一个类型,并具有同一个外观,这样组合完成的装饰才能够递归的调用下去。
组件类是不知道装饰器的存在的,装饰器给组件添加功能是一种透明的包装,组件类毫不知情。需要改变的是外部使用组件类的地方,现在需要使用包装后的类,接口是一样的,但是具体的实现类发生了改变。
6. 退化形式
如果仅仅只是想要添加一个功能,就没有必要再设计装饰器的抽象类了,直接在装饰器里面实现跟组件一样的接口,然后实现相应的装饰功能就可以了。但是建议最好还是设计上装饰器的抽象类,这样有利于程序的扩展。
透明装饰模式与半透明装饰模式
在上面的示例中,装饰后的对象是通过抽象对象id<BattercakeProtocol> 的变量来引用的,在鸡蛋装饰器这个类中我们新增了 egg 方法,如果此时我们想要单独调用该方法是调用不到的
除非引用变量的类型改为 EggDecorator,这样就可以调用了
EggDecorator *eggBattercake = [EggDecorator new];
[eggBattercake egg];
在实际使用过程中,由于新增行为可能需要单独调用,因此这种形式的装饰模式也经常出现,这种装饰模式被称为半透明(Semi-transparent)装饰模式,而标准的装饰模式是透明(Transparent)装饰模式。
1. 透明装饰模式
在透明装饰模式中,要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该将对象声明为具体构件类型或具体装饰类型,而应该全部声明为抽象构件类型。
2. 半透明装饰模式
透明装饰模式的设计难度较大,而且有时我们需要单独调用新增的业务方法。为了能够调用到新增方法,我们不得不用具体装饰类型来定义装饰之后的对象,而具体构件类型还是可以使用抽象构件类型来定义,这种装饰模式即为半透明装饰模式。
半透明装饰模式可以给系统带来更多的灵活性,设计相对简单,使用起来也非常方便;但是其最大的缺点在于不能实现对同一个对象的多次装饰,而且客户端需要有区别地对待装饰之前的对象和装饰之后的对象。
优缺点及场景
主要优点
-
对于扩展一个对象的功能,装饰模式比继承更加灵活性,不会导致类的个数急剧增加。
-
可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的具体装饰类,从而实现不同的行为。
-
可以对一个对象进行多次装饰,通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合,得到功能更为强大的对象。
-
具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,原有类库代码无须改变,符合“开闭原则”。
主要缺点
-
使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,大量小对象的产生势必会占用更多的系统资源,在一定程序上影响程序的性能。
-
装饰模式提供了一种比继承更加灵活机动的解决方案,但同时也意味着比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐。
适用场景
-
在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
-
当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类已定义为不能被继承(如Java语言中的final类)。