【推荐】【老外写的iOS设计模式系列】第4部分 装饰器模式

装饰器(Decorator)模式
装饰器模式在不修改原来代码的情况下动态的给对象增加新的行为和职责,它通过一个对象包装被装饰对象的方法来修改类的行为,这种方法可以做为子类化的一种替代方法。
Objective-C 中,存在两种非常常见的实现 :Category( 类别)和 Delegation (委托)。
 
Category(类别)  
Category( 类别 ) 是一种不需要子类化就可以让你能动态的给已经存在的类增加方法的强有力的机制。新增的方法是在编译期增加的,这些方法执行的时候和被扩展的类的其它方法是一样的。它可能与装饰器设计模式的定义稍微有点不同,因为 Category( 类别 ) 不会保存被扩展类的引用。
注意 :   你除了可以扩展你自己的类以外,你还可以给 Cocoa 自己的类增加方法。  
如何使用类别
设想一种情况,你需要让 Album( 专辑 ) 对象显示在一个表格视图( TableView )中:


 
专辑的标题从何而来?因为专辑是模型对象,它本身不需要关心你如何显示它的数据。你需要增加一些代码去扩展专辑类的行为,但是不需要直接修改专辑类。
你将创建一个专辑类扩展的类别,它将定义一个新的方法,这个方法会返回能很容易和 UITableViews 使用的数据结构。这个数据结构如下图所示:
 

 
为了给 Album 增加一个类别,导航到 “File\New\File...\" ,选择 Objective-C category 模板,不要习惯性的选择 Objective-C class 模板。在 Category 域输入 TableRepresentation Category on 域输入 Album.
注意 : 你已经注意到了新建文件的名字了吗? Album+TableRepresentation 意味着你正在扩展 Album 类。这种约定是非常重要,因为它方便阅读以及阻止和你或者其他人创建的类别冲突。
打开 Album+TableRepresentation.h 类,新增如下的方法原型:
- (NSDictionary*)tr_tableRepresentation;  
注意在方法开头有一个 tr_ 前缀,它是类别 TableRepresentation 的缩写。再一次,这种约定也可以阻止和其它的方法冲突。
注意 : 如果方法与原来类的方法重名了,或者与同样的类(甚至它的父类)的其它的扩展重名,那么运行期到底应该调用哪个方法是未定义的。当你仅仅是在扩展你自己写的类时,这没什么问题,    但是当你在扩展标准的 Cocoa  或者 Cocoa Touch 类的时候,它可能会导致严重的问题。
打开 Album+TableRepresentation.m ,增加下面的方法:
- (NSDictionary*)tr_tableRepresentation  
{  
    return @{@"titles":@[@"Artist", @"Album", @"Genre", @"Year"],  
             @"values":@[self.artist, self.title, self.genre, self.year]};  
}  
咋们稍停片刻来看看这个模式的强大之处:
1.  你可以直接使用 Album 的属性
2.  你不需要子类化就可以增加方法。当然如果你想子类化 Album ,你任然可以那样做。
3.  简简单单的几句代码就让你在不修改 Album 的情况下,返回了一个 UITableView 风格的 Album  
苹果在 Foundation 类中大量使用了 Category 。想知道他们是怎么做的,你可以代开 NSString.h 文件,找到 @interface NSString, 你将看到类和其它三个类别的定义: NSStringExtensionMethods,NSExtendedStringPropertyListParsing NSStringDeprecated. 类别让方法组织成相互独立的部分。  
Delegation(委托)
委托作为另外一个装饰器模式,它是一种和其它对象交互的机制。举例来说,当你使用 UITableView 的时候,你必须要实现 tableView:numberOfRowsInSection: 方法。
你不可能让 UITableView 知道它需要在每个区域显示多少行,因为这些是应用特定的数据。因此计算每个区域需要显示多少行的职责就给了 UITableView 的委托。这就让 UITableView 类独立于它要显示的数据。  
这里通过一个图来解释当你创建 UITableView 的时候会发生什么:


 
 
UITableView 的职责就是显示一个表格视图。然而最终它需要一些它自身没有的信息。那么它就求助于它的委托,通过发送消息给委托来获取信息。在 Objective-C 实现委托模式的时候,一个类可以通过协议( Protocol )来声明可选以及必要的方法。本指南稍后会涉及协议方面的内容。
子类化一个对象,复写需要的方法看起来好像更容易一点,但是考虑到你只能子类化一个类,如果你想一个对象作为两个或者更多对象的委托的话,使用子类化将不能实现。  
注意 : 这个是一个重要的模式。苹果在 UIKit 类中大量使用了它: UITableView ,  UITextView , UITextField ,  UIWebView ,  UIAlert ,  UIActionSheet ,  UICollectionView , UIPickerView , UIGestureRecognizer ,  UIScrollView 等等等。
 
如何使用委托模式
打开 ViewController.m 文件,在文件开头增加下面的导入语句:
#import "LibraryAPI.h"  
   #import "Album+TableRepresentation.h"  
现在,给类的扩展增加如下的私有变量,最终类的扩展如下所示:
@interfaceViewController () {  
    UITableView *dataTable;  
    NSArray *allAlbums;  
    NSDictionary *currentAlbumData;  
    int currentAlbumIndex;  
   }  
   
   @end  
然后用下面的代码取代类型扩展中 @interface 一行:  
@interface ViewController () <UITableViewDataSource, 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];  
}  
下面我们来解释一下上面代码中标记了数字的地方:
1.  改变背景色为漂亮的藏青色
2.  通过 API 获取专辑数据。你不需要直接使用 PersistencyManager
3.  创建 UITableView ,声明 viewController UITableView 的委托和数据源;因此 viewController 将提供所有的被 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: 从专辑数组中获取需要的专辑数据。当你想去显示新的数据的时候,你仅仅需要调用 reloadData. 这将使得 UITableView 去问委托一些如下的信息:表格视图有多少个区域,每个区域应该显示多少行,每个单元格长什么样。  
viewDidLoad 最后增加下面一行代码:  
[self showDataForAlbumAtIndex:currentAlbumIndex];  
上面的代码会在 app 启动的时候加载当前的专辑,因为 currentAlbumIndex 设置为 0 ,所以它将显示第一个专辑。
构建并运行你的工程;你将得到一个崩溃信息,在调试控制台上面将显示如下的异常:


 
 
这里出了什么问题?你声明 ViewController 作为 UITableView 的委托和数据源,但是这样做了,也就意味着你必须实现所必须的方法 - 这些方法包括你还没有实现的 tableView:numberOfRowsInSection: 方法
ViewController.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:numberOfRowsInSection: 方法返回表格视图需要显示的行数。这个和数据结构中的标题数量是匹配的。  
tableView:cellForRowAtIndexPath: 创建并返回一个包含标题和标题值的的单元格。
构建并运行你的工程,你的 app 应该会正常启动并显示下图所示的画面:


 
 
 
到目前为止进展挺顺利。但是你回忆第一张本 app 最终效果的图,你会发现在屏幕顶部有一个水平滚动视图在不同的专辑之间切换。并不是构建一个只为这次使用的单一目的的水平滚动视图,你为什么不做一个可以让任何视图复用的滚动视图呢?
为了使这个视图可以复用,应该由委托来决定所有的从左边开始依次到下一个对象的内容。水平滚动条应该声明那些能和它一起工作的委托方法,这有点类似 UITableView 的委托方法的方式。我们将在讨论下一个模式的时候来实现它。

原文出处:http://xmuzyq.iteye.com/blog/1942379
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值