iOS设计模式-组合模式
问题:
树状结构在软件中非常常见,比如文件夹、软件中的菜单、办公软件的公司组织架构等,如何运用面向对象的方式来处理这种树状结构是组合模式需要解决的问题,组合模式通过一种巧妙的设计方案是使得户可以一致性的处理整个树状结构或者树状结构的一部分,也可以一致性的处理树状结构中的叶子节点(没有子节点)和树枝节点(包含子节点)。
在文件夹中,想要遍历所有的文件夹的文件,比如如下的文件结构:
文件夹:资料
——文件夹:文本文件
————图像文件:图片文件1
————图像文件:图片文件2
————图像文件:图片文件3
——文件夹:图像文件
————文本文件:文本文件1
————文本文件:文本文件2
————文本文件:文本文件3
观察上面的文件结构,有一下几个特点:
-
一个根节点,比如资料,它没有父节点,它可以包含其他的子节点;
-
树枝节点,有一类节点可以包含其他的节点,称之为树枝节点,比如文本文件夹,图像文件夹;
-
叶子节点,有一类节点没有子节点,称之为叶子节点,比如图片文件1、2、3,文本文件1、2、3;
想在要管理这个文件夹,假如要求打印出这个文件夹里面所有文件的文件名称,应该怎么实现呢。
不太优雅的实现
要打印所有文件的文件名称,就要管理这个文件夹的各个节点,现在有三类节点,根节点,树枝节点,叶子节点,在进一步分析发现,根节点和树枝节点是类似的,都可以包含其他的子节点,把他们成为容器节点。
这样,文件树的节点就分为两种,容器节点,叶子节点。容器节点还包含其他的容器节点或者叶子节点。把他们分别实现成为对象,也就是容器对象和叶子对象,容器对象可以包含其他的容器对象或者叶子对象,换句话说,容器对象也是一种组合对象。
看看代码实现:
1.叶子对象的代码实现:
/** 图像文件类 */
@interface ImageFile : NSObject
@property (nonatomic, copy) NSString *name;
- (void)logFileName;
@end
@implementation ImageFile
- (void)logFileName {
NSLog(@"图像文件-: %@", self.name);
}
@end
/** 文本文件类 */
@interface TextFile : NSObject
@property (nonatomic, copy) NSString *name;
- (void)logFileName;
@end
@implementation TextFile
- (void)logFileName {
NSLog(@"文本文件: %@", self.name);
}
@end
2.组合对象的代码实现:
@interface Folder : NSObject
@property (nonatomic, copy) NSString *name;
- (void)addFolder:(id)folder;
- (void)addImageFile:(id)imageFile;
- (void)addTextFile:(id)textFile;
- (void)logFileName;
@end
@implementation Folder {
NSMutableArray *_folderList;
NSMutableArray *_imageList;
NSMutableArray *_textList;
}
- (instancetype)init
{
self = [super init];
if (self) {
_folderList = [NSMutableArray array];
_imageList = [NSMutableArray array];
_textList = [NSMutableArray array];
}
return self;
}
- (void)addFolder:(id)folder {
[_folderList addObject:folder];
}
- (void)addImageFile:(id)imageFile {
[_imageList addObject:imageFile];
}
- (void)addTextFile:(id)textFile {
[_textList addObject:textFile];
}
- (void)logFileName {
NSLog(@"文件夹:%@", self.name);
for (id obj in _folderList) {
if ([obj isKindOfClass:[Folder class]]) {
[(Folder *)obj logFileName];
}
}
for (id obj in _imageList) {
if ([obj isKindOfClass:[ImageFile class]]) {
[(ImageFile *)obj logFileName];
}
}
for (id obj in _textList) {
if ([obj isKindOfClass:[TextFile class]]) {
[(TextFile *)obj logFileName];
}
}
}
3.客户端代码:
Folder *folder1 = [Folder new];
folder1.name = @"资料";
Folder *folder2 = [Folder new];
folder2.name = @"图像文件";
Folder *folder3 = [Folder new];
folder3.name = @"文本文件";
ImageFile *image1 = [ImageFile new];
image1.name = @"图片文件1";
ImageFile *image2 = [ImageFile new];
image2.name = @"图片文件2";
ImageFile *image3 = [ImageFile new];
image3.name = @"图片文件3";
TextFile *textFile1 = [TextFile new];
textFile1.name = @"文本文件1";
TextFile *textFile2 = [TextFile new];
textFile2.name = @"文本文件2";
TextFile *textFile3 = [TextFile new];
textFile3.name = @"文本文件3";
[folder1 addFolder:folder2];
[folder1 addFolder:folder3];
[folder2 addImageFile:image1];
[folder2 addImageFile:image2];
[folder2 addImageFile:image3];
[folder3 addTextFile:textFile1];
[folder3 addTextFile:textFile2];
[folder3 addTextFile:textFile3];
[folder1 logFileName];
Log:
文件夹:资料
——文件夹:图像文件
————图像文件: 图片文件1
————图像文件: 图片文件2
————图像文件: 图片文件3
——文件夹:文本文件
————文本文件: 文本文件1
————文本文件: 文本文件2
————文本文件: 文本文件3
这样实现的问题
上面的实现虽然能实现要求的功能,但是有几个明显的问题:
1.Folder类的设计和实现都非常复杂
文件夹类Folder的设计和实现都非常复杂,需要定义多个集合储存不同类型的成员
而且需要针对不同的成员提供增加、删除、和查询的方法,存在大量的冗余代码,系统维护较为困难。
2.不方便拓展
现在如果文件夹中增加视频文件要怎么办呢?
首先要增加视频文件的VideoFile类,然后还要去修改Folder类中的代码去兼容视频文件,很显然这是不符合开闭原则的 。
3.必须区分组合对象和叶子对象,并进行有区别的对待
比如在Folder类里面要判断是ImageFile还是TextFile;在客户端里面也要区别对待这两种对象。
区别对待组合对象和叶子对象,不仅让程序变得复杂,还对功能的扩展也带来不便。
实际上,大多数情况下用户并不想要去区别它们,而是认为它们是一样的,这样他们操作起来最简单。
怎么办?
那么如何比较优雅的解决上述问题呢,那么接下来就要引出组合模式,组合模式就是为了解决此类问题而诞生的,它可以让叶子对象和容器对象的使用具有一致性。
组合模式(Composite Pattern):组合多个对象形成树状结构以表示具有“整体—部分”关系的层次结构。组合模式对单个对象(叶子对象)和组合对象(容器对象)的使用具有一致性,组合模式又可以成为“整体—部分”(Part-Whole)模式,它是一种对象结构型模式。
类图:
1336015104_5713.jpg角色:
- Component(抽象组件):它可以是接口也是可以是抽象类,为叶子构建和容器构建对象声明接口,也可以提供默认实现,在该角色中可以包含所有子类共有行为的声明和实现。在抽象构件中定义了访问以及管理它的子构件的方法,如增加子构件、删除子构件、获取子构件等;
@interface AbstractFile : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger hierarchy;
+ (instancetype)fileWithName:(NSString *)fileName;
- (void)addFile:(AbstractFile *)file;
- (void)removeFile:(AbstractFile *)file;
- (AbstractFile *)fileAtIndex:(NSInteger)index;
- (void)logFileName;
@end
@implementation AbstractFile
+ (instancetype)fileWithName:(NSString *)fileName {
AbstractFile *file = [self new];
file.name = fileName;
return file;
}
- (void)addFile:(AbstractFile *)file {}
- (void)removeFile:(AbstractFile *)file {}
- (AbstractFile *)fileAtIndex:(NSInteger)index {
return nil;
}
- (void)logFileName {}
@end
- Leaf(叶子构件):它在组合结构中表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,可以通过异常等方式进行处理。
@interface ImageFile : AbstractFile
@end
@implementation ImageFile
- (void)addFile:(AbstractFile *)file {
NSLog(@"不支持该方法");
}
- (void)removeFile:(AbstractFile *)file {
NSLog(@"不支持该方法");
}
- (AbstractFile *)fileAtIndex:(NSInteger)index {
NSLog(@"不支持该方法");
return nil;
}
- (void)logFileName {
NSString *hierarchyString = @"";
for (int i = 0; i < self.hierarchy; i ++) {
hierarchyString = [hierarchyString stringByAppendingString:@"——"];
}
NSLog(@"%@图像文件:%@", hierarchyString, self.name);
}
@end
- Composite(容器构件):它在组合结构中表示容器节点对象,容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问以及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法。
/**
新的Composite对象需要继承组件对象;
原来用来记录包含的其它组合对象的集合,和包含的其它叶子对象的集合,这两个集合被合并成为一个,就是统一的包含其它子组件对象的集合。使用组合模式来实现,不再需要区分到底是组合对象还是叶子对象了;
原来的addFolder:和addImageFile:或者addTextFile:的方法,可以不需要了,合并实现成组件对象中定义的方法addFile:,当然需要现在的Composite来实现这个方法。使用组合模式来实现,不再需要区分到底是组合对象还是叶子对象了;
原来的logFileName方法的实现,完全要按照现在的方式来写,变化较大;
*/
@interface Folder : AbstractFile
@end
@implementation Folder {
NSMutableArray <AbstractFile *>*_fileList;
}
- (void)addFile:(AbstractFile *)file {
if (!_fileList) {
_fileList = [NSMutableArray array];
}
file.hierarchy = self.hierarchy + 1;
[_fileList addObject:file];
}
- (void)removeFile:(AbstractFile *)file {
if (_fileList && file) {
[_fileList removeObject:file];
}
}
- (AbstractFile *)fileAtIndex:(NSInteger)index {
if (index < _fileList.count) {
id obj = [_fileList objectAtIndex:index];
if ([obj isKindOfClass:AbstractFile.class]) {
return obj;
}
}
return nil;
}
- (void)logFileName {
NSString *hierarchyString = @"";
for (int i = 0; i < self.hierarchy; i ++) {
hierarchyString = [hierarchyString stringByAppendingString:@"——"];
}
NSLog(@"%@文件夹:%@", hierarchyString, self.name);
for (AbstractFile *file in _fileList) {
[file logFileName];
}
}
@end
客户端调用:
AbstractFile *folder1 = [Folder fileWithName:@"资料"];
AbstractFile *folder2 = [Folder fileWithName:@"文本文件"];
AbstractFile *folder3 = [Folder fileWithName:@"图像文件"];
AbstractFile *imageFile1 = [ImageFile fileWithName:@"图片文件1"];
AbstractFile *imageFile2 = [ImageFile fileWithName:@"图片文件2"];
AbstractFile *imageFile3 = [ImageFile fileWithName:@"图片文件3"];
AbstractFile *textFile1 = [TextFile fileWithName:@"文本文件1"];
AbstractFile *textFile2 = [TextFile fileWithName:@"文本文件2"];
AbstractFile *textFile3 = [TextFile fileWithName:@"文本文件3"];
[folder1 addFile:folder2];
[folder1 addFile:folder3];
[folder2 addFile:imageFile1];
[folder2 addFile:imageFile2];
[folder2 addFile:imageFile3];
[folder3 addFile:textFile1];
[folder3 addFile:textFile2];
[folder3 addFile:textFile3];
[folder1 logFileName];
Log:
文件夹:资料
——文件夹:文本文件
————图像文件:图片文件1
————图像文件:图片文件2
————图像文件:图片文件3
——文件夹:图像文件
————文本文件:文本文件1
————文本文件:文本文件2
————文本文件:文本文件3
通过上面的示例,可以看出来,通过组合模式,把一个“部分—整体”的层次结构表示成了对象树的结构,这样一来,客户端就无需在区分造作的是组合对象还是叶子对象了,对于客户端而言,操作的都是组件对象。
模式讲解
1.组合模式的目的
组合模式的目的是:让客户端不再区分操作的是组合对象还是叶子对象,而是以一个统一的方式来操作。
2.组合模式的关键
组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,而客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理。同时容器对象与抽象构件类之间还建立一个聚合关联关系,在容器对象中既可以包含叶子,也可以包含容器,以此实现递归组合,形成一个树形结构。如果不使用组合模式,客户端代码将过多地依赖于容器对象复杂的内部实现结构,容器对象内部实现结构的变化将引起客户代码的频繁变化,带来了代码维护复杂、可扩展性差等弊端。组合模式的引入将在一定程度上解决这些问题。
3.对象树
通常,组合模式会组合出树形结构来,组成这个树形结构所使用的多个组件对象,就自然的形成了对象树。这也意味着凡是可以使用对象树来描述或操作的功能,都可以考虑使用组合模式,比如读取XML文件,或是对语句进行语法解析等。
4.组合模式中的递归
组合模式中的递归,指的是对象递归组合,不是常说的递归算法。通常我们谈的递归算法,是指“一个方法会调用方法自己”这样的算法,是从功能上来讲的。而这里的组合模式中的递归,是对象本身的递归,是对象的组合方式,是从设计上来讲的,在设计上称作递归关联,是对象关联关系的一种。
5.最大化Component定义
前面讲到了组合模式的目的是:让客户端不再区分操作的是组合对象还是叶子对象,而是以一种统一的方式来操作。
由于要统一两种对象的操作,所以Component里面的方法也主要是两种对象对外方法的和,换句话说,有点大杂烩的意思,组件里面既有叶子对象需要的方法,也有组合对象需要的方法。
其实这种实现是与类的设计原则相冲突的,类的设计有这样的原则:一个父类应该只定义那些对它的子类有意义的操作。但是看看上面的实现就知道,Component中的有些方法对于叶子对象是没有意义的。那么怎么解决这一冲突呢?
常见的做法是在Component里面为对某些子对象没有意义的方法,提供默认的实现,或是默认抛出不支持该功能的例外。这样一来,如果子对象需要这个功能,那就覆盖实现它,如果不需要,那就不用管了,使用父类的默认实现就可以了。
从另一个层面来说,如果把叶子对象看成是一个特殊的Composite对象,也就是没有子节点的组合对象而已。这样看来,对于Component而言,子对象就全部看作是组合对象,因此定义的所有方法都是有意义的了。
安全性与透明性
根据前面的讲述,在组合模式中,把组件对象分成了两种,一种是可以包含子组件的Composite对象,一种是不能包含其他组件的叶子对象。
Composite对象就像是一个容器,可以包含其它的Composite对象或叶子对象。当然有了容器,就要能对这个容器进行维护,需要向里面添加对象,并能够从容器里面获取对象,还有能从容器中删除对象,也就是说需要管理子组件对象。
这就产生了一个很重要的问题:到底在组合模式的类层次结构中,在哪一些类里面定义这些管理子组件的操作,到底应该在Component中声明这些操作,还是在Composite中声明这些操作?
这就需要仔细思考,在不同的实现中,进行安全性和透明性的权衡选择。
安全性:
从客户使用组合模式上看是否更安全。如果是安全的,那么不会有发生误操作的可能,能访问的方法都是被支持的功能。
如果把管理子组件的操作定义在Composite中,那么客户在使用叶子对象的时候,就不会发生使用添加子组件或是删除子组件的操作了,因为压根就没有这样的功能,这种实现方式是安全的。
但是这样一来,客户端在使用的时候,就必须区分到底使用的是Composite对象,还是叶子对象,不同对象的功能是不一样的。也就是说,这种实现方式,对客户而言就不是透明的了。
透明性:
从客户使用组合模式上,是否需要区分到底是组合对象还是叶子对象。如果是透明的,那就是不再区分,对于客户而言,都是组件对象,具体的类型对于客户而言是透明的,是客户无需要关心的。
如果把管理子组件的操作定义在Component中,那么客户端只需要面对Component,而无需关心具体的组件类型,这种实现方式就是透明性的实现。事实上,前面示例的实现方式都是这种实现方式。
但是透明性的实现是以安全性为代价的,因为在Component中定义的一些方法,对于叶子对象来说是没有意义的,比如:增加、删除子组件对象。而客户不知道这些区别,对客户是透明的,因此客户可能会对叶子对象调用这种增加或删除子组件的方法,这样的操作是不安全的。
组合模式的优缺点
主要优点:
-
组合模式可以清楚的定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
-
客户端可以一致的使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端的代码。
-
在组合模式中增加新的容器构件或者子构件都很方便,无须对现有类库进行任何修改,符合“开闭原则”。
-
组合模式为树状结构的面向对象实现提供了一种灵活的解决方案,通过叶子对象和容器对象的递归组合,可以形成复杂的树状结构,但对树状结构的控制却非常简单。
主要缺点:
- 在增加新构件时很难对容器中的构件类型进行限制。有时候我们希望一个容器中只能有某些特定类型的对象,例如在某个文件夹中只能包含文本文件,使用组合模式时,不能依赖类型系统来施加这些约束,因为它们都来自于相同的抽象层,在这种情况下,必须通过在运行时进行类型检查来实现,这个实现过程较为复杂。
使用场景
在以下情况下可以考虑使用组合模式:
-
在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们。
-
在一个使用面向对象语言开发的系统中需要处理一个树形结构。
-
在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型。