[关闭]
@qidiandasheng 2021-01-11T13:59:49.000000Z 字数 14160 阅读 605

设计模式(四):结构型模式之外观、组合、享元(😁)

架构


三种结构型模式

介绍

这一篇主要介绍结构模式中的外观模式、组合模式、享元模式。相对于上一篇介绍的四种结构型模式,这一篇的三种结构型模式更不常用一些,他们针对的场景更加特殊,应用场景更加明确。

外观模式和适配器模式的区别

适配器模式和外观模式其实很类似,他们的共同点是将不好用的接口适配成好用的接口。

不同点主要是应用场景不同:

组合模式和组合关系的区别

组合模式跟我们之前讲的面向对象设计中的“组合关系(通过组合来组装两个类)”,完全是两码事。这里讲的“组合模式”,主要是用来处理树形结构数据。这里的“数据”,你可以简单理解为一组对象集合。


外观模式

定义

外观模式(Facade Pattern):外观模式定义了一个高层接口,为子系统中的一组接口提供一个统一的接口。外观模式又称为门面模式,它是一种结构型设计模式模式。

定义解读:通过这个高层接口,可以将客户端与子系统解耦:客户端可以不直接访问子系统,而是通过外观类间接地访问;同时也可以提高子系统的独立性和可移植性。

适用场景

成员与类图

成员

外观模式包括客户端共有三个成员:

模式类图

导出图片Mon Apr 13 2020 10_53_45 GMT+0800 (中国标准时间).png-181.5kB

代码示例

场景概述

模拟一个智能家居系统。这个智能家居系统可以用一个中央遥控器操作其所接入的一些家具:台灯,音箱,空调等等。

在这里我们简单操纵几个设备:

场景分析

有的时候,我们需要某个设备可以一次执行两个不同的操作;也可能会需要多个设备共同协作来执行一些任务。比如:

假设我们可以用遥控器直接开启热风,那么实际上就是两个步骤:

  1. 开启空调
  2. 空调切换为热风模式

我们把这两个步骤用一个操作包含起来,一步到位。像这样简化操作步骤的场景比较适合用外观模式。

同样的,我们想听歌的话,需要四个步骤:

  1. 开启CD Player
  2. 开启音箱
  3. 连接CD Player和音箱
  4. 播放CD Player

这些步骤我们也可以装在单独的一个接口里面。

类似的,如果我们想看DVD的话,步骤会更多,因为DVD需要同时输出声音和影像:

  1. 开启DVD player
  2. 开启音箱
  3. 音响与DVD Player连接
  4. 开启投影仪
  5. 投影仪与DVD Player连接
  6. 播放DVD Player

这些接口也可以装在一个单独的接口里。

最后,如果我们要出门,需要关掉所有家用电器,也不需要一个一个将他们关掉,也只需要一个关掉的总接口就好了,因为这个关掉的总接口里面可以包含所有家用电器的关闭接口。

因此,这些设备可以看做是该智能家居系统的子系统;而这个遥控器则扮演的是外观类的角色。

下面我们用代码来看一下如何实现这些设计。

代码实现

因为所有家用电器都有开启和关闭的操作,所以我们先创建一个家用电器的基类HomeDevice

  1. //================== HomeDevice.h ==================
  2. //设备基类
  3. @interface HomeDevice : NSObject
  4. //连接电源
  5. - (void)on;
  6. //关闭电源
  7. - (void)off;
  8. @end

然后是继承它的所有家用电器类:

空调类AirConditioner:

  1. //================== AirConditioner.h ==================
  2. @interface AirConditioner : HomeDevice
  3. //高温模式
  4. - (void)startHighTemperatureMode;
  5. //常温模式
  6. - (void)startMiddleTemperatureMode;
  7. //低温模式
  8. - (void)startLowTemperatureMode;
  9. @end

CD Player类:CDPlayer:

  1. //================== CDPlayer.h ==================
  2. @interface CDPlayer : HomeDevice
  3. - (void)play;
  4. @end

DVD Player类:DVDPlayer:

  1. //================== DVDPlayer.h ==================
  2. @interface DVDPlayer : HomeDevice
  3. - (void)play;
  4. @end

