深入解析设计模式中的装饰器模式在iOS应用开发中的实现
装饰器模式可以在不修改代码的情况下灵活的为一对象添加行为和职责。当你要修改一个被其它类包含的类的行为时,它可以代替子类化方法。
一、基本实现
下面我把类的结构图向大家展示如下:
让我们简单分析一下上面的结构图,component是定义一个对象接口,可以给这些对象动态地添加职责。concretecomponent是定义了一个具体的对象,也可以给这个对象添加一些职责。decorator,装饰抽象类,继承了component,从外类来扩展component类的功能,但对于component来说,是无需知道decorator的存在的。至于concretedecorator就是具体的装饰对象,起到给component添加职责的功能。
下面,还是老套路,我会尽可能的给出objective c实现的最简单的实例代码,首先声明一下,这些代码是运行在arc环境下的,所以对于某些可能引起内存泄漏的资源并没有采用手动释放的方式,这一点还是需要大家注意。
注意:本文所有代码均在arc环境下编译通过。
components类接口文件
#import<foundation/foundation.h>
@interface components :nsobject
-(void) operation;
@end
components类实现文件
#import"components.h"
@implementation components
-(void)operation{
return;
}
@end
decorator类接口文件
#import"components.h"
@interface decorator :components{
@protected components *components;
}
-(void)setcomponents:(components*)component;
@end
decorator类实现文件
#import"decorator.h"
@implementation decorator
-(void)setcomponents:(components*)component{
components = component;
}
-(void)operation{
if(components!=nil){
[components operation];
}
}
@end
concretecomponent类接口文件
#import"components.h"
@interface concretecomponent :components
@end
concretecomponent类实现文件
#import "concretecomponent.h"
@implementation concretecomponent
-(void)operation{
nslog(@"具体操作的对象");
}
@end
concretedecoratora类接口文件
#import "concretedecoratora.h"
#import "decorator.h"
@interface concretedecoratora :decorator
@end
concretedecoratora类实现文件
#import"concretedecoratora.h"
@implementation concretedecoratora
-(void)operation{
nslog(@"具体装饰对象a的操作");
[super operation];
}
@end
concretedecoratorb类接口文件
#import "decorator.h"
@interface concretedecoratorb :decorator
@end
concretedecoratorb类实现文件
#import "concretedecoratorb.h"
@implementation concretedecoratorb
-(void)operation{
nslog(@"具体装饰对象b的操作");
[super operation];
}
@end
main方法
#import <foundation/foundation.h>
#import "concretecomponent.h"
#import "concretedecoratora.h"
#import "concretedecoratorb.h"
int main (int argc,const char* argv[])
{
@autoreleasepool{
concretecomponent *c = [[concretecomponent alloc]init];
concretedecoratora *d1 = [[concretedecoratora alloc]init];
concretedecoratorb *d2 = [[concretedecoratorb alloc]init];
[d1 setcomponents:c];
[d2 setcomponents:d1];
[d2 operation];
}
return 0;
}
好啦,上面是需要展示的类,语法上都很简单,没有什么需要重点说的,可能值得一提的是关于子类调用父类方法的知识点,就是在调用每个对象的operation方法的时候,里面会有一句代码是[super operation];这句代码构很关键,他构成了各个对象之间operation方法的跳转,以此完成对components类对象的”装饰”。
二、分类(category)和委托(delegation)
在 object-c 里有两个种非常常见的实现模式:分类(category)和委托(delegation)。
1.分类 category
分类是一种非常强大的机制,它允许你在一个已存在的类里添加新方法,而不需要去为他添加一个子类。新方法在编译的时候添加,它能像这个类的扩展方法一样正常执行。一个装饰器跟类的定义稍微有点不同的就是,因为装饰器不能被实例化,它只是一个扩展。
提示:除了你自己类的扩展,你还可在任何 cocoa 类里的扩展添加方法。
如何使用分类:
现在你有一个 album 对象,你需要把它显示在一个表单视图里(table view):
专辑的标题从哪里来?album 只是一个模型对象,它才不会去关心你如果去显示这些数据。为了这些,你需要给 album 类添加一些额外的代码,但是请不要直接修改这个类。
你现在就需要为 album 添加一个分类 (category) 的扩展;它将定义一个新地方法用来返回一个数据结构,这个数据结构可以很容易的被 uitableviews 使用。
这个数据结构看起来如下:
为 album 添加一个分类,导航 file\new\file… 选择 object-c category 模版─不要习惯的去选择 object-c class,在 category 后面输入 tablerepresentation,category to 后面输入 album。
提示:你有没有注意这个新文件的名字?album+tablerepresentation 说明它是 album 类的一个扩展。这个习惯很重要,因为第一这很容易读,第二防止你或者其他人创建的分类跟其冲突。
打开 album+tablerepresentation,加入下面的方法原型:
- (nsdictionary*)tr_tablerepresentation;
注意,这是一个 tr_ 开头的方法名,就像是这个分类名字的缩写一样:tablerepresentation。其次,这个习惯会避免这个方法跟其它方法重名!
提示:如果分类 (category) 声明的一个方法跟原始类的一个方法重名,或者跟同类里的的另一个分类名字重复(或者是它的父类),当它在运行的时候,它就不知道要执行哪个方法。如果是在你自己类的分类里,它不太可能出现大的问题,但是如果一个标准 cocoa 或者 cocoa touch 类里面添加这个分类的方法,就可能会引起严重的问题。
打开 album+tablerepersentation.m 文件添加下面的方法:
- (nsdictionary*)tr_tablerepersentation
{
return @{@"titles":@[@"artist", @"album", @"genre", @"year"],
@"values":@[self.artist, self.title, self.genre, self.year]};
};
考虑一会,为什么这种模式如些强大:
你能够直接使用 album 的属性。
你已经添加在 album 类里,但它并不是它的子类。如果子类需要,你同样也可以这样做。
这样一个简单的添加,album 类的数据返回一个 uitableview 可用的数据结构,但并不需要修改 album 的代码。
苹果在基础类里大量的使用了分类设计模式。去看看他们是怎么做的,打开 nsstring.h。找到 @interface nsstring,你将会看到这个类定义了三个分类:nsstringextensionmethods, nsextendedstringpropertylistparsing 和 nsstingdeprecated。在代码片里,分类将帮助你保持方法的组织性和分离必。
2.委托 delegation
另外一种装饰器的设计模式是,委托 (delegation),它是一种机制,一个对象代表另外一个对象或者其相互合作。例子,当你使用 uitableview 的时候,其中一个方法是你必需要执行的,tableview:numberofrowsinsection:。
你可能并不期望 uitableview 知道每个 section 中有多少行,这是程序的特性。因此,计算每个 section 有多少行的工作就交给了 uitableview 的委托 (delegate)。它允许 uitableview 类不依赖它显示的数据。
当你创建了一个新的 uitableview 的时候,这里有一个类似的解释:
uitableview 对象的工作就是显示一个表单视图。然而,最终它都需要一些它信息,它并不拥有这些信息。然后,它会转向它的委托,发送一个添加信息的消息。在 object-c 中实现委托模式,一个类可以通过协议 (protocol) 来声明一个可选和必选的方法。稍后,在这个教程你将覆盖一个协议 (protocols)。
它看起来比子类更容易,覆盖需要的方法,但是考虑如果是单类的话你只能创建子类。如果你想一个对象委托两个或者多个对象的时候,子类化的方法是不能实现的。
提示:这是一个很重要的模式。苹果在 uikit 类中大量的使用了此方法:uitableview, uitextview, uitextfield, uiwebview, uialert, uiactionsheet, uicollectionview, uipickerview, uigesturerecognizer, uiscrollview。这个列表还可以有很多。
如何使用委托模式:
打开 viewcontroller.m,在顶部引入如下文件
#import "libraryapi.h"
#import "album+tablerepresentation.h"
现在,在类的扩展里的添加一些私有变量,它们看起来如下:
@interface viewcontroller (){
uitableview *datatable;
nsarray *allalbums;
nsdictionary *currentalbumdata;
int currentalbumindex;
}
@end
现在,替换类扩展里的 @interface 这一行,完成后如下:
@interface viewcontroller () <uitableviewdatasoure, uitableviewdelegate> {
这就是如何设置一个正确的委托─把它相象成允许一个委托来履行一个方法的合同。这里,表明 viewcontroller 将会遵照 uitableviewdatasource 和 uitableviewdelegate 协议。这种方法下 uitableview 必须执行它自己的委托方法。
下面,用下面的代码替换 viewdidload:
- (void)viewdidload {
[super viewdidload];
// 1
self.view.backgroundcolor = [uicolor colorwithred:0.76f green:0.81f blue:0.87f alpha:1];
currentalbumindex = 0;
//2
allalbums = [[libraryapi sharedinstance] getalbums];
// 3
// the uitableview that presents the album data
datatable = [[uitableview alloc] initwithframe:cgrectmake(0, 120, self.view.frame.size.width, self.view.frame.size.height-120) style:uitableviewstylegrouped];
datatable.delegate = self;
datatable.datasource = self;
datatable.backgroundview = nil;
[self.view addsubview:datatable];
}
这里分析下上面的代码:
把背景色改为漂亮的深蓝色。
从 api 获取一个列表,它包含所有的专辑数据。不能直接使用 persistencymanager。
创建一个 uitableview。你声明了视图控制器是 uitableview delegate/data source;因此,uitableview 将会提供视图控制器需要的所有信息。
现在,在 viewcontroller.m 里面添加如下方法:
- (void)showdataforalbumatindex:(int)albumindex{
// defensive code: make sure the requested index is lower than the amount of albums
if (albumindex < allalbums.count) {
// fetch the album
album *album = allalbums[albumindex];
// save the albums data to present it later in the tableview
currentalbumdata = [album tr_tablerepresentation];
} else {
currentalbumdata = nil;
}
// we have the data we need, let's refresh our tableview
[datatable reloaddata];
}
showdataforalbumatindex: 从专辑数组中取出需要的专辑数据。当你需要显示新数据的时候,你只需要重载数据 (relaoddata)。这是因为 uitableview 需要请求它的委托代理,像有多少 sections 将会在表单视图中显示,每个 section 中有多少行,每行看起来是什么样的。
在 viewdidload 中添加下面代码
[self showdataforalbumatindex:currentalbumindex];
当程序运行的时候它会加载当前的专辑信息。由于 currentalbumindex 的预设值为 0,所以会显示收藏中的第一张专辑信息。
构建并运行你的项目,你的程序会崩溃掉,在控制台会输入如下的异常:
出现什么问题了?你已经声明了 viewcontroller 中的 uitableview 的委托(delegate)和数据源(data source)。但是在这种情况下,你必需执行所有的必需方法─包含 tableview:numberofrowsinsection:─你现在还没有它。
在 viewcontrller.m 的 @implementation 和 @end 的任何地方添加如下代码:
- (nsinteger)tableview:(uitableview *)tableview numberofrowsinsection:(nsinteger)section {
return [currentalbumdata[@"titles"] count];
}
- (uitableviewcell*)tableview:(uitableview *)tableview cellforrowatindexpath:(nsindexpath *)indexpath {
uitableviewcell *cell = [tableview dequeuereusablecellwithidentifier:@"cell"];
if (!cell) {
cell = [[uitableviewcell alloc] initwithstyle:uitableviewcellstylevalue1 reuseidentifier:@"cell"];
}
cell.textlabel.text = currentalbumdata[@"titles"][indexpath.row];
cell.detailtextlabel.text = currentalbumdata[@"values"][indexpath.row];
return cell;
}
tableview:numberofrowsindexsection: 返回表单视图显示的行数,匹配数据结构中标题的数目。
tableview:cellforrowatindexpath: 创建并返回一个带标题和信息的 cell。
现在构建并运行你的项目。你的程序开始运行并显示出下图的界面:
这目前为止事情看起来很不错。但是如果你回过去看第一张图片的时候,你会发现在屏幕的顶端有一个可以水平滚动的视图,用于切换专辑。它只是简单的水平滚动,为什么不做一个可以重复使用的视图来代替它呢。