iOS设计模式汇总

     iOS设计模式-你可能已经听说过这个词,但是你真正理解它意味着什么吗?虽然大多数的开发者可能都会认为设计模式是非常重要的,然而关于设计模式这一主题的文章却不多,并且有时候我们开发者在写代码的时候也不会太关注它。

       在软件设计领域,设计模式是对通用问题的可复用的解决方案。设计模式是一系列帮你写出更可理解和复用代码的模板,设计模式帮你创建松耦合的代码以便你不需要费多大力就可以改变或者替换代码中的组件。

       如果你刚接触设计模式,我们有好消息告诉你!首先,多亏了Cocoa的构建方式,你已经使用了许多的设计模式以及被鼓励的最佳实践。

       其次本指南将带你使用绝大多数(并不是所有)Cocoa中频繁使用的IOS设计模式。


一.模型-视图-控制器(MVC)模式-设计模式之王

       模型-视图-控制器(MVC)是Cocoa的构建块之一,毫无疑问它是使用最频繁的设计模式。它根据通用的角色去划分类,这样就使得类的职责可以根据角色清晰的划分开来。


涉及到的三个角色如下:
Model:
       模型保存应用程序的数据,定义了怎么去操作它。例如在本应用中模型就是Album类。
View:
       视图是模型的可视化表示以及用户交互的控件;基本上来说,所有的UIView对象以及它的子类都属于视图。在本应用中AlbumView代表了视图。
Controller:
       控制器是一个协调所有工作的中介者(Mediator)。它访问模型中的数据并在视图中展示它们,同时它们还监听事件和根据需要操作数据。你可以猜猜哪个类是控制器吗?它正是:ViewController。

       一个MVC模式的好的实现也就意味着每一个对象都会被划分到上面所说的组中。

       我们可以很好的用下图来描述通过控制器实现的视图到模型的交互过程:

       模型会把任何数据的变更通知控制器,然后控制器更新视图数据。视图对象通知控制器用户的操作,控制器要么根据需要来更新模型,要么检索任何被请求的数据。

       你可能在想为什么不能仅仅使用控制器,在一个类中实现视图和模型,这样貌似更加容易

       所有的这些都归结于代码关注点分离以及复用。在理想的状态下,视图应该和模型完全的分离。如果视图不依赖某个实际的模型,那么视图就可以被复用来展示不同模型的数据。

       举个例子来说,如果将来你打算加入电影或者书籍到你的资料库中,你仍然可以使用同样的AlbumView去显示电影和书籍数据。更进一步来说,如果你想创建一个新的与专辑有关联的工程,你可以很简单的复用Album类,因为它不依赖任何视图。这就是MVC的强大之处。

 

如何使用MVC模式

       首先,你需要确保在你工程中的每个类是控制器,模型和视图中的一种,不要在一个类中组合两种角色的功能。到目前为止,你创建了一个Album类和AlbumView类,这样做挺好的。

       其次,为了确保你能符合这种工作方法,你应该创建三个工程组(Project Group)来保存你的代码,每个工程组只存放一种类型的代码。

      导航到”文件\新建\组(File\New\Group)”(或者按下Command + Option + N),命名组为Model,重复同样的过程来创建View和Controller组。

       现在拖动Album.h和Album.m去模型组,拖动AlbumView.h和AlbumView.m去视图组,最后拖动ViewController.h和ViewController.m到控制器组。

       此时工程结构应该看起来和下图类似:

       没有了之前所有文件都散落在各处,现在你的工程已经开起来好多了。显然你也可以有其它的组和类,但是本应用的核心包含在这三个类别中(Model,View,Controller)。

       现在所有的组件都已经安排好了,你需要从某处获取专辑数据。你将创建一个贯穿于代码的管理数据的API-这也就代表将有机会去讨论下一个设计模式-单例(单态)模式。

二.单例(单态)模式


       单例设计模式确保对于一个给定的类只有一个实例存在,这个实例有一个全局唯一的访问点。它通常采用懒加载的方式在第一次用到实例的时候再去创建它。

       注意:苹果大量使用了此模式。例如:[NSUserDefaults standardUserDefaults],[UIApplication sharedApplication],[UIScreen mainScreen],[NSFileManager defaultManager],所有的这些方法都返回一个单例对象。

       你很可能会想为什么这么关心是否一个类有多个实例?毕竟代码和内存都是廉价的,对吗?

       有一些情况下,只有一个实例显得非常合理。举例来说,你不需要有多个Logger的实例,除非你想去写多个日志文件。或者一个全局的配置处理类:实现线程安全的方式访问共享实例是容易的,比如一个配置文件,有好多个类同时修改这个文件。

 