音箱类VoiceBox:

  1. //================== VoiceBox.h ==================
  2. @class CDPlayer;
  3. @class DVDPlayer;
  4. @interface VoiceBox : HomeDevice
  5. //与CDPlayer连接
  6. - (void)connetCDPlayer:(CDPlayer *)cdPlayer;
  7. //与CDPlayer断开连接
  8. - (void)disconnetCDPlayer:(CDPlayer *)cdPlayer;
  9. //与DVD Player连接
  10. - (void)connetDVDPlayer:(DVDPlayer *)dvdPlayer;
  11. //与DVD Player断开连接
  12. - (void)disconnetDVDPlayer:(DVDPlayer *)dvdPlayer;
  13. @end

投影仪类Projecter

  1. //================== Projecter.h ==================
  2. @interface Projecter : HomeDevice
  3. //与DVD Player连接
  4. - (void)connetDVDPlayer:(DVDPlayer *)dvdPlayer;
  5. //与DVD Player断开连接
  6. - (void)disconnetDVDPlayer:(DVDPlayer *)dvdPlayer;
  7. @end

注意,音箱是可以连接CD Player和DVD Player的;而投影仪只能连接DVD Player

现在我们把所有的家用电器类和他们的接口都定义好了,下面我们看一下该实例的外观类HomeDeviceManager如何设计。

首先我们看一下客户端期望外观类实现的接口:

  1. //================== HomeDeviceManager.h ==================
  2. @interface HomeDeviceManager : NSObject
  3. //===== 关于空调的接口 =====
  4. //空调吹冷风
  5. - (void)coolWind;
  6. //空调吹热风
  7. - (void)warmWind;
  8. //===== 关于CD Player的接口 =====
  9. //播放CD
  10. - (void)playMusic;
  11. //关掉音乐
  12. - (void)offMusic;
  13. //===== 关于DVD Player的接口 =====
  14. //播放DVD
  15. - (void)playMovie;
  16. //关闭DVD
  17. - (void)offMoive;
  18. //===== 关于总开关的接口 =====
  19. //打开全部家用电器
  20. - (void)allDeviceOn;
  21. //关闭所有家用电器
  22. - (void)allDeviceOff;
  23. @end

上面的接口分为了四大类,分别是:

为了便于读者理解,这四类的接口所封装的子系统接口的数量是逐渐增多的。

在看这些接口时如何实现的之前,我们先看一下外观类是如何保留这些子系统类的实例的。在该代码示例中,这些子系统类的实例在外观类的构造方法里被创建,而且作为外观类的成员变量被保存了下来。

  1. //================== HomeDeviceManager.m ==================
  2. @implementation HomeDeviceManager
  3. {
  4. NSMutableArray *_registeredDevices;//所有注册(被管理的)的家用电器
  5. AirConditioner *_airconditioner;
  6. CDPlayer *_cdPlayer;
  7. DVDPlayer *_dvdPlayer;
  8. VoiceBox *_voiceBox;
  9. Projecter *_projecter;
  10. }
  11. - (instancetype)init{
  12. self = [super init];
  13. if (self) {
  14. _airconditioner = [[AirConditioner alloc] init];
  15. _cdPlayer = [[CDPlayer alloc] init];
  16. _dvdPlayer = [[DVDPlayer alloc] init];
  17. _voiceBox = [[VoiceBox alloc] init];
  18. _projecter = [[Projecter alloc] init];
  19. _registeredDevices = [NSMutableArray arrayWithArray:@[_airconditioner,
  20. _cdPlayer,
  21. _dvdPlayer,
  22. _voiceBox,
  23. _projecter]];
  24. }
  25. return self;
  26. }

其中 _registeredDevices这个成员变量是一个数组,它包含了所有和这个外观类实例关联的子系统实例。

子系统与外观类的关联实现方式不止一种,不作为本文研究重点,现在只需知道外观类保留了这些子系统的实例即可。按照顺序,我们首先看一下关于空调的接口的实现:

  1. //================== HomeDeviceManager.m ==================
  2. //空调吹冷风
  3. - (void)coolWind{
  4. [_airconditioner on];
  5. [_airconditioner startLowTemperatureMode];
  6. }
  7. //空调吹热风
  8. - (void)warmWind{
  9. [_airconditioner on];
  10. [_airconditioner startHighTemperatureMode];
  11. }

