@qidiandasheng
2021-01-11T21:59:49.000000Z
字数 14160
阅读 1105
架构
这一篇主要介绍结构模式中的外观模式、组合模式、享元模式。相对于上一篇介绍的四种结构型模式,这一篇的三种结构型模式更不常用一些,他们针对的场景更加特殊,应用场景更加明确。
外观模式:是一种结构型设计模式,能为程序库、框架或其他复杂类提供一个简单的接口。
组合模式:使用它将对象组合成树状结构,并且能像使用独立对象一样使用它们。
享元模式:它摒弃了在每个对象中保存所有数据的方式,通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。
适配器模式和外观模式其实很类似,他们的共同点是将不好用的接口适配成好用的接口。
不同点主要是应用场景不同:
适配器是做接口转换,解决的是原接口和目标接口不匹配的问题。
外观模式做接口整合,解决的是多接口调用带来的问题。
组合模式跟我们之前讲的面向对象设计中的“组合关系(通过组合来组装两个类)”,完全是两码事。这里讲的“组合模式”,主要是用来处理树形结构数据。这里的“数据”,你可以简单理解为一组对象集合。
外观模式(Facade Pattern):外观模式定义了一个高层接口,为子系统中的一组接口提供一个统一的接口。外观模式又称为门面模式,它是一种结构型设计模式模式。
定义解读:通过这个高层接口,可以将客户端与子系统解耦:客户端可以不直接访问子系统,而是通过外观类间接地访问;同时也可以提高子系统的独立性和可移植性。
外观模式包括客户端共有三个成员:
客户端类(Client):客户端是意图操作子系统的类,它与外观类直接接触;与外观类间接接触
外观类(Facade):外观类知晓各个子系统的职责和接口,封装子系统的接口并提供给客户端
子系统类(SubSystem):子系统类实现子系统的功能,对外观类一无所知
模拟一个智能家居系统。这个智能家居系统可以用一个中央遥控器操作其所接入的一些家具:台灯,音箱,空调等等。
在这里我们简单操纵几个设备:
有的时候,我们需要某个设备可以一次执行两个不同的操作;也可能会需要多个设备共同协作来执行一些任务。比如:
假设我们可以用遥控器直接开启热风,那么实际上就是两个步骤:
我们把这两个步骤用一个操作包含起来,一步到位。像这样简化操作步骤的场景比较适合用外观模式。
同样的,我们想听歌的话,需要四个步骤:
这些步骤我们也可以装在单独的一个接口里面。
类似的,如果我们想看DVD的话,步骤会更多,因为DVD需要同时输出声音和影像:
这些接口也可以装在一个单独的接口里。
最后,如果我们要出门,需要关掉所有家用电器,也不需要一个一个将他们关掉,也只需要一个关掉的总接口就好了,因为这个关掉的总接口里面可以包含所有家用电器的关闭接口。
因此,这些设备可以看做是该智能家居系统的子系统;而这个遥控器则扮演的是外观类的角色。
下面我们用代码来看一下如何实现这些设计。
因为所有家用电器都有开启和关闭的操作,所以我们先创建一个家用电器的基类HomeDevice
:
//================== HomeDevice.h ==================
//设备基类
@interface HomeDevice : NSObject
//连接电源
- (void)on;
//关闭电源
- (void)off;
@end
然后是继承它的所有家用电器类:
空调类AirConditioner
:
//================== AirConditioner.h ==================
@interface AirConditioner : HomeDevice
//高温模式
- (void)startHighTemperatureMode;
//常温模式
- (void)startMiddleTemperatureMode;
//低温模式
- (void)startLowTemperatureMode;
@end
CD Player类:CDPlayer
:
//================== CDPlayer.h ==================
@interface CDPlayer : HomeDevice
- (void)play;
@end
DVD Player类:DVDPlayer
:
//================== DVDPlayer.h ==================
@interface DVDPlayer : HomeDevice
- (void)play;
@end
音箱类VoiceBox
:
//================== VoiceBox.h ==================
@class CDPlayer;
@class DVDPlayer;
@interface VoiceBox : HomeDevice
//与CDPlayer连接
- (void)connetCDPlayer:(CDPlayer *)cdPlayer;
//与CDPlayer断开连接
- (void)disconnetCDPlayer:(CDPlayer *)cdPlayer;
//与DVD Player连接
- (void)connetDVDPlayer:(DVDPlayer *)dvdPlayer;
//与DVD Player断开连接
- (void)disconnetDVDPlayer:(DVDPlayer *)dvdPlayer;
@end
投影仪类Projecter
:
//================== Projecter.h ==================
@interface Projecter : HomeDevice
//与DVD Player连接
- (void)connetDVDPlayer:(DVDPlayer *)dvdPlayer;
//与DVD Player断开连接
- (void)disconnetDVDPlayer:(DVDPlayer *)dvdPlayer;
@end
注意,音箱是可以连接CD Player和DVD Player的;而投影仪只能连接DVD Player
现在我们把所有的家用电器类和他们的接口都定义好了,下面我们看一下该实例的外观类HomeDeviceManager
如何设计。
首先我们看一下客户端期望外观类实现的接口:
//================== HomeDeviceManager.h ==================
@interface HomeDeviceManager : NSObject
//===== 关于空调的接口 =====
//空调吹冷风
- (void)coolWind;
//空调吹热风
- (void)warmWind;
//===== 关于CD Player的接口 =====
//播放CD
- (void)playMusic;
//关掉音乐
- (void)offMusic;
//===== 关于DVD Player的接口 =====
//播放DVD
- (void)playMovie;
//关闭DVD
- (void)offMoive;
//===== 关于总开关的接口 =====
//打开全部家用电器
- (void)allDeviceOn;
//关闭所有家用电器
- (void)allDeviceOff;
@end
上面的接口分为了四大类,分别是:
为了便于读者理解,这四类的接口所封装的子系统接口的数量是逐渐增多的。
在看这些接口时如何实现的之前,我们先看一下外观类是如何保留这些子系统类的实例的。在该代码示例中,这些子系统类的实例在外观类的构造方法里被创建,而且作为外观类的成员变量被保存了下来。
//================== HomeDeviceManager.m ==================
@implementation HomeDeviceManager
{
NSMutableArray *_registeredDevices;//所有注册(被管理的)的家用电器
AirConditioner *_airconditioner;
CDPlayer *_cdPlayer;
DVDPlayer *_dvdPlayer;
VoiceBox *_voiceBox;
Projecter *_projecter;
}
- (instancetype)init{
self = [super init];
if (self) {
_airconditioner = [[AirConditioner alloc] init];
_cdPlayer = [[CDPlayer alloc] init];
_dvdPlayer = [[DVDPlayer alloc] init];
_voiceBox = [[VoiceBox alloc] init];
_projecter = [[Projecter alloc] init];
_registeredDevices = [NSMutableArray arrayWithArray:@[_airconditioner,
_cdPlayer,
_dvdPlayer,
_voiceBox,
_projecter]];
}
return self;
}
其中 _registeredDevices
这个成员变量是一个数组,它包含了所有和这个外观类实例关联的子系统实例。
子系统与外观类的关联实现方式不止一种,不作为本文研究重点,现在只需知道外观类保留了这些子系统的实例即可。按照顺序,我们首先看一下关于空调的接口的实现:
//================== HomeDeviceManager.m ==================
//空调吹冷风
- (void)coolWind{
[_airconditioner on];
[_airconditioner startLowTemperatureMode];
}
//空调吹热风
- (void)warmWind{
[_airconditioner on];
[_airconditioner startHighTemperatureMode];
}
吹冷风和吹热风的接口都包含了空调实例的两个接口,第一个都是开启空调,第二个则是对应的冷风和热风的接口。
我们接着看关于CD Player的接口的实现:
//================== HomeDeviceManager.m ==================
- (void)playMusic{
//1. 开启CDPlayer开关
[_cdPlayer on];
//2. 开启音箱
[_voiceBox on];
//3. 音响与CDPlayer连接
[_voiceBox connetCDPlayer:_cdPlayer];
//4. 播放CDPlayer
[_cdPlayer play];
}
//关掉音乐
- (void)offMusic{
//1. 切掉与音箱的连接
[_voiceBox disconnetCDPlayer:_cdPlayer];
//2. 关掉音箱
[_voiceBox off];
//3. 关掉CDPlayer
[_cdPlayer off];
}
在上面的场景分析中提到过,听音乐这个指令要分四个步骤:CD Player和音箱的开启,二者的连接,以及播放CD Player,这也比较符合实际生活中的场景。关掉音乐也是先断开连接再切断电源(虽然直接切断电源也可以)。
接下来我们看一下关于DVD Player的接口的实现:
//================== HomeDeviceManager.m ==================
- (void)playMovie{
//1. 开启DVD player
[_dvdPlayer on];
//2. 开启音箱
[_voiceBox on];
//3. 音响与DVDPlayer连接
[_voiceBox connetDVDPlayer:_dvdPlayer];
//4. 开启投影仪
[_projecter on];
//5.投影仪与DVDPlayer连接
[_projecter connetDVDPlayer:_dvdPlayer];
//6. 播放DVDPlayer
[_dvdPlayer play];
}
- (void)offMoive{
//1. 切掉音箱与DVDPlayer连接
[_voiceBox disconnetDVDPlayer:_dvdPlayer];
//2. 关掉音箱
[_voiceBox off];
//3. 切掉投影仪与DVDPlayer连接
[_projecter disconnetDVDPlayer:_dvdPlayer];
//4. 关掉投影仪
[_projecter off];
//5. 关掉DVDPlayer
[_dvdPlayer off];
}
因为DVD Player要同时连接音箱和投影仪,所以这两个接口封装的子系统接口相对于CD Player的更多一些。
最后我们看一下关于总开关的接口的实现:
//================== HomeDeviceManager.m ==================
//打开全部家用电器
- (void)allDeviceOn{
[_registeredDevices enumerateObjectsUsingBlock:^(HomeDevice *device, NSUInteger idx, BOOL * _Nonnull stop) {
[device on];
}];
}
//关闭所有家用电器
- (void)allDeviceOff{
[_registeredDevices enumerateObjectsUsingBlock:^(HomeDevice *device, NSUInteger idx, BOOL * _Nonnull stop) {
[device off];
}];
}
这两个接口是为了方便客户端开启和关闭所有设备的,有这两个接口的话,用户就不用一一开启或关闭多个设备了。
关于这两个接口的实现:
上文说过,该外观类通过一个数组成员变量_registeredDevices
来保存所有可操作的设备。所以如果我们需要开启或关闭所有的设备就可以遍历这个数组并向每个元素调用on
或off
方法。因为这些元素都继承于HomeDevice
,也就是都有on
或off
方法。
这样做的好处是,我们不需要单独列出所有设备来分别调用它们的接口;而且后面如果添加或者删除某些设备的话也不需要修改这两个接口的实现了。
从上面的UML类图中可以看出,该示例的子系统之间的耦合还是比较多的;而外观类
HomeDeviceManager
的接口大大简化了User
对这些子系统的使用成本。
SDWebImage
封装了负责图片下载的类和负责图片缓存的类,而外部仅向客户端暴露了简约的下载图片的接口。组合模式是一种结构型设计模式,你可以使用它将对象组合成树状结构,并且能像使用独立对象一样使用它们。
如果你需要实现树状对象结构,可以使用组合模式。
组合模式为你提供了两种共享公共接口的基本元素类型:简单叶节点和复杂容器。容器中可以包含叶节点和其他容器。这使得你可以构建树状嵌套递归对象结构。
如果你希望客户端代码以相同方式处理简单和复杂元素, 可以使用该模式。
组合模式中定义的所有元素共用同一个接口。在这一接口的帮助下,客户端不必在意其所使用的对象的具体类。
设计一个类来表示文件系统中的目录,能方便地实现下面这些功能:
这里的文件系统就是一个树状结构,有两种基本元素简单叶节点(文件)和复杂容器(子目录)。所以比较适合使用组合模式。
基类(包含两种元素的共同接口):
@interface FileSystemNode : NSObject
@property(nonatomic, copy, readonly)NSString *path;
- (instancetype)initWithPath:(NSString *)path;
- (NSInteger)countNumOfFiles;
- (NSInteger)countSizeOfFiles;
@end
@implementation FileSystemNode
- (instancetype)initWithPath:(NSString *)path{
self = [super init];
if (self) {
_path = path;
}
return self;
}
@end
文件类:
@interface FileNode : FileSystemNode
@end
@implementation FileNode
- (NSInteger)countNumOfFiles{
return 1;
}
- (NSInteger)countSizeOfFiles{
//1. 计算文件大小
//2. 返回当前文件大小
return 0;
}
@end
目录类:
==========================DirectoryNode.h============================
@interface DirectoryNode : FileSystemNode
- (void)addSubNode:(FileSystemNode *)node;
- (void)removeSubNode:(FileSystemNode *)node;
@end
==========================DirectoryNode.m============================
@interface DirectoryNode()
@property(nonatomic, strong)NSMutableArray<FileSystemNode *> *subNodes;
@end
@implementation DirectoryNode
- (NSInteger)countNumOfFiles{
NSInteger numOfFiles = 0;
for (FileSystemNode *node in self.subNodes) {
numOfFiles += [node countNumOfFiles];
}
return numOfFiles;
}
- (NSInteger)countSizeOfFiles{
NSInteger sizeofFiles = 0;
for (FileSystemNode *node in self.subNodes) {
sizeofFiles += [node countSizeOfFiles];
}
return sizeofFiles;
}
- (void)addSubNode:(FileSystemNode *)node{
[self.subNodes addObject:node];
}
- (void)removeSubNode:(FileSystemNode *)node{
[self.subNodes removeObject:node];
}
- (NSMutableArray<FileSystemNode *> *)subNodes{
if (!_subNodes) {
_subNodes = [NSMutableArray new];
}
return _subNodes;
}
@end
客户端使用:
/**
/
/wz/
/wz/a.txt
/wz/b.txt
/wz/movies/
/wz/movies/c.avi
**/
DirectoryNode *fileSystemTree = [[DirectoryNode alloc] initWithPath:@"/"];
DirectoryNode *wz_node = [[DirectoryNode alloc] initWithPath:@"/wz/"];
[fileSystemTree addSubNode:wz_node];
FileNode *wz_a_node = [[FileNode alloc] initWithPath:@"/wz/a.txt"];
FileNode *wz_b_node = [[FileNode alloc] initWithPath:@"/wz/b.txt"];
DirectoryNode *wz_movies_node = [[DirectoryNode alloc] initWithPath:@"/wz/movies/"];
[wz_node addSubNode:wz_a_node];
[wz_node addSubNode:wz_b_node];
[wz_node addSubNode:wz_movies_node];
FileNode *wz_movies_c_node = [[FileNode alloc] initWithPath:@"/wz/movies/c.avi"];
[wz_movies_node addSubNode:wz_movies_c_node];
NSLog(@"%ld %ld",[fileSystemTree countNumOfFiles],[fileSystemTree countSizeOfFiles]);
享元模式(Flyweight Pattern):运用共享技术复用大量细粒度的对象,降低程序内存的占用,提高程序的性能。
定义解读:
享元对象能做到共享的关键是区分内部状态(Internal State)和外部状态(External State)。
享元模式一共有三个成员:
这里我们使用《Objective-C 编程之道:iOS设计模式解析》里的第21章使用的例子:在一个页面展示数百个大小,位置不同的花的图片,然而这些花的样式只有6种。
看一下截图:
由于这里我们需要创建很多对象,而这些对象有可以共享的内部状态(6种图片内容)以及不同的外部状态(随机的,数百个位置坐标和图片大小),因此比较适合使用享元模式来做。
根据上面提到的享元模式的成员:
UIImageView
,它可以显示图片UIImageView
,因为后续我们可以直接添加更多其他的属性。首先我们创建一个工厂,这个工厂可以根据所传入花的类型来返回花内部图片对象,在这里可以直接使用原生的UIImage
对象,也就是图片对象。而且这个工厂持有一个保存图片对象的池子:
下面我们看一下代码是如何实现的:
//================== FlowerFactory.h ==================
typedef enum
{
kAnemone,
kCosmos,
kGerberas,
kHollyhock,
kJasmine,
kZinnia,
kTotalNumberOfFlowerTypes
} FlowerType;
@interface FlowerFactory : NSObject
- (FlowerImageView *) flowerImageWithType:(FlowerType)type
@end
//================== FlowerFactory.m ==================
@implementation FlowerFactory
{
NSMutableDictionary *_flowersPool;
}
- (FlowerImageView *) flowerImageWithType:(FlowerType)type
{
if (_flowersPool == nil){
_flowersPool = [[NSMutableDictionary alloc] initWithCapacity:kTotalNumberOfFlowerTypes];
}
//尝试获取传入类型对应的花内部图片对象
UIImage *flowerImage = [_flowersPool objectForKey:[NSNumber numberWithInt:type]];
//如果没有对应类型的图片,则生成一个
if (flowerImage == nil){
NSLog(@"create new flower image with type:%u",type);
switch (type){
case kAnemone:
flowerImage = [UIImage imageNamed:@"anemone.png"];
break;
case kCosmos:
flowerImage = [UIImage imageNamed:@"cosmos.png"];
break;
case kGerberas:
flowerImage = [UIImage imageNamed:@"gerberas.png"];
break;
case kHollyhock:
flowerImage = [UIImage imageNamed:@"hollyhock.png"];
break;
case kJasmine:
flowerImage = [UIImage imageNamed:@"jasmine.png"];
break;
case kZinnia:
flowerImage = [UIImage imageNamed:@"zinnia.png"];
break;
default:
flowerImage = nil;
break;
}
[_flowersPool setObject:flowerImage forKey:[NSNumber numberWithInt:type]];
}else{
//如果有对应类型的图片,则直接使用
NSLog(@"reuse flower image with type:%u",type);
}
//创建花对象,将上面拿到的花内部图片对象赋值并返回
FlowerImageView *flowerImageView = [[FlowerImageView alloc] initWithImage:flowerImage];
return flowerImageView;
}
- 在这个工厂类里面定义了六种图片的类型
- 该工厂类持有
_flowersPool
私有成员变量,保存新创建过的图片。flowerImageWithType:
的实现:结合了_flowersPool
:当_flowersPool
没有对应的图片时,新创建图片并返回;否则直接从_flowersPool
获取对应的图片并返回。
接着我们定义这些花对象FlowerImageView
:
//================== FlowerImageView.h ==================
@interface FlowerImageView : UIImageView
@end
//================== FlowerImageView.m ==================
@implementation FlowerImageView
@end
在这里面其实也可以直接使用
UIImageView
,之所以创建一个子类是为了后面可以更好地扩展这些花独有的一些属性。注意一下花对象和花内部图片对象的区别:花对象
FlowerImageView
是包含花内部图片对象的UIImage
。因为在Objective-C里面,UIImage
是FlowerImageView
所继承的UIImageView
的一个属性,所以在这里FlowerImageView
就直接包含了UIImage
。
下面我们来看一下客户端如何使用FlowerFactory
和FlowerImageView
这两个类:
//================== client ==================
//首先建造一个生产花内部图片对象的工厂
FlowerFactory *factory = [[FlowerFactory alloc] init];
for (int i = 0; i < 500; ++i)
{
//随机传入一个花的类型,让工厂返回该类型对应花类型的花对象
FlowerType flowerType = arc4random() % kTotalNumberOfFlowerTypes;
FlowerImageView *flowerImageView = [factory flowerImageWithType:flowerType];
// 创建花对象的外部属性值(随机的位置和大小)
CGRect screenBounds = [[UIScreen mainScreen] bounds];
CGFloat x = (arc4random() % (NSInteger)screenBounds.size.width);
CGFloat y = (arc4random() % (NSInteger)screenBounds.size.height);
NSInteger minSize = 10;
NSInteger maxSize = 50;
CGFloat size = (arc4random() % (maxSize - minSize + 1)) + minSize;
//将位置和大小赋予花对象
flowerImageView.frame = CGRectMake(x, y, size, size);
//展示这个花对象
[self.view addSubview:flowerImageView];
}
上面代码里面是生成了500朵位置和大小都是随机的花内部图片对象。这500朵花最主要的区别还是它们的位置和大小;而它们使用的花的图片对象只有6个,因此可以用专门的Factory
来生成和管理这些少数的花内部图片对象,从工厂的打印我们可以看出来:
create new flower image with type:1
create new flower image with type:3
create new flower image with type:4
reuse flower image with type:3
create new flower image with type:5
create new flower image with type:2
create new flower image with type:0
reuse flower image with type:5
reuse flower image with type:5
reuse flower image with type:4
reuse flower image with type:1
reuse flower image with type:3
reuse flower image with type:4
reuse flower image with type:0
从上面的打印结果可以看出,在六种图片都创建好以后,再获取时就直接拿生成过的图片了,在一定程度上减少了内存的开销。
这里需要注意的是
- 工厂和花对象是组合关系:
FlowerFactroy
生成了多个FlowerImageView
对象,也就是花的内部图片对象,二者的关系属于强关系,因为在该例子中二者如果分离而独立存在都将会失去意义,所以在UML类图中用了组合的关系(实心菱形)。- 抽象享元类是
UIImageView
,它的一个内部对象是UIImage
(这两个都是Objective-C原生的关于图片的类)。- 客户端依赖的对象是工厂对象和花对象,而对花的内部图片对象
UIImage
可以一无所知,因为它是被FlowerFactroy
创建并被FlowerImageView
所持有的。(但是因为UIImage
是FlowerImageView
的一个外部可以引用的属性,所以在这里客户端还是可以访问到UIImage
,这是Objective-C原生的实现。后面我们在用享元模式的时候可以不将内部属性暴露出来)
使用享元模式需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
对象在缓冲池中的复用需要考虑线程问题。
UITableViewCell
的复用池就是使用享元模式的一个例子。