如何使用单例模式

首先来看看下面的图:

       上面的图描述了一个有单一属性(它就是单一实例)和sharedInstance,init两个方法的类。客户端第一次发送sharedInstance消息的时候,instance属性尚未被初始化,所以此时你需要创建一个新的实例,然后返回它的引用。

       当你下一次调用sharedInstance的时候,instance不需要任何初始化可以立即返回。这个逻辑保证总是只有一个实例。你接下来将用这个模式来创建一个管理所有专辑数据的类。你将注意到工程中有一个API的组,在这个组里你可以放入给你应用提供服务的所有类。在此组中,用IOS\Cocoa Touch\Objective-C class模板创建一个新类,命名它为LibraryAPI,设置父类为NSObject.
       打开LibraryAPI.h,用如下代码替换它的内容:

<span style="font-size:14px;">@interfaceLibraryAPI : NSObject
 
+ (LibraryAPI*)sharedInstance;
 
@end</span>

       现在打开LibraryAPI.m,在@implementation那一行后面插入下面的方法:

<span style="font-size:14px;">+ (LibraryAPI*)sharedInstance
{
    // 1
    static LibraryAPI *_sharedInstance = nil;
 
    // 2
    static dispatch_once_t oncePredicate;
 
    // 3
    dispatch_once(&oncePredicate, ^{
        _sharedInstance = [[LibraryAPI alloc] init];
    });
    return _sharedInstance;
}</span>

在这个简短的方法中,有一些需要需要注意的点:

  • 1.声明一个静态变量去保存类的实例,确保它在类中的全局可用性。
  • 2.声明一个静态变量dispatch_once_t ,它确保初始化器代码只执行一次
  • 3.使用Grand Central Dispatch(GCD)执行初始化LibraryAPI变量的block.这正是单例模式的关键:一旦类已经被初始化,初始化器永远不会再被调用。

       下一次你调用sharedInstance的时候,dispatch_once块中的代码将不会执行(因为它已经被执行了一次),你将得到原先已经初始化好的实例。

       你现在有一个单例的对象作为管理专辑数据的入口。咋们更进一步来创建一个处理资料库数据持久化的类。

       在API组中,使用iOS\Cocoa Touch\Objective-C class模板创建一个新类,命名它为PersistencyManager,设置父类为NSObject.
       打开PersistencyManager.h在文件头部增加下面的导入语句:

#import “Album.h”

      接下来,在PersistenceManager.h文件的@interface之后,增加下面的代码:

<span style="font-size:14px;">- (NSArray*)getAlbums;
- (void)addAlbum:(Album*)album atIndex:(int)index;
- (void)deleteAlbumAtIndex:(int)index;</span>

       上面是你需要处理专辑数据的方法的原型。

       打开PersistencyManager.m文件,在@implementation行之前,增加下面的代码:

<span style="font-size:14px;">@interfacePersistencyManager () {
    // an array of all albums
    NSMutableArray *albums;
}</span>

       上面增加了一个类扩张(class extension),这是另外一个增加私有方法和变量以至于外部类不会看到它们的方式。这里,你申明了一个数组NSMutableArry来保存专辑数据。这个数组是可变的方便你增加和删除专辑。

       现在在PersistencyManager.m文件中@implementation行之后增加如下代码:

<span style="font-size:14px;">- (id)init
{
    self = [super init];
    if (self) {
        // a dummy list of albums
        albums = [NSMutableArrayarrayWithArray:
                 @[[[Album alloc] initWithTitle:@"Best of Bowie" artist:@"David Bowie" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_david%20bowie_best%20of%20bowie.png" year:@"1992"],
                 [[Album alloc] initWithTitle:@"It's My Life" artist:@"No Doubt" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_no%20doubt_its%20my%20life%20%20bathwater.png" year:@"2003"],
                 [[Album alloc] initWithTitle:@"Nothing Like The Sun" artist:@"Sting" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_sting_nothing%20like%20the%20sun.png" year:@"1999"],
                 [[Album alloc] initWithTitle:@"Staring at the Sun" artist:@"U2" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_u2_staring%20at%20the%20sun.png" year:@"2000"],
                 [[Album alloc] initWithTitle:@"American Pie" artist:@"Madonna" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_madonna_american%20pie.png" year:@"2000"]]];
    }
    return self;
}</span>

       在init中,你用五条样例专辑填充数组。如果你不喜欢上面的专辑,你可以自由用你喜欢的专辑替换它们。

       现在在PersistencyManager.m文件中增加下面的三个方法:

<span style="font-size:14px;">- (NSArray*)getAlbums
{
    return albums;
}
 
- (void)addAlbum:(Album*)album atIndex:(int)index
{
    if (albums.count >= index)
        [albums insertObject:album atIndex:index];
    else
        [albums addObject:album];
}
 
- (void)deleteAlbumAtIndex:(int)index
{
    [albums removeObjectAtIndex:index];
}</span>
       这些方法让你可以增加和删除专辑。构建你的工程确保每个资源都可以被正确的编译。

三.外观(Facade)模式

       外观模式针对复杂的子系统提供了单一的接口,不需要暴露一些列的类和API给用户,你仅仅公开一个简单统一的API。下面的图解释了这个概念:

       这个API的使用者完全不需要关心背后的复杂性。这个模式非常适合有一大堆很难使用或者理解的类的情况。外观模式解耦了使用系统的代码和需要隐藏的接口和实现类。它也降低了外部代码对内部子系统的依赖性。当隐藏在外观之后的类很容易发生变化的时候,此模式就很有用了,因为当背后的类发生变化的时候,外观类始终保持了同样的API。

       举个例子来说,如果有一天你想取代后端服务,你不需要改变API的使用者,因为API没有发生变化。


如何使用外观模式

       当前你已经用PersistencyManager本地保存专辑数据,使用HTTPClient处理远程连接,工程中的其它类暂时与本次实现的逻辑无关。为了实现这个模式,只有LibraryAPI应该保存PersistencyManager和HTTPClient的实例,然后LibraryAPI将暴漏一个简单的API去访问这些服务。

       注意:通常来说,单例类的生命周期贯穿于整个应用的生命周期中,你不应对保存太多其它对象的强引用,因为他们只有到应用关闭的时候才能被释放。

       本次设计看起来像下图:


       LibraryAPI将暴漏给其它代码,但是它隐藏了HTTPClient和PersistencyManager的复杂性。

       打开LibraryAPI.h,在文件头部增加下面的导入语句:

<span style="font-size:14px;">#import "Album.h"</span>
       接下来,在LibraryAPI.h中增加如下的方法定义:
<span style="font-size:14px;">- (NSArray*)getAlbums;
- (void)addAlbum:(Album*)album atIndex:(int)index;
- (void)deleteAlbumAtIndex:(int)index;</span>
       目前有一些你需要暴漏给其它类的方法。打开LibraryAPI.m,增加如下的两个导入语句:
<span style="font-size:14px;">#import "PersistencyManager.h"
#import "HTTPClient.h"</span>
       这里将是唯一的导入这两个类的地方。记住:你的API是对于复杂系统唯一的访问点。现在,增加通过类扩展(class extension)增加一些私有的变量(在@implementation行之上):
<span style="font-size:14px;">@interfaceLibraryAPI () {
    PersistencyManager *persistencyManager;
    HTTPClient *httpClient;
    BOOL isOnline;
 
}
@end</span>
       isOnline决定了是否服务器中任何专辑数据的改变应该被更新,例如增加或者删除专辑。你现在需要通过init初始化这些变量。在LibraryAPI.m中增加如下的代码:
<span style="font-size:14px;"><span style="font-size:14px;">- (id)init{</span>
    self = [super init];
 
    if (self) {
 
        persistencyManager = [[PersistencyManager alloc] init];
 
        httpClient = [[HTTPClient alloc] init];
 
        isOnline = NO;
 
    }
 
    return self;
 
}</span>

       HTTP客户端实际上不会真正的和一个服务器交互,它在这里仅仅是用来演示外观模式的使用,所以isOnline将总是NO。

       接下来,增加如下的三个方法到LibraryAPI.m:

<span style="font-size:14px;">-(NSArray*)getAlbums
{
    return [persistencyManager getAlbums];
}
 
- (void)addAlbum:(Album*)album atIndex:(int)index
{
    [persistencyManager addAlbum:album atIndex:index];
 
    if (isOnline)
 
    {
 
        [httpClient postRequest:@"/api/addAlbum" body:[album description]];
 
    }
 
}
 
- (void)deleteAlbumAtIndex:(int)index
{
 
    [persistencyManager deleteAlbumAtIndex:index];
 
    if (isOnline)
    {
 
        [httpClient postRequest:@"/api/deleteAlbum" body:[@(index) description]];
 
    }
 
}</span>

          我们来看一看addAlbum:atIndex:.这个类首先更新本地的数据,然后如果有网络连接,它更新远程服务器。这就是外观模式的强大之处。当某些外部的类增加一个新的专辑的时候,它不知道也不需要知道背后的复杂性。

       注意:当为子系统的类设计外观的时候,要记住:任何东西都不能阻止客户端直接访问这些隐藏的类。不要对这些防御性的代码太过于吝啬,并且也不要假设所有的客户端都会和外观一样使用你的类。


四.装饰器(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类,新增如下的方法原型:

<span style="font-size:14px;">- (NSDictionary*)tr_tableRepresentation;</span>

       注意在方法开头有一个tr_前缀,它是类别TableRepresentation的缩写。再一次,这种约定也可以阻止和其它的方法冲突。

       注意:如果方法与原来类的方法重名了,或者与同样的类(甚至它的父类)的其它的扩展重名,那么运行期到底应该调用哪个方法是未定义的。当你仅仅是在扩展你自己写的类时,这没什么问题,但是当你在扩展标准的Cocoa或者Cocoa Touch类的时候,它可能会导致严重的问题。
       打开Album+TableRepresentation.m,增加下面的方法:

<span style="font-size:18px;">- (NSDictionary*)tr_tableRepresentation
{
    return @{@"titles":@[@"Artist", @"Album", @"Genre", @"Year"],
             @"values":@[self.artist, self.title, self.genre, self.year]};
}</span>

      咱们稍停片刻,来看看这个模式的强大之处:

  • 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文件,在文件开头增加下面的导入语句:

<span style="font-size:14px;">#import "LibraryAPI.h"
#import "Album+TableRepresentation.h"</span>
       现在,给类的扩展增加如下的私有变量,最终类的扩展如下所示:
<span style="font-size:14px;">@interfaceViewController () {
    UITableView *dataTable;
    NSArray *allAlbums;
    NSDictionary *currentAlbumData;
    int currentAlbumIndex;
}
 
@end</span>
       然后用下面的代码取代类型扩展中@interface一行:
<span style="font-size:14px;">@interface ViewController ()  {</span>

       这就是如何使得委托符合协议,你可以把它认为是委托履行协议方法契约的约定。在这里,你指定ViewController将实现UITableViewDataSource和UITableViewDelegate协议。这种方式使得UITableView非常确定那些委托必须要实现的方法。

       接下来,用如下代码取代viewDidLoad方法:

<span style="font-size:14px;">- (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];
}</span>

下面我们来解释一下上面代码中标记了数字的地方:

  • 1.改变背景色为漂亮的藏青色
  • 2.通过API获取专辑数据。你不需要直接使用PersistencyManager。
  • 3.创建UITableView,声明viewController为UITableView的委托和数据源;因此viewController将提供所有的被UITableView需要的信息。

现在,在ViewController.m中增加如下的方法:

<span style="font-size:14px;">- (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];
}</span>

       showDataForAlbumAtIndex:从专辑数组中获取需要的专辑数据。当你想去显示新的数据的时候,你仅仅需要调用reloadData.这将使得UITableView去问委托一些如下的信息:表格视图有多少个区域,每个区域应该显示多少行,每个单元格长什么样。

       在viewDidLoad最后增加下面一行代码:

<span style="font-size:14px;">[self showDataForAlbumAtIndex:currentAlbumIndex];</span>

        上面的代码会在app启动的时候加载当前的专辑,因为currentAlbumIndex设置为0,所以它将显示第一个专辑。

        构建并运行你的工程;你将得到一个崩溃信息,在调试控制台上面将显示如下的异常:


        这里出了什么问题?你声明ViewController作为UITableView的委托和数据源,但是这样做了,也就意味着你必须实现所必须的方法-这些方法包括你还没有实现的tableView:numberOfRowsInSection:方法.

        在ViewController.m文件中@implementation和@end之间的任何位置,增加下面的代码:

<span style="font-size:14px;">- (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;
}</span>
       tableView:numberOfRowsInSection:方法返回表格视图需要显示的行数。这个和数据结构中的标题数量是匹配的。tableView:cellForRowAtIndexPath:创建并返回一个包含标题和标题值的的单元格。


六.适配器(Adapter)模式

       适配器可以让一些接口不兼容的类一起工作。它包装一个对象然后暴漏一个标准的交互接口。

       如果你熟悉适配器设计模式,苹果通过一个稍微不同的方式来实现它,苹果使用了协议的方式来实现。你可能已经熟悉UITableViewDelegate, UIScrollViewDelegate, NSCoding 和 NSCopying协议。举个例子,使用NSCopying协议,任何类都可以提供一个标准的copy方法。


       不太常用,这里不做过多解释了



七.观察者(Observer)模式

       在观察者模式中,一个对象任何状态的变更都会通知另外的对改变感兴趣的对象。这些对象之间不需要知道彼此的存在,这其实是一种松耦合的设计。当某个属性变化的时候,我们通常使用这个模式去通知其它对象。

       此模式的通用实现中,观察者注册自己感兴趣的其它对象的状态变更事件。当状态发生变化的时候,所有的观察者都会得到通知。苹果的推送通知(Push Notification)就是一个此模式的例子。

       如果你要遵从MVC模式的概念,你需要让模型对象和视图对象在不相互直接引用的情况下通信。这正是观察者模式的用武之地。

       Cocoa通过通知(Notifications)和Key-Value Observing(KVO)来实现观察者模式。


通知(Notifications)

        不要和远程推送以及本地通知所混淆,通知是一种基于订阅-发布模式的模型,它让发布者可以给订阅者发送消息,并且发布者不需要对订阅者有任何的了解。

        通知在苹果官方被大量的使用。举例来说,当键盘弹出或者隐藏的时候,系统会独立发送  UIKeyboardWillShowNotification/UIKeyboardWillHideNotification 通知。当你的应用进入后台运行的时候,系统会发送一个UIApplicationDidEnterBackgroundNotification 通知。

       注意:打开UIApplication.h,在文件的末尾,你将看到一个由系统发出的超过20个通知组成的列表。


如何使用通知(Notifications)

       打开AlbumView.m,在initWithFrame:albumCover::方法的[self addSubview:indicator];语句之后加入如下代码:

<span style="font-size:14px;">[[NSNotificationCenterdefaultCenter] postNotificationName:@"BLDownloadImageNotification"
                                                    object:self
                                                  userInfo:@{@"imageView":coverImage, @"coverUrl":albumCover}];</span>

       这行代码通过NSNotificationCenter单例发送了一个通知。这个通知包含了UIImageView和需要下载的封面URL,这些是你下载任务所需要的所有信息。

       在LibraryAPI.m文件init方法的isOnline=NO之后,增加如下的代码:

<span style="font-size:14px;">[[NSNotificationCenterdefaultCenter] addObserver:self selector:@selector(downloadImage:) name:@"BLDownloadImageNotification" object:nil];</span>

       这个是观察者模式中两部分的另外一部分:观察者。每次AlbumView发送一个BLDownloadImageNotification通知,因为LibraryAPI已经注册为同样的通知的观察者,那么系统就会通知LibraryAPI,LibraryAPI又会调用downloadImage:来响应。

       然而在你实现downloadImage:方法之前,你必须在你的对象销毁的时候,退订所有之前订阅的通知。如果你不能正确的退订的话,一个通知发送给一个已经销毁的对象会导致你的app崩溃。

       在Library.m中增加下面的代码:

- (void)dealloc
{
    [[NSNotificationCenterdefaultCenter] removeObserver:self];
 
}

       当对象被销毁的时候,它将移除所有监听通知的观察者。

       还有一件事情需要去做,将已经下载的封面图片本地存储起来是个不错的主意,这样可以避免每次都重新下载相同的封面。

       打开PersistencyManager.h文件,增加下面两个方法原型:

<span style="font-size:14px;">- (void)saveImage:(UIImage*)image filename:(NSString*)filename;
 
- (UIImage*)getImage:(NSString*)filename;</span>
      在PersistencyManager.m文件中,增加方法的实现:
<span style="font-size:14px;">- (void)saveImage:(UIImage*)image filename:(NSString*)filename
 
{
 
    filename = [NSHomeDirectory() stringByAppendingFormat:@"/Documents/%@", filename];
 
    NSData *data = UIImagePNGRepresentation(image);
 
    [data writeToFile:filename atomically:YES];
 
}
 
- (UIImage*)getImage:(NSString*)filename
 
{
 
    filename = [NSHomeDirectory() stringByAppendingFormat:@"/Documents/%@", filename];
 
    NSData *data = [NSDatadataWithContentsOfFile:filename];
 
    return [UIImage imageWithData:data];
 
}</span>

       上面的代码相当直接。下载的图片会被保存在文档(Documents)目录,如果在文档目录不存在指定的文件,getImage:方法将返回nil.

       现在在LibraryAPI.m中增加下面的方法:

<span style="font-size:14px;">- (void)downloadImage:(NSNotification*)notification
 
{
 
    
// 1
 
    UIImageView *imageView = notification.userInfo[@"imageView"];
 
    NSString *coverUrl = notification.userInfo[@"coverUrl"];
 
    
// 2
 
    imageView.image = [persistencyManager getImage:[coverUrl lastPathComponent]];
 
    if (imageView.image == nil)
 
    {
 
        
// 3
 
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
 
            UIImage *image = [httpClient downloadImage:coverUrl];
 
            
// 4
 
            dispatch_sync(dispatch_get_main_queue(), ^{
 
                imageView.image = image;
 
                [persistencyManager saveImage:image filename:[coverUrl lastPathComponent]];
 
            });
 
        });
 
    }   
 
}</span>

下面是以上代码分段描述:

  • 1. downloadImage方法是通过通知被执行的,所以通知对象会当作参数传递。UIImageView和图片URL都会从通知中获取。
  • 2. 如果图片已经被下载过了,直接从PersistencyManager方法获取。
  • 3. 如果图片还没有被下载,通过HTTPClient去获取它。
  • 4. 当图片下载的时候,将它显示在UIImageView中,同时使用PersistencyManager保存到本地。

再一次,你使用了门面(Facade)模式隐藏了下载图片的复杂性。通知的发送者不需要关心图片是来自网络还是来自本地文件系统。


八.Key-Value Observing(KVO)模式

       在KVO中,一个对象可以要求在它自身或者其它对象的属性发送变化的时候得到通知。如果你对KVO感兴趣的话,可以百度查找更多相关资料.


如何使用KVO


       正如上面所说的,KVO机制让对象可以感知到属性的变化。在本例中,你可以使用KVO去观察UIImageView的image属性的变化。

       打开AlbumView.m文件,在initWithFrame:albumCover:方法[self addSubview:indicator]这一行后,增加下面的代码:

[coverImage addObserver:self forKeyPath:@"image" options:0 context:nil];
       这里它增加了它自己(当前的类)作为image属性的观察者。当完成的时候,你同样需要注销相应的观察者。仍然在AlbumView.m中增加下面的代码:
- (void)dealloc
 {
      [coverImage removeObserver:self forKeyPath:@"image"];
}

       最后增加下面的方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
 
{
 
    if ([keyPath isEqualToString:@"image"])
 
    {
 
        [indicator stopAnimating];
 
    }
 
}

       你必须在每个观察者类中实现这个方法。系统会在被观察的属性发送变化的时候通知观察者。在上面的代码中,当image属性变化的时候,你停止了封面上面的旋转提示器。这样以来,当图片加载完后,旋转提示器将会停止。

       注意:你要总是记得去移除已经销毁的观察者,否则当给不存在的观察者发送消息的时候,你的应用可能会崩溃。


九.备忘录(Memento)模式


       备忘录模式快照对象的内部状态并将其保存到外部。换句话说,它将状态保存到某处,过会你可以不破坏封装的情况下恢复对象的状态,也就是说原来对象中的私有数据仍然是私有的。

       不常用,不做过多说明了.


.命令模式


       命令模式将一个请求封装为一个对象。封装以后的请求会比原生的请求更加灵活,因为这些封装后的请求可以在多个对象之间传递,存储以便以后使用,还可以动态的修改,或者放进一个队列中。苹果通过Target-Action机制和Invocation实现命令模式。
       你可以通过苹果的官方在线文档阅读更多关于Target-Action的内容,至于Invocation,它采用了NSInvocation类,这个类包含了一个目标对象,方法选择器,以及一些参数。这个对象可以动态的修改并且可以按需执行。实践中它是一个命令模式很好的例子。它解耦了发送对象和接受对象,并且可以保存一个或者多个请求。

如何使用命令模式

       在你深入了解invocation之前,你需要首先来设置一个支持撤销操作的大体骨架。所以你需要定义一个UIToolBar和用作撤销堆栈的NSMutableArray。
       在ViewController.m的扩展中,在你定义其它变量的地方定义如下的变量:
<span style="font-size:14px;">UIToolbar *toolbar;
 
    // We will use this array as a stack to push and pop operation for the undo option
 
NSMutableArray *undoStack;</span>
       这里我们创建了包含新增按钮的工具栏,同时还创建了一个用作命令存储队列的数组。
       在viewDidLoad方法的第二个注释之前,增加下面的代码:
toolbar = [[UIToolbar alloc] init];
 
   UIBarButtonItem *undoItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemUndo target:self action:@selector(undoAction)];
 
   undoItem.enabled = NO;
 
   UIBarButtonItem *space = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
 
   UIBarButtonItem *delete = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemTrash target:self action:@selector(deleteAlbum)];
 
   [toolbar setItems:@[undoItem,space,delete]];
 
   [self.view addSubview:toolbar];
 
  undoStack = [[NSMutableArrayalloc] init];

       上面的代码在工具栏上面增加了2个按钮和一个可变长度组件(flexible space),它还创建了一个空的撤销操作栈,刚开始撤销按钮是不可用的,因为撤销栈是空的。
       另外你可能注意到工具条没有使用frame来初始化,因为viewDidLoad不是决定frame大小最终的地方。
       在ViewController.m中增加如下设置frame大小的代码:
- (void)viewWillLayoutSubviews
 
{
 
    toolbar.frame = CGRectMake(0, self.view.frame.size.height-44, self.view.frame.size.width, 44);
 
    dataTable.frame = CGRectMake(0, 130, self.view.frame.size.width, self.view.frame.size.height - 200);
 
}

你将还需要在ViewController.m中增加三个方法来管理专辑:增加,删除,撤销。
第一个方法是增加一个新的专辑:
- (void)addAlbum:(Album*)album atIndex:(int)index
 
{
 
    [[LibraryAPI sharedInstance] addAlbum:album atIndex:index];
 
    currentAlbumIndex = index;
 
    [self reloadScroller];
 
}

在这里你增加专辑,并设置当前专辑索引,然后重新加载滚动视图。
接下来是删除方法:
- (void)deleteAlbum
 
{
 
    // 1
 
    Album *deletedAlbum = allAlbums[currentAlbumIndex];
 
    // 2
 
    NSMethodSignature *sig = [self methodSignatureForSelector:@selector(addAlbum:atIndex:)];
 
    NSInvocation *undoAction = [NSInvocationinvocationWithMethodSignature:sig];
 
    [undoAction setTarget:self];
 
    [undoAction setSelector:@selector(addAlbum:atIndex:)];
 
    [undoAction setArgument:&deletedAlbum atIndex:2];
 
    [undoAction setArgument:¤tAlbumIndex atIndex:3];
 
    [undoAction retainArguments];
 
    // 3
 
    [undoStack addObject:undoAction];
 
    // 4
 
    [[LibraryAPI sharedInstance] deleteAlbumAtIndex:currentAlbumIndex];
 
    [self reloadScroller];
 
    // 5
 
    [toolbar.items[0] setEnabled:YES];
 
}

上面的代码中有一些新的激动人心的特性,所以下面我们就来考虑每个被标注了注释的地方:
    1. 获取需要删除的专辑
    2. 定义了一个类型为NSMethodSignature的对象去创建NSInvocation,它将用来撤销删除操作。NSInvocation需要知道三件事情:选择器(发送什么消息),目标对象(发送消息的对象),还有就是消息所需要的参数。在上面的例子中,消息是与删除方法相反的操作,因为当你想撤销删除的时候,你需要将刚删除的数据回加回去。
    3. 创建了undoAction以后,你需要将其增加到undoStack中。撤销操作将被增加在数组的末尾。
    4. 使用LibraryAPI删除专辑,然后重新加载滚动视图。
    5. 因为在撤销栈中已经有了操作,你需要使得撤销按钮可用。
注意:使用NSInvocation,你需要记住下面的几点:
1.参数必须以指针的形式传递.
2.参数从索引2开始,索引0,1为目标(target)和选择器(selector)保留。
3.如果参数有可能会被销毁,你需要调用retainArguments.
最后,增加下面的撤销方法:
- (void)undoAction
 
{
 
    if (undoStack.count > 0)
 
    {
 
        NSInvocation *undoAction = [undoStack lastObject];
 
        [undoStack removeLastObject];
 
        [undoAction invoke];
 
    }
 
    if (undoStack.count == 0)
 
    {
 
        [toolbar.items[0] setEnabled:NO];
 
    }
 
}

撤销操作弹出栈顶的NSInvocation对象,然后通过invoke调用它。这将调用你在原先删除专辑的时候创建的命令,将删除的专辑加回专辑列表。因为你已经删除了一个栈中的对象,所以你需要去检查栈是否为空,如果为空,也就意味着不需要进行撤销操作了,你这时候需要将撤销按钮设置为不可用。
构建并运行的你应用,测试撤销机制,删除一个或者多个专辑,然后点击撤销按钮看看效果:

这里你正好也可以测试我们对专辑数据的变更是不是已经被存储了以便可以在不同的会话间使用。现在,你删除一条数据,将应用发送到后台,然后终止应用,下次应用启动的时候应该不会显示删除的专辑了。
接下来做啥?
你可以从这里下载完整的工程源代码: BlueLibrary-final
    在本应用中,我们没有涉及到其它两个设计模式,但是我们还是要提一下它们: Abstract Factory (aka Class Cluster) and  Chain of Responsibility (aka Responder Chain).你可以自由选择去阅读上面的两篇文字以扩展你对设计模式的认知范围。
在本指南中,你看到如何利用设计模式的威力以一种直接和松耦合的方式去解决复杂的任务。你已经学到了许多的设计模式以及 它们的概念:单例模式,MVC模式,委托模式,协议,门面模式,观察者模式,备忘录模式以及命令模式。
你最终的代码是松耦合,可复用以及可读的。如果另外一个开发者阅读你的代码,他们会马上理解代码逻辑以及每个类都做了什么。
我们并不是说要在你写的每句代码中使用设计模式。相反,我们要清楚的意识到可以用设计模式解决一些特定的问题,特别是在设计之初。他们会让作为开发者的生涯更加轻松,同时你的代码也将变的更加漂亮。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1、 IOS设计模式的六大设计原则之单一职责原则(SRP,Single Responsibility Principle) 定义   就一个类而言,应该仅有一个引起它变化的原因。 定义解读   这是六大原则中最简单的一种,通俗点说,就是不存在多个原因使得一个类发生变化,也就是一个类只负责一种职责的工作。 优点 类的复杂度降低,一个类只负责一个功能,其逻辑要比负责多项功能简单的多; 类的可读性增强,阅读起来轻松; 可维护性强,一个易读、简单的类自然也容易维护; 变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。 问题提出   假设有一个类C,它负责两个不同的职责:职责P1和P2。当职责P1需求发生改变而需要修改类C时,有可能会导致原本运行正常的职责P2功能发生故障。 解决方案   遵循单一职责原则。分别建立两个类C1、C2,使C1完成职责P1,C2完成职责P2。这样,当修改类C1时,不会使职责P2发生故障风险;同理,当修改C2时,也不会使职责P1发生故障风险。   说到这里,大家会觉得这个原则太简单了。稍有经验的程序员,即使没有听说过单一职责原则,在设计软件时也会自觉的遵守这一重要原则。在实际的项目开发中,谁也不希望因为修改了一个功能导致其他的功能发生故障。而避免出现这一问题的方法便是遵循单一职责原则。虽然单一职责原则如此简单,并且被认为是常识,即便是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。为什么会出现这种现象呢?因为有职责扩散。实际项目中,因为某种原因,职责P被分化为粒度更细的职责P1和P2。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值