吹冷风和吹热风的接口都包含了空调实例的两个接口,第一个都是开启空调,第二个则是对应的冷风和热风的接口。

我们接着看关于CD Player的接口的实现:

  1. //================== HomeDeviceManager.m ==================
  2. - (void)playMusic{
  3. //1. 开启CDPlayer开关
  4. [_cdPlayer on];
  5. //2. 开启音箱
  6. [_voiceBox on];
  7. //3. 音响与CDPlayer连接
  8. [_voiceBox connetCDPlayer:_cdPlayer];
  9. //4. 播放CDPlayer
  10. [_cdPlayer play];
  11. }
  12. //关掉音乐
  13. - (void)offMusic{
  14. //1. 切掉与音箱的连接
  15. [_voiceBox disconnetCDPlayer:_cdPlayer];
  16. //2. 关掉音箱
  17. [_voiceBox off];
  18. //3. 关掉CDPlayer
  19. [_cdPlayer off];
  20. }

在上面的场景分析中提到过,听音乐这个指令要分四个步骤:CD Player和音箱的开启,二者的连接,以及播放CD Player,这也比较符合实际生活中的场景。关掉音乐也是先断开连接再切断电源(虽然直接切断电源也可以)。

接下来我们看一下关于DVD Player的接口的实现:

  1. //================== HomeDeviceManager.m ==================
  2. - (void)playMovie{
  3. //1. 开启DVD player
  4. [_dvdPlayer on];
  5. //2. 开启音箱
  6. [_voiceBox on];
  7. //3. 音响与DVDPlayer连接
  8. [_voiceBox connetDVDPlayer:_dvdPlayer];
  9. //4. 开启投影仪
  10. [_projecter on];
  11. //5.投影仪与DVDPlayer连接
  12. [_projecter connetDVDPlayer:_dvdPlayer];
  13. //6. 播放DVDPlayer
  14. [_dvdPlayer play];
  15. }
  16. - (void)offMoive{
  17. //1. 切掉音箱与DVDPlayer连接
  18. [_voiceBox disconnetDVDPlayer:_dvdPlayer];
  19. //2. 关掉音箱
  20. [_voiceBox off];
  21. //3. 切掉投影仪与DVDPlayer连接
  22. [_projecter disconnetDVDPlayer:_dvdPlayer];
  23. //4. 关掉投影仪
  24. [_projecter off];
  25. //5. 关掉DVDPlayer
  26. [_dvdPlayer off];
  27. }

因为DVD Player要同时连接音箱和投影仪,所以这两个接口封装的子系统接口相对于CD Player的更多一些。

最后我们看一下关于总开关的接口的实现:

  1. //================== HomeDeviceManager.m ==================
  2. //打开全部家用电器
  3. - (void)allDeviceOn{
  4. [_registeredDevices enumerateObjectsUsingBlock:^(HomeDevice *device, NSUInteger idx, BOOL * _Nonnull stop) {
  5. [device on];
  6. }];
  7. }
  8. //关闭所有家用电器
  9. - (void)allDeviceOff{
  10. [_registeredDevices enumerateObjectsUsingBlock:^(HomeDevice *device, NSUInteger idx, BOOL * _Nonnull stop) {
  11. [device off];
  12. }];
  13. }

这两个接口是为了方便客户端开启和关闭所有设备的,有这两个接口的话,用户就不用一一开启或关闭多个设备了。

关于这两个接口的实现:

上文说过,该外观类通过一个数组成员变量_registeredDevices来保存所有可操作的设备。所以如果我们需要开启或关闭所有的设备就可以遍历这个数组并向每个元素调用onoff方法。因为这些元素都继承于HomeDevice,也就是都有onoff方法。

这样做的好处是,我们不需要单独列出所有设备来分别调用它们的接口;而且后面如果添加或者删除某些设备的话也不需要修改这两个接口的实现了。

代码对应的类图

导出图片Mon Apr 13 2020 10_54_04 GMT+0800 (中国标准时间).png-274.5kB

