@qidiandasheng
2021-01-11T13:59:35.000000Z
字数 16613
阅读 2049
架构
代理、桥接、装饰器、适配器,这4种模式是比较常用的结构型设计模式。它们的代码结构非常相似。笼统来说,它们都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封 装原始类。
尽管代码结构相似,但这 4 种设计模式的用意完全不同,也就是说要解决的问题、应用场景不同,这也是它们的主要区别。
代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。
装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持 多个装饰器的嵌套使用。
适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理 模式、装饰器模式提供的都是跟原始类相同的接口。
代理模式和装饰器模式其实很想,都是使用“组合关系”这种代码结构的设计模式,只是他们的使用场景稍微有点不同。
代理模式中,代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。
// 代理模式的代码结构(下面的接口也可以替换成抽象类)public interface IA {void f();}public class A impelements IA {public void f() { //... }}public class AProxy impements IA {private IA a;public AProxy(IA a) {this.a = a;}public void f() {// 新添加的代理逻辑a.f();// 新添加的代理逻辑}}
// 装饰器模式的代码结构(下面的接口也可以替换成抽象类)public interface IA {void f();}public class A impelements IA {public void f() { //... }}public class ADecorator impements IA {private IA a;public ADecorator(IA a) {this.a = a;}public void f() {// 功能增强代码a.f();// 功能增强代码}}
代理模式(Proxy Pattern) :为某个对象提供一个代理,并由这个代理对象控制对原对象的访问。
定义解读:使用代理模式以后,客户端直接访问代理,代理在客户端和目标对象之间起到中介的作用。
在某些情况下,一个客户不想或者不能直接引用一个对象,此时可以通过一个称之为“代理”的第三者来实现间接引用。
因为代理对象可以在客户端和目标对象之间起到中介的作用,因此可以通过代理对象去掉客户不能看到的内容和服务或者添加客户需要的额外服务。
代理模式常用在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。我们将这些附加功能与业务功能解耦,放到代理类统一处理,让程序员只需要关注业务方面的开发。
代理模式算上客户端一共有四个成员:

在这里举一个买房者通过买房中介买房的例子。现在一般我们买房子不直接接触房东,而是先接触中介,买房的相关合同和一些事宜可以先和中介进行沟通。
在本例中,我们在这里让买房者直接支付费用给中介,然后中介收取一部分的中介费,再将剩余的钱交给房东。中介作为房东的代理,与买房者直接接触。而且中介还需要在真正交易前做其他的事情(收取中介费,帮买房者check房源的真实性等等),因此该场景比较适合使用代理模式。
根据上面的代理模式的成员:
定义房东和代理需要实现的抽象接口PaymentInterface:
@protocol PaymentInterface <NSObject>- (void)getPayment:(double)money;@end
代理类HouseProxy:
@interface HouseProxy : NSObject<PaymentInterface>@endconst float agentFeeRatio = 0.35;@interface HouseProxy()@property (nonatomic, copy) HouseOwner *houseOwner;@end@implementation HouseProxy- (void)getPayment:(double)money{double agentFee = agentFeeRatio * money;NSLog(@"Proxy get payment : %.2lf",agentFee);[self.houseOwner getPayment:(money - agentFee)];}- (HouseOwner *)houseOwner{if (!_houseOwner) {_houseOwner = [[HouseOwner alloc] init];}return _houseOwner;}@end
在HouseProxy里面,持有了房东,也就是被代理者的实例。然后在的getPayment:方法里,调用了房东实例的getPayment:方法。而且我们可以看到,在调用房东实例的getPayment:方法,代理先拿到了中介费(中介费比率agentFeeRatio定义为0.35,即中介费的比例占35%)。
这里面除了房东实例的getPayment:方法之外的一些操作就是代理存在的意义:它可以在真正被代理对象做事情之前,之后做一些其他额外的事情。比如类似AOP编程一样,定义类似的before***Method或是after**Method方法等等。
房东类(HouseOwner):
@interface HouseOwner : NSObject<PaymentInterface>@end@implementation HouseOwner- (void)getPayment:(double)money{NSLog(@"House owner get payment : %.2lf",money);}@end
房东类HouseOwner按照自己的方式实现了getPayment:方法。
很多时候被代理者(委托者)可以完全按照自己的方式去做事情,而把一些额外的事情交给代理来做,这样可以保持原有类的功能的纯粹性,符合开闭原则。
使用:
//客户端支付给了中介100元。HouseProxy *proxy = [[HouseProxy alloc] init];[proxy getPayment:100];
下面我们看一下打印结果,中介费收取了35%的中介费,剩下的交给了房东:
Proxy get payment : 35.00House owner get payment : 65.00