从上面的UML类图中可以看出,该示例的子系统之间的耦合还是比较多的;而外观类HomeDeviceManager的接口大大简化了User对这些子系统的使用成本。

优点

缺点

Objective-C 的实践

组合模式

定义

组合模式是一种结构型设计模式,你可以使用它将对象组合成树状结构,并且能像使用独立对象一样使用它们。

适用场景

代码示例

场景概述

设计一个类来表示文件系统中的目录,能方便地实现下面这些功能:

场景分析

这里的文件系统就是一个树状结构,有两种基本元素简单叶节点(文件)和复杂容器(子目录)。所以比较适合使用组合模式。

代码实现

基类(包含两种元素的共同接口):

  1. @interface FileSystemNode : NSObject
  2. @property(nonatomic, copy, readonly)NSString *path;
  3. - (instancetype)initWithPath:(NSString *)path;
  4. - (NSInteger)countNumOfFiles;
  5. - (NSInteger)countSizeOfFiles;
  6. @end
  7. @implementation FileSystemNode
  8. - (instancetype)initWithPath:(NSString *)path{
  9. self = [super init];
  10. if (self) {
  11. _path = path;
  12. }
  13. return self;
  14. }
  15. @end

文件类:

  1. @interface FileNode : FileSystemNode
  2. @end
  3. @implementation FileNode
  4. - (NSInteger)countNumOfFiles{
  5. return 1;
  6. }
  7. - (NSInteger)countSizeOfFiles{
  8. //1. 计算文件大小
  9. //2. 返回当前文件大小
  10. return 0;
  11. }
  12. @end

目录类:

  1. ==========================DirectoryNode.h============================
  2. @interface DirectoryNode : FileSystemNode
  3. - (void)addSubNode:(FileSystemNode *)node;
  4. - (void)removeSubNode:(FileSystemNode *)node;
  5. @end
  6. ==========================DirectoryNode.m============================
  7. @interface DirectoryNode()
  8. @property(nonatomic, strong)NSMutableArray<FileSystemNode *> *subNodes;
  9. @end
  10. @implementation DirectoryNode
  11. - (NSInteger)countNumOfFiles{
  12. NSInteger numOfFiles = 0;
  13. for (FileSystemNode *node in self.subNodes) {
  14. numOfFiles += [node countNumOfFiles];
  15. }
  16. return numOfFiles;
  17. }
  18. - (NSInteger)countSizeOfFiles{
  19. NSInteger sizeofFiles = 0;
  20. for (FileSystemNode *node in self.subNodes) {
  21. sizeofFiles += [node countSizeOfFiles];
  22. }
  23. return sizeofFiles;
  24. }
  25. - (void)addSubNode:(FileSystemNode *)node{
  26. [self.subNodes addObject:node];
  27. }
  28. - (void)removeSubNode:(FileSystemNode *)node{
  29. [self.subNodes removeObject:node];
  30. }
  31. - (NSMutableArray<FileSystemNode *> *)subNodes{
  32. if (!_subNodes) {
  33. _subNodes = [NSMutableArray new];
  34. }
  35. return _subNodes;
  36. }
  37. @end

客户端使用:

  1. /**
  2. /
  3. /wz/
  4. /wz/a.txt
  5. /wz/b.txt
  6. /wz/movies/
  7. /wz/movies/c.avi
  8. **/
  9. DirectoryNode *fileSystemTree = [[DirectoryNode alloc] initWithPath:@"/"];
  10. DirectoryNode *wz_node = [[DirectoryNode alloc] initWithPath:@"/wz/"];
  11. [fileSystemTree addSubNode:wz_node];
  12. FileNode *wz_a_node = [[FileNode alloc] initWithPath:@"/wz/a.txt"];
  13. FileNode *wz_b_node = [[FileNode alloc] initWithPath:@"/wz/b.txt"];
  14. DirectoryNode *wz_movies_node = [[DirectoryNode alloc] initWithPath:@"/wz/movies/"];
  15. [wz_node addSubNode:wz_a_node];
  16. [wz_node addSubNode:wz_b_node];
  17. [wz_node addSubNode:wz_movies_node];
  18. FileNode *wz_movies_c_node = [[FileNode alloc] initWithPath:@"/wz/movies/c.avi"];
  19. [wz_movies_node addSubNode:wz_movies_c_node];
  20. NSLog(@"%ld %ld",[fileSystemTree countNumOfFiles],[fileSystemTree countSizeOfFiles]);

优点

缺点


享元模式

定义

享元模式(Flyweight Pattern):运用共享技术复用大量细粒度的对象,降低程序内存的占用,提高程序的性能。

定义解读:

适用场景

成员与类图

成员

享元模式一共有三个成员:

模式类图

导出图片Mon Apr 13 2020 10_57_04 GMT+0800 (中国标准时间).png-149.3kB

代码示例

场景概述

这里我们使用《Objective-C 编程之道:iOS设计模式解析》里的第21章使用的例子:在一个页面展示数百个大小,位置不同的花的图片,然而这些花的样式只有6种。

看一下截图:
百花图

场景分析

由于这里我们需要创建很多对象,而这些对象有可以共享的内部状态(6种图片内容)以及不同的外部状态(随机的,数百个位置坐标和图片大小),因此比较适合使用享元模式来做。

根据上面提到的享元模式的成员:

代码实现

首先我们创建一个工厂,这个工厂可以根据所传入花的类型来返回花内部图片对象,在这里可以直接使用原生的UIImage对象,也就是图片对象。而且这个工厂持有一个保存图片对象的池子:

下面我们看一下代码是如何实现的:

  1. //================== FlowerFactory.h ==================
  2. typedef enum
  3. {
  4. kAnemone,
  5. kCosmos,
  6. kGerberas,
  7. kHollyhock,
  8. kJasmine,
  9. kZinnia,
  10. kTotalNumberOfFlowerTypes
  11. } FlowerType;
  12. @interface FlowerFactory : NSObject
  13. - (FlowerImageView *) flowerImageWithType:(FlowerType)type
  14. @end
  15. //================== FlowerFactory.m ==================
  16. @implementation FlowerFactory
  17. {
  18. NSMutableDictionary *_flowersPool;
  19. }
  20. - (FlowerImageView *) flowerImageWithType:(FlowerType)type
  21. {
  22. if (_flowersPool == nil){
  23. _flowersPool = [[NSMutableDictionary alloc] initWithCapacity:kTotalNumberOfFlowerTypes];
  24. }
  25. //尝试获取传入类型对应的花内部图片对象
  26. UIImage *flowerImage = [_flowersPool objectForKey:[NSNumber numberWithInt:type]];
  27. //如果没有对应类型的图片,则生成一个
  28. if (flowerImage == nil){
  29. NSLog(@"create new flower image with type:%u",type);
  30. switch (type){
  31. case kAnemone:
  32. flowerImage = [UIImage imageNamed:@"anemone.png"];
  33. break;
  34. case kCosmos:
  35. flowerImage = [UIImage imageNamed:@"cosmos.png"];
  36. break;
  37. case kGerberas:
  38. flowerImage = [UIImage imageNamed:@"gerberas.png"];
  39. break;
  40. case kHollyhock:
  41. flowerImage = [UIImage imageNamed:@"hollyhock.png"];
  42. break;
  43. case kJasmine:
  44. flowerImage = [UIImage imageNamed:@"jasmine.png"];
  45. break;
  46. case kZinnia:
  47. flowerImage = [UIImage imageNamed:@"zinnia.png"];
  48. break;
  49. default:
  50. flowerImage = nil;
  51. break;
  52. }
  53. [_flowersPool setObject:flowerImage forKey:[NSNumber numberWithInt:type]];
  54. }else{
  55. //如果有对应类型的图片,则直接使用
  56. NSLog(@"reuse flower image with type:%u",type);
  57. }
  58. //创建花对象,将上面拿到的花内部图片对象赋值并返回
  59. FlowerImageView *flowerImageView = [[FlowerImageView alloc] initWithImage:flowerImage];
  60. return flowerImageView;
  61. }
  • 在这个工厂类里面定义了六种图片的类型
  • 该工厂类持有_flowersPool私有成员变量,保存新创建过的图片。
  • flowerImageWithType:的实现:结合了_flowersPool:当_flowersPool没有对应的图片时,新创建图片并返回;否则直接从_flowersPool获取对应的图片并返回。