NSProxy可以为持有的对象进行消息转发来实现代理模式(消息转发式的代理实现)UITableView使用了代理模式桥接模式(Simple Factory Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。
定义解读:桥接模式的核心是两个抽象以组合的形式关联到一起,从而他们的实现就互不依赖了。
如果一个系统存在两个独立变化的维度,而且这两个维度都需要进行扩展的时候比较适合使用桥接模式。
桥接模式一共只有三个成员:
注意:这里提到的内容与编程语言中的接口或抽象类无关,它们并不是一回事。

从类图中可以看出
Abstraction持有Implementor,但是二者的实现类互不依赖。这就是桥接模式的核心。
创建一些不同的形状,这些形状带有不同的颜色。
三种形状:
三种颜色:
根据上述需求,可能有的朋友会这么设计:
这样的设计确实可以实现上面的需求。但是设想一下,如果后来增加了一种颜色或者形状的话,是不是要多出来很多类?如果形状的种类数是m,颜色的种类数是n,以这种方式创建的总类数就是m*n,当m或n非常大的时候,它们相乘的结果就会变得很大。
我们观察一下这个场景:形状和颜色这二者的是没有关联性的,二者可以独立扩展和变化,这样的组合比较适合用桥接模式来做。
根据上面提到的桥接模式的成员:
首先我们创建形状的基类Shape:
//================== Shape.h ==================@interface Shape : NSObject{@protected Color *_color;}- (void)renderColor:(Color *)color;- (void)show;@end//================== Shape.m ==================@implementation Shape- (void)renderColor:(Color *)color{_color = color;}- (void)show{NSLog(@"Show %@ with %@",[self class],[_color class]);}@end
由上面的代码可以看出:
- 形状类
Shape持有Color类的实例,二者是以组合的形式结合到一起的。而且Shape类定义了供外部传入Color实例的方法renderColor::在这个方法里面接收从外部传入的Color实例并保存起来。- 另外一个公共接口
show实际上就是打印这个图形的名称及其所搭配的颜色,便于我们后续验证。
接着我们创建三种不同的图形类,它们都继承于Shape类:
//正方形类@interface Square : Shape@end@implementation Square- (void)show{[super show];}@end//长方形类@interface Rectangle : Shape@end@implementation Rectangle- (void)show{[super show];}@end//圆形类@interface Circle : Shape@end@implementation Circle- (void)show{[super show];}@end
所有颜色类的父类Color:
@interface Color : NSObject@end@implementation Color@end
接着我们创建继承这个Color类的三个颜色类:
//红色类@interface RedColor : Color@end@implementation RedColor@end//绿色类@interface GreenColor : Color@end@implementation GreenColor@end//蓝色类@interface BlueColor : Color@end@implementation BlueColor@end
我们看一下客户端是如何使用它们来组合成不同的带有颜色的形状的:
//create 3 shape instancesRectangle *rect = [[Rectangle alloc] init];Circle *circle = [[Circle alloc] init];Square *square = [[Square alloc] init];//create 3 color instancesRedColor *red = [[RedColor alloc] init];GreenColor *green = [[GreenColor alloc] init];BlueColor *blue = [[BlueColor alloc] init];//rect & red color[rect renderColor:red];[rect show];//rect & green color[rect renderColor:green];[rect show];//circle & blue color[circle renderColor:blue];[circle show];//circle & green color[circle renderColor:green];[circle show];//square & blue color[square renderColor:blue];[square show];//square & red color[square renderColor:red];[square show];
上面的代码里,我们先声明了所有的形状类和颜色类的实例,然后自由搭配,形成不同的形状+颜色的组合。
下面我们通过打印的结果来看一下组合的效果:
Show Rectangle with RedColorShow Rectangle with GreenColorShow Circle with BlueColorShow Circle with GreenColorShow Square with BlueColorShow Square with RedColor
从打印的接口可以看出组合的结果是没问题的。
跟上面没有使用桥接模式的设计相比,使用桥接模式需要的类的总和是 m + n:当m或n的值很大的时候是远小于 m * n(没有使用桥接,而是使用继承的方式)的。
而且如果后面还要增加形状和颜色的话,使用桥接模式就可以很方便地将原有的形状和颜色和新的形状和颜色进行搭配了,新的类和旧的类互不干扰。

从UML类图可以看出,该设计是由两个抽象层的类
Shape和Color构建的,正因为依赖的双方都是抽象类(而不是具体的实现),而且二者是以组合的方式联系到一起的,所以扩展起来非常方便,互不干扰。这对于今后我们对代码的设计有比较好的借鉴意义。
利用桥接模式拆分程序中同时管理遥控器及其设备的庞杂代码。遥控器Remote类则作为抽象部分,设备Device类作为实现部分。
遥控器基类声明了一个指向设备对象的引用成员变量。所有遥控器通过通用设备接口与设备进行交互,使得同一个遥控器可以支持不同类型的设备。
你可以开发独立于设备类的遥控器类,只需新建一个遥控器子类即可。例如基础遥控器可能只有两个按钮,但你可在其基础上扩展新功能,比如开关设备。
客户端代码通过遥控器构造函数将特定种类的遥控器与设备对象连接起来。
遥控器(抽象部分)
/*“抽象部分”定义了两个类层次结构中“控制”部分的接口。它管理着一个指向“实现部分”层次结构中对象的引用,并会将所有真实工作委派给该对象*/class RemoteControl isprotected field device: Deviceconstructor RemoteControl(device: Device) isthis.device = devicemethod togglePower() isif (device.isEnabled()) thendevice.disable()elsedevice.enable()method volumeDown() isdevice.setVolume(device.getVolume() - 10)method volumeUp() isdevice.setVolume(device.getVolume() + 10)method channelDown() isdevice.setChannel(device.getChannel() - 1)method channelUp() isdevice.setChannel(device.getChannel() + 1)// 从抽象层中扩展遥控器子类class AdvancedRemoteControl extends RemoteControl ismethod mute() isdevice.setVolume(0)
设备(实现部分)
/*“实现部分”接口声明了在所有具体实现类中通用的方法。它不需要与抽象接口相匹配。实际上,这两个接口可以完全不一样。通常实现接口只提供原语操作,而抽象接口则会基于这些操作定义较高层次的操作。*/interface Device ismethod isEnabled()method enable()method disable()method getVolume()method setVolume(percent)method getChannel()method setChannel(channel)//所有设备都遵循相同的接口。class Tv implements Device is// ...class Radio implements Device is// ...
客户端调用:
tv = new Tv()remote = new RemoteControl(tv)remote.togglePower()radio = new Radio()remote = new AdvancedRemoteControl(radio)
装饰模式(Decorator Pattern) :不改变原有对象的前提下,动态地给一个对象增加一些额外的功能。
装饰者模式一共有四个成员:

对敏感数据进行压缩和加密, 从而将数据从使用数据的代码中独立出来。
程序使用一对装饰来封装数据源对象。 这两个封装器都改变了从磁盘读写数据的方式:
当数据即将被写入磁盘前, 装饰对数据进行加密和压缩。 在原始类对改变毫无察觉的情况下, 将加密后的受保护数据写入文件。
当数据刚从磁盘读出后, 同样通过装饰对数据进行解压和解密。 装饰和数据源类实现同一接口, 从而能在客户端代码中相互替换。
抽象构件(数据读写的抽象接口):
@protocol DataSource <NSObject>- (void)writeData:(NSData *)data;- (NSData *)readData;@end
具体构件:
//具体构件提供操作的默认实现。这些类在程序中可能会有几个变体(比如写入数据库)@interface FileDataSource : NSObject<DataSource>- (void)writeData:(NSData *)data;- (NSData *)readData;@end@implementation FileDataSource- (void)writeData:(NSData *)data{// 将数据写入文件}- (NSData *)readData{// 从文件读取数据NSData *data = nil;return data;}@end
装饰(Decorator):
/*装饰基类和其他组件遵循相同的接口。该类的主要任务是定义所有具体装饰的封装接口。封装的默认实现代码中可能会包含一个保存被封装组件的成员变量,并且负责对其进行初始化*/@interface DataSourceDecorator : NSObject<DataSource>- (void)writeData:(NSData *)data;- (NSData *)readData;@end==================================================@interface DataSourceDecorator(){id<DataSource> _wrappee;}@end@implementation DataSourceDecorator- (instancetype)initWithDataSource:(id<DataSource>)data{self = [super init];if (self) {_wrappee = data;}return self;}- (void)writeData:(NSData *)data{[_wrappee writeData:data];}- (NSData *)readData{return [_wrappee readData];}@end
具体装饰:
====================加密装饰器====================@interface EncryptionDecorator : DataSourceDecorator@end@implementation EncryptionDecorator- (void)writeData:(NSData *)data{// 1. 对传递数据进行加密。data = [Encryption encrypt:data];// 2. 将加密后数据传递给被封装对象 writeData(写入数据)方法。[super writeData:data];}- (NSData *)readData{// 1. 通过被封装对象的 readData(读取数据)方法获取数据。NSData *data = [super readData];// 2. 如果数据被加密就尝试解密。data = [Decryption decrypt:data];// 3. 返回结果。return data;}@end====================压缩装饰器====================@interface CompressionDecorator : DataSourceDecorator@end@implementation CompressionDecorator- (void)writeData:(NSData *)data{// 1. 压缩传递数据data = [Compression compress:data];// 2. 将压缩后数据传递给被封装对象 writeData(写入数据)方法[super writeData:data];}- (NSData *)readData{// 1. 通过被封装对象的 readData(读取数据)方法获取数据NSData *data = [super readData];// 2. 如果数据被压缩就尝试解压data = [Decompression decompress:data];// 3. 返回结果return data;}@end
客户端使用:
NSData *orderData = [NSData new];// 已将明码数据写入目标文件id<DataSource> source = [FileDataSource new];[source writeData:orderData];// 已将压缩数据写入目标文件source = [[EncryptionDecorator alloc] initWithDataSource:source];[source writeData:orderData];// 已将压缩且加密的数据写入目标文件source = [[CompressionDecorator alloc] initWithDataSource:source];[source writeData:orderData];
适配器模式(Adapter Pattern) :将一个接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。适配器模式的别名是包装器模式(Wrapper),是一种结构型设计模式。
定义解读:适配器模式又分为对象适配器和类适配器两种。
适配器模式有三个成员:
模拟一个替换缓存组件的场景:目前客户端已经依赖于旧的缓存组件的接口,而后来发现有一个新的缓组件的性能更好一些,需要将旧的缓存组件替换成新的缓存组件,但是新的缓存组件的接口与旧的缓存接口不一致,所以目前来看客户端是无法直接与新缓存组件一起工作的。
由于客户端在很多地方依赖了旧缓存组件的接口,将这些地方的接口都换成新缓存组件的接口会比较麻烦,而且万一后面还要换回旧缓存组件或者再换成另外一个新的缓存组件的话就还要做重复的事情,这显然是不够优雅的。
因此该场景比较适合使用适配器模式:创建一个适配器,让原本与旧缓存接口的客户端可以与新缓存组件一起工作。
在这里,新的缓存组件就是Adaptee,旧的缓存组件(接口)就是Target,因为它是直接和客户端接触的。而我们需要创建一个适配器类Adaptor来让客户端与新缓存组件一起工作。
首先我们创建旧缓存组件,并让客户端正常使用它。
先创建旧缓存组件的接口OldCacheProtocol:
对应Java的接口,Objective-C中叫做协议,也就是protocol。
//================== OldCacheProtocol.h ==================@protocol OldCacheProtocol <NSObject>- (void)old_saveCacheObject:(id)obj forKey:(NSString *)key;- (id)old_getCacheObjectForKey:(NSString *)key;@end
可以看到该接口包含了两个操作缓存的方法,方法前缀为
old。
再简单创建一个缓存组件类OldCache,它实现了OldCacheProtocol接口:
//================== OldCache.h ==================@interface OldCache : NSObject <OldCacheProtocol>@end//================== OldCache.m ==================@implementation OldCache- (void)old_saveCacheObject:(id)obj forKey:(NSString *)key{NSLog(@"saved cache by old cache object");}- (id)old_getCacheObjectForKey:(NSString *)key{NSString *obj = @"get cache by old cache object";NSLog(@"%@",obj);return obj;}@end
为了读者区分方便,将新旧两个缓存组件取名为
NewCache和OldCache。实现代码也比较简单,因为不是本文介绍的重点,只需区分接口名称即可。
现在我们让客户端来使用这个旧缓存组件:
//================== client.m ==================@interface ViewController ()@property (nonatomic, strong) id<OldCacheProtocol>cache;@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];//使用旧缓存[self useOldCache];//使用缓存组件操作[self saveObject:@"cache" forKey:@"key"];}//实例化旧缓存并保存在``cache``属性里- (void)useOldCache{self.cache = [[OldCache alloc] init];}//使用cache对象- (void)saveObject:(id)object forKey:(NSString *)key{[self.cache old_saveCacheObject:object forKey:key];}
- 在这里的客户端就是
ViewController,它持有一个遵从OldCacheProtocol协议的实例,也就是说它目前依赖于OldCacheProtocol的接口。useOldCache方法用来实例化旧缓存并保存在cache属性里。saveObject:forKey:方法是真正使用cache对象来保存缓存。
运行并打印一下结果输出是:saved cache by old cache object。现在看来客户端使用旧缓存是没有问题的。
新的缓存组件:
首先定义新缓存组件的接口NewCacheProtocol:
//================== NewCacheProtocol.h ==================@protocol NewCacheProtocol <NSObject>- (void)new_saveCacheObject:(id)obj forKey:(NSString *)key;- (id)new_getCacheObjectForKey:(NSString *)key;@end
可以看到,NewCacheProtocol与OldCacheProtocol接口大致是相似的,但是名称还是不同,这里使用了不同的方法前缀做了区分。
接着看一下新缓存组件是如何实现这个接口的:
//================== NewCache.h ==================@interface NewCache : NSObject <NewCacheProtocol>@end//================== NewCache.m ==================@implementation NewCache- (void)new_saveCacheObject:(id)obj forKey:(NSString *)key{NSLog(@"saved cache by new cache object");}- (id)new_getCacheObjectForKey:(NSString *)key{NSString *obj = @"saved cache by new cache object";NSLog(@"%@",obj);return obj;}@end
现在我们拿到了新的缓存组件,但是客户端类目前依赖的是旧的接口,因此适配器类应该上场了:
//================== Adaptor.h ==================@interface Adaptor : NSObject <OldCacheProtocol>- (instancetype)initWithNewCache:(NewCache *)newCache;@end//================== Adaptor.m ==================@implementation Adaptor{NewCache *_newCache;}- (instancetype)initWithNewCache:(NewCache *)newCache{self = [super init];if (self) {_newCache = newCache;}return self;}- (void)old_saveCacheObject:(id)obj forKey:(NSString *)key{//transfer responsibility to new cache object[_newCache new_saveCacheObject:obj forKey:key];}- (id)old_getCacheObjectForKey:(NSString *)key{//transfer responsibility to new cache objectreturn [_newCache new_getCacheObjectForKey:key];}@end
- 首先,适配器类也实现了旧缓存组件的接口;目的是让它也可以接收到客户端操作旧缓存组件的方法。
- 然后,适配器的构造方法里面需要传入新组件类的实例;目的是在收到客户端操作旧缓存组件的命令后,将该命令转发给新缓存组件类,并调用其对应的方法。
- 最后我们看一下适配器类是如何实现两个旧缓存组件的接口的:在
old_saveCacheObject:forKey:方法中,让新缓存组件对象调用对应的new_saveCacheObject:forKey:方法;同样地,在old_getCacheObjectForKey方法中,让新缓存组件对象调用对应的new_getCacheObjectForKey:方法。
这样一来,适配器类就定义好了。
那么最后我们看一下在客户端里面是如何使用适配器的:
//================== client ==================- (void)viewDidLoad {[super viewDidLoad];//使用新缓存组件[self useNewCache];[self saveObject:@"cache" forKey:@"key"];}- (void)useOldCache{self.cache = [[OldCache alloc] init];}//使用新缓存组件- (void)useNewCache{self.cache = [[Adaptor alloc] initWithNewCache:[[NewCache alloc] init]];}//使用cache对象- (void)saveObject:(id)object forKey:(NSString *)key{[self.cache old_saveCacheObject:object forKey:key];}
我们可以看到,在客户端里面,只需要改一处就可以了:将我们定义好的适配器类保存在原来的
cache属性中就可以了(useNewCache方法的实现)。而真正操作缓存的方法saveObject:forKey不需要有任何改动。
我们可以看到,使用适配器模式,客户端调用旧缓存组件接口的方法都不需要改变;只需稍作处理,就可以在新旧缓存组件中来回切换,也不需要原来客户端对缓存的操作。
而之所以可以做到这么灵活,其实也是因为在一开始客户端只是依赖了旧缓存组件类所实现的接口,而不是旧缓存组件类的类型。有心的读者可能注意到了,上面viewController的属性是@property (nonatomic, strong) id<OldCacheProtocol>cache;。正因为如此,我们新建的适配器实例才能直接用在这里,因为适配器类也是实现了<OldCacheProtocol>接口。相反,如果我们的cache属性是这么写的:@property (nonatomic, strong) OldCache *cache;,即客户端依赖了旧缓存组件的类型,那么我们的适配器类就无法这么容易地放在这里了。因此为了我们的程序在将来可以更好地修改和扩展,依赖接口是一个前提。

类适配器模式:

类适配器中采用了多继承的方式(多继承在Objective-C中可以通过遵循多个协议来实现):适配器同时继承了目标类和被适配者类,也就都持有了者二者的方法。
如果 Adaptee 接口很多,而且Adaptee和ITarget接口定义大部分都相同,那我们推 荐使用类适配器,因为Adaptor复用父类Adaptee的接口,比起对象适配器的实现方式,Adaptor 的代码量要少一些。
// 类适配器: 基于继承public interface ITarget {void f1();void f2();void fc();}public class Adapteepublic void fa() { //.....}public void fb() { //.....}public void fc() { //.....}}public class Adaptor extends Adaptee implements ITarget {public void f1() {super.fa();}public void f2() {//...重新实现f2()...}// 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点}

对象适配器中,被适配者的对象被适配器所持有。当适配器的
request方法被调用时,在这个方法内部再调用被适配者对应的方法。
如果 Adaptee接口很多,而且Adaptee和ITarget接口定义大部分都不相同,那我们推荐使用对象适配器,因为组合结构相对于继承更加灵活。
// 对象适配器:基于组合public interface ITarget {void f1();void f2();void fc();}public class Adaptee {public void fa() { //... }public void fb() { //... }public void fc() { //... }}public class Adaptor implements ITarget {private Adaptee adaptee;public Adaptor(Adaptee adaptee) {this.adaptee = adaptee;}public void f1() {adaptee.fa(); //委托给Adaptee}public void f2() {//...重新实现f2()...}public void fc() {adaptee.fc();}}