接着我们定义这些花对象FlowerImageView

  1. //================== FlowerImageView.h ==================
  2. @interface FlowerImageView : UIImageView
  3. @end
  4. //================== FlowerImageView.m ==================
  5. @implementation FlowerImageView
  6. @end

在这里面其实也可以直接使用UIImageView,之所以创建一个子类是为了后面可以更好地扩展这些花独有的一些属性。

注意一下花对象和花内部图片对象的区别:花对象FlowerImageView是包含花内部图片对象的UIImage。因为在Objective-C里面,UIImageFlowerImageView所继承的UIImageView的一个属性,所以在这里FlowerImageView就直接包含了UIImage

下面我们来看一下客户端如何使用FlowerFactoryFlowerImageView这两个类:

  1. //================== client ==================
  2. //首先建造一个生产花内部图片对象的工厂
  3. FlowerFactory *factory = [[FlowerFactory alloc] init];
  4. for (int i = 0; i < 500; ++i)
  5. {
  6. //随机传入一个花的类型,让工厂返回该类型对应花类型的花对象
  7. FlowerType flowerType = arc4random() % kTotalNumberOfFlowerTypes;
  8. FlowerImageView *flowerImageView = [factory flowerImageWithType:flowerType];
  9. // 创建花对象的外部属性值(随机的位置和大小)
  10. CGRect screenBounds = [[UIScreen mainScreen] bounds];
  11. CGFloat x = (arc4random() % (NSInteger)screenBounds.size.width);
  12. CGFloat y = (arc4random() % (NSInteger)screenBounds.size.height);
  13. NSInteger minSize = 10;
  14. NSInteger maxSize = 50;
  15. CGFloat size = (arc4random() % (maxSize - minSize + 1)) + minSize;
  16. //将位置和大小赋予花对象
  17. flowerImageView.frame = CGRectMake(x, y, size, size);
  18. //展示这个花对象
  19. [self.view addSubview:flowerImageView];
  20. }

上面代码里面是生成了500朵位置和大小都是随机的花内部图片对象。这500朵花最主要的区别还是它们的位置和大小;而它们使用的花的图片对象只有6个,因此可以用专门的Factory来生成和管理这些少数的花内部图片对象,从工厂的打印我们可以看出来:

  1. create new flower image with type:1
  2. create new flower image with type:3
  3. create new flower image with type:4
  4. reuse flower image with type:3
  5. create new flower image with type:5
  6. create new flower image with type:2
  7. create new flower image with type:0
  8. reuse flower image with type:5
  9. reuse flower image with type:5
  10. reuse flower image with type:4
  11. reuse flower image with type:1
  12. reuse flower image with type:3
  13. reuse flower image with type:4
  14. reuse flower image with type:0

从上面的打印结果可以看出,在六种图片都创建好以后,再获取时就直接拿生成过的图片了,在一定程度上减少了内存的开销。

代码对应的类图

导出图片Mon Apr 13 2020 10_57_30 GMT+0800 (中国标准时间).png-114.8kB

这里需要注意的是

  • 工厂和花对象是组合关系:FlowerFactroy生成了多个FlowerImageView对象,也就是花的内部图片对象,二者的关系属于强关系,因为在该例子中二者如果分离而独立存在都将会失去意义,所以在UML类图中用了组合的关系(实心菱形)。
  • 抽象享元类是UIImageView,它的一个内部对象是UIImage(这两个都是Objective-C原生的关于图片的类)。
  • 客户端依赖的对象是工厂对象和花对象,而对花的内部图片对象UIImage可以一无所知,因为它是被FlowerFactroy创建并被FlowerImageView所持有的。(但是因为UIImageFlowerImageView的一个外部可以引用的属性,所以在这里客户端还是可以访问到UIImage,这是Objective-C原生的实现。后面我们在用享元模式的时候可以不将内部属性暴露出来)

优点

缺点

Objective-C 的实践

参考

结构型模式

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注