IOS设计模式之四(备忘录模式,命令模式)

本文原文请见:http://www.raywenderlich.com/46988/ios-design-patterns.

由 @krq_tiger(http://weibo.com/xmuzyq)翻译,如果你发现有什么错误,请与我联系谢谢。

备忘录(Memento)模式

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

如何使用备忘录模式

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

Objective-c代码  

  1. - (void)saveCurrentState
  2. {
  3. // When the user leaves the app and then comes back again, he wants it to be in the exact same state
  4. // he left it. In order to do this we need to save the currently displayed album.
  5. // Since it‘s only one piece of information we can use NSUserDefaults.
  6. [[NSUserDefaultsstandardUserDefaults] setInteger:currentAlbumIndex forKey:@"currentAlbumIndex"];
  7. }
  8. - (void)loadPreviousState
  9. {
  10. currentAlbumIndex = [[NSUserDefaultsstandardUserDefaults] integerForKey:@"currentAlbumIndex"];
  11. [self showDataForAlbumAtIndex:currentAlbumIndex];
  12. }

saveCurrentState 保存当前的专辑索引到NSUserDefaults,NSUserDefaults是IOS提供的保存应用设置信息和数据的地方。

loadPreviousState 加载之前保存的索引。这里其实不是备忘录模式完整的实现,但是你已经了解到它了。

现在,在ViewController.m的viewDidLoad方法中,在scroller初始化之前增加下面的代码:

Objective-c代码  

  1. [self loadPreviousState];

它将在应用启动的时候加载原先保存的状态。但是在什么时候来保存应用的状态呢?你将使用通知来实现它。当应用进入后台的时候,IOS会发送UIApplicationDidEnterBackgroundNotification通知,你可以使用这个通知去保存状态,这是不是很方便?

在viewDidLoad中增加下面的代码:

Objective-c代码  

  1. [[NSNotificationCenterdefaultCenter] addObserver:self selector:@selector(saveCurrentState) name:UIApplicationDidEnterBackgroundNotification object:nil];

现在,当应用进入后台的时候,ViewController将通过saveCurrentState方法自动保存当前的状态。

现在增加下面的代码:

Objective-c代码  

  1. - (void)dealloc
  2. {
  3. [[NSNotificationCenterdefaultCenter] removeObserver:self];
  4. }

这将确保当ViewController被销毁的时候移除观察者。

构建和运行你的应用,导航到一个专辑,然后通过Command+Shift+H(模拟器的情况下)将app发送到后台,然后关闭app。再一次打开app,检查原先选择的专辑是不是被显示在中间:

看起来专辑数据是正确的,但是中间的视图却没有显示正确的专辑。出了什么情况?这是可选方法initialViewIndexForHorizontalScroller的目的所在。因为这个方法没有在委托中实现,这样的话初始化视图总是第一个视图。

为了修正这个问题,在ViewController.m中增加下面的代码:

Objective-c代码  

  1. - (NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller *)scroller
  2. {
  3. return currentAlbumIndex;
  4. }

现在HorizontalScroller的第一个视图终于设置为了currentAlbumIndex指定的视图。这使得app在下次使用的时候还保留了上次使用的状态。

再一次运行你的app,和之前一样滚动专辑,停止应用,重启,确保上面的问题已经修复了:

如果你查看PersistencyManager的init方法,你将注意到专辑数据被硬编码并且每次都要重新创建它们。但是更好的方式是创建专辑列表一次,然后存储它们到一个文件,你怎么保存专辑数据到一个文件呢?

一个可选的方式就是循环Album的属性,保存它们到一个plist文件中,当它们需要的时候再重新构建它们。这个不是一个最好的方式,因为你需要去编写与每个类的属性关联的特定的代码。举例来说如果过会你要创建一个具有不同属性的Movie类,保存和加载的代码需要重新写。

此外,你也不能保存每个类的私有变量,因为它们在外面的类中是不可见的。这正是苹果创建了归档(Archiving)机制的原因。(译者注:Java中这里也可以说是序列化)

归档(Archiving)

归档是苹果对于备忘录模式的特定实现之一。这种机制可以转换一个对象到一个可保存的数据流中,过会可以在不暴漏私有属性给外部的情况下重建它们。你可以在iOS 6 by Tutorials书的第16章读取更多关于此功能的信息,或者你也可以参考:Apple’s Archives and Serializations Programming Guide.

如何使用归档

首先,你需要声明Album可以被归档的,这需要Album遵循NSCoding协议。打开Album.h文件,改变@interface行为如下所示:

Objective-c代码  

  1. @interfaceAlbum : NSObject<NSCoding>

在Album.m中增加如下的两个方法:

Objective-c代码  

  1. - (void)encodeWithCoder:(NSCoder *)aCoder
  2. {
  3. [aCoder encodeObject:self.year forKey:@"year"];
  4. [aCoder encodeObject:self.title forKey:@"album"];
  5. [aCoder encodeObject:self.artist forKey:@"artist"];
  6. [aCoder encodeObject:self.coverUrl forKey:@"cover_url"];
  7. [aCoder encodeObject:self.genre forKey:@"genre"];
  8. }
  9. - (id)initWithCoder:(NSCoder *)aDecoder
  10. {
  11. self = [super init];
  12. if (self)
  13. {
  14. _year = [aDecoder decodeObjectForKey:@"year"];
  15. _title = [aDecoder decodeObjectForKey:@"album"];
  16. _artist = [aDecoder decodeObjectForKey:@"artist"];
  17. _coverUrl = [aDecoder decodeObjectForKey:@"cover_url"];
  18. _genre = [aDecoder decodeObjectForKey:@"genre"];
  19. }
  20. return self;
  21. }

你可以在归档一个类的实例对象的时候调用encodeWithCoder:,相反的当你要从归档中重建Album实例的时候,你可以调用initWithCoder:,这样做是不是很简单,但是它是一种强大的机制哦。

在PersistencyManager.h中,增加下面的签名(方法原型):

Objective-c代码  

  1. - (void)saveAlbums;

这个正是保存专辑的方法。

现在在PersistencyManager.m中,增加方法的实现:

Objective-c代码  

  1. - (void)saveAlbums
  2. {
  3. NSString *filename = [NSHomeDirectory() stringByAppendingString:@"/Documents/albums.bin"];
  4. NSData *data = [NSKeyedArchiverarchivedDataWithRootObject:albums];
  5. [data writeToFile:filename atomically:YES];
  6. }

NSKeyedArchiver归档专辑数据到albums.bin文件中。

当你在归档一个对象的时候,归档器会递归的归档对象包含的子对象以及子对象的子对象等等。在本例中,归档开始自一个名为albums的数组,因为NSArry和Album两者都支持NSCoding协议,因此数组中每个对象都会被归档.

现在用下面的代码取代PersistencyManager.m中的init方法:

Objective-c代码  

  1. - (id)init
  2. {
  3. self = [super init];
  4. if (self) {
  5. NSData *data = [NSDatadataWithContentsOfFile:[NSHomeDirectory() stringByAppendingString:@"/Documents/albums.bin"]];
  6. albums = [NSKeyedUnarchiverunarchiveObjectWithData:data];
  7. if (albums == nil)
  8. {
  9. albums = [NSMutableArrayarrayWithArray:
  10. @[[[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"],
  11. [[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"],
  12. [[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"],
  13. [[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"],
  14. [[Album alloc] initWithTitle:@"American Pie" artist:@"Madonna" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_madonna_american%20pie.png" year:@"2000"]]];
  15. [self saveAlbums];
  16. }
  17. }
  18. return self;
  19. }

新的代码中,如果专辑数据存在,NSKeyedUnarchiver会从文件中加载专辑数据,如果专辑数据不存在,它会创建专辑数据并立即保存它以便下次启动的时候使用。

你想在每次app进入后台的时候都保存专辑数据。现在这可能看起来不是很必要,但是如果过会你想增加一个修改专辑数据的选项呢?那时候你就想确保所有的改变都会被保存。

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

Objective-c代码  

  1. - (void)saveAlbums;

因为主应用通过LibraryAPI访问所有的服务,这样就要求PersistencyManager知道它负责保存专辑数据。

现在在LibraryAPI.m实现中增加方法实现:

Objective-c代码  

  1. - (void)saveAlbums
  2. {
  3. [persistencyManager saveAlbums];
  4. }

这个方法将调用LibraryAPI保存数据的请求委托给PersistencyManager处理。在ViewController.m中saveCurrentState方法末尾,增加如下的代码:

Objective-c代码  

  1. [[LibraryAPI sharedInstance] saveAlbums];

无论何时ViewController保存应用状态的时候,上面的代码使用LibraryAPI触发专辑数据的保存。

构建你的应用,检查每个资源是否被正确编译。

不幸的是,没有一个简单的方式去检查数据持久化的正确性。你可以通过Finder在应用的Documents目录查看到专辑数据文件已经被创建,但是为了能看到任何其它的变化,你还需要增加改变专辑数据的功能。

但是并不仅仅是改变数据,如果你需要删除不想要的专辑数据呢?另外,是不是可以很漂亮的来增加一个撤销删除的功能呢?

这就到了我们讨论下个设计模式(命令模式)的机会了。

命令模式

命令模式将一个请求封装为一个对象。封装以后的请求会比原生的请求更加灵活,因为这些封装后的请求可以在多个对象之间传递,存储以便以后使用,还可以动态的修改,或者放进一个队列中。苹果通过Target-Action机制和Invocation实现命令模式。

你可以通过苹果的官方在线文档阅读更多关于Target-Action的内容,至于Invocation,它采用了NSInvocation类,这个类包含了一个目标对象,方法选择器,以及一些参数。这个对象可以动态的修改并且可以按需执行。实践中它是一个命令模式很好的例子。它解耦了发送对象和接受对象,并且可以保存一个或者多个请求。

如何使用命令模式

在你深入了解invocation之前,你需要首先来设置一个支持撤销操作的大体骨架。所以你需要定义一个UIToolBar和用作撤销堆栈的NSMutableArray。

在ViewController.m的扩展中,在你定义其它变量的地方定义如下的变量:

Objective-c代码  

  1. UIToolbar *toolbar;
  2. // We will use this array as a stack to push and pop operation for the undo option
  3. NSMutableArray *undoStack;

这里我们创建了包含新增按钮的工具栏,同时还创建了一个用作命令存储队列的数组。

在viewDidLoad方法的第二个注释之前,增加下面的代码:

Objective-c代码  

  1. toolbar = [[UIToolbar alloc] init];
  2. UIBarButtonItem *undoItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemUndo target:self action:@selector(undoAction)];
  3. undoItem.enabled = NO;
  4. UIBarButtonItem *space = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
  5. UIBarButtonItem *delete = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemTrash target:self action:@selector(deleteAlbum)];
  6. [toolbar setItems:@[undoItem,space,delete]];
  7. [self.view addSubview:toolbar];
  8. undoStack = [[NSMutableArrayalloc] init];

上面的代码在工具栏上面增加了2个按钮和一个可变长度组件(flexible space),它还创建了一个空的撤销操作栈,刚开始撤销按钮是不可用的,因为撤销栈是空的。

另外你可能注意到工具条没有使用frame来初始化,因为viewDidLoad不是决定frame大小最终的地方。

在ViewController.m中增加如下设置frame大小的代码:

Objective-c代码  

  1. - (void)viewWillLayoutSubviews
  2. {
  3. toolbar.frame = CGRectMake(0, self.view.frame.size.height-44, self.view.frame.size.width, 44);
  4. dataTable.frame = CGRectMake(0, 130, self.view.frame.size.width, self.view.frame.size.height - 200);
  5. }

你将还需要在ViewController.m中增加三个方法来管理专辑:增加,删除,撤销。

第一个方法是增加一个新的专辑:

Objective-c代码  

  1. - (void)addAlbum:(Album*)album atIndex:(int)index
  2. {
  3. [[LibraryAPI sharedInstance] addAlbum:album atIndex:index];
  4. currentAlbumIndex = index;
  5. [self reloadScroller];
  6. }

在这里你增加专辑,并设置当前专辑索引,然后重新加载滚动视图。

接下来是删除方法:

Objective-c代码  

  1. - (void)deleteAlbum
  2. {
  3. // 1
  4. Album *deletedAlbum = allAlbums[currentAlbumIndex];
  5. // 2
  6. NSMethodSignature *sig = [self methodSignatureForSelector:@selector(addAlbum:atIndex:)];
  7. NSInvocation *undoAction = [NSInvocationinvocationWithMethodSignature:sig];
  8. [undoAction setTarget:self];
  9. [undoAction setSelector:@selector(addAlbum:atIndex:)];
  10. [undoAction setArgument:&deletedAlbum atIndex:2];
  11. [undoAction setArgument:&currentAlbumIndex atIndex:3];
  12. [undoAction retainArguments];
  13. // 3
  14. [undoStack addObject:undoAction];
  15. // 4
  16. [[LibraryAPI sharedInstance] deleteAlbumAtIndex:currentAlbumIndex];
  17. [self reloadScroller];
  18. // 5
  19. [toolbar.items[0] setEnabled:YES];
  20. }

上面的代码中有一些新的激动人心的特性,所以下面我们就来考虑每个被标注了注释的地方:

1. 获取需要删除的专辑

2. 定义了一个类型为NSMethodSignature的对象去创建NSInvocation,它将用来撤销删除操作。NSInvocation需要知道三件事情:选择器(发送什么消息),目标对象(发送消息的对象),还有就是消息所需要的参数。在上面的例子中,消息是与删除方法相反的操作,因为当你想撤销删除的时候,你需要将刚删除的数据回加回去。

3. 创建了undoAction以后,你需要将其增加到undoStack中。撤销操作将被增加在数组的末尾。

4. 使用LibraryAPI删除专辑,然后重新加载滚动视图。

5. 因为在撤销栈中已经有了操作,你需要使得撤销按钮可用。

注意:使用NSInvocation,你需要记住下面的几点:

1.参数必须以指针的形式传递.

2.参数从索引2开始,索引0,1为目标(target)和选择器(selector)保留。

3.如果参数有可能会被销毁,你需要调用retainArguments.

最后,增加下面的撤销方法:

Objective-c代码  

  1. - (void)undoAction
  2. {
  3. if (undoStack.count > 0)
  4. {
  5. NSInvocation *undoAction = [undoStack lastObject];
  6. [undoStack removeLastObject];
  7. [undoAction invoke];
  8. }
  9. if (undoStack.count == 0)
  10. {
  11. [toolbar.items[0] setEnabled:NO];
  12. }
  13. }

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

构建并运行的你应用,测试撤销机制,删除一个或者多个专辑,然后点击撤销按钮看看效果:

这里你正好也可以测试我们对专辑数据的变更是不是已经被存储了以便可以在不同的会话间使用。现在,你删除一条数据,将应用发送到后台,然后终止应用,下次应用启动的时候应该不会显示删除的专辑了。

接下来做啥?

你可以从这里下载完整的工程源代码:BlueLibrary-final

在本应用中,我们没有涉及到其它两个设计模式,但是我们还是要提一下它们:Abstract Factory (aka Class Cluster) and Chain of Responsibility (aka Responder Chain).你可以自由选择去阅读上面的两篇文字以扩展你对设计模式的认知范围。

在本指南中,你看到如何利用设计模式的威力以一种直接和松耦合的方式去解决复杂的任务。你已经学到了许多的设计模式以及 它们的概念:单例模式,MVC模式,委托模式,协议,门面模式,观察者模式,备忘录模式以及命令模式。

你最终的代码是松耦合,可复用以及可读的。如果另外一个开发者阅读你的代码,他们会马上理解代码逻辑以及每个类都做了什么。

我们并不是说要在你写的每句代码中使用设计模式。相反,我们要清楚的意识到可以用设计模式解决一些特定的问题,特别是在设计之初。他们会让作为开发者的生涯更加轻松,同时你的代码也将变的更加漂亮。

IOS设计模式之四(备忘录模式,命令模式),布布扣,bubuko.com

时间: 2024-10-09 08:52:30

IOS设计模式之四(备忘录模式,命令模式)的相关文章

IOS设计模式之二(门面模式,装饰器模式)

本文原文请见:http://www.raywenderlich.com/46988/ios-design-patterns. 由 @krq_tiger(http://weibo.com/xmuzyq)翻译,如果你发现有什么错误,请与我联系谢谢. 门面(Facade)模式(译者注:facade有些书籍译为门面,有些书籍译为外观,此处译为门面) 门面模式针对复杂的子系统提供了单一的接口,不需要暴漏一些列的类和API给用户,你仅仅暴漏一个简单统一的API. 下面的图解释了这个概念: 这个API的使用者

Java设计模式(九)责任链模式 命令模式

(十七)责任链模式 责任链模式的目的是通过给予多个对象处理请求的机会,已解除请求发送者与接受者之间的耦合关系.面对对象的开发力求对象之前保持松散耦合,确保对象各自的责任最小化,这样的设计可以使得系统更加容易修改,同时降低产生缺陷的风险. public class ChainTest { public static void main(String[] args) { String pass1="123456"; String pass2="123456"; Stri

java设计模式--行为型模式--命令模式

1 命令模式 2 概述 3 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化:对请求排队或记录请求日志,以及支持可撤消的操作. 4 5 6 适用性 7 1.抽象出待执行的动作以参数化某对象. 8 9 2.在不同的时刻指定.排列和执行请求. 10 11 3.支持取消操作. 12 13 4.支持修改日志,这样当系统崩溃时,这些修改可以被重做一遍. 14 15 5.用构建在原语操作上的高层操作构造一个系统. 16 17 18 参与者 19 1.Command 20 声明执行操作的接口.

JS设计模式(6)命令模式

什么是命令模式? 定义:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化. 主要解决:在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录.撤销或重做.事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适. 何时使用:在某些场合,比如要对行为进行"记录.撤销/重做.事务"等处理,这种无法抵御变化的紧耦合是不合适的.在这种情况下,如何将"行为请求者"与"行为实现者"解耦?将一组行为抽象为

设计模式课程 设计模式精讲 23-1 命令模式讲解

  1 课程讲解 1.1 类型: 1.2 定义: 1.3 适用场景: 1.4 优点: 1.5 缺点: 1.6 命令相关的设计模式 1 课程讲解 1.1 类型: 行为型 1.2 定义: ◆定义:将“请求”封装成对象,以便使用不同的请求 扩展:◆命令模式解决了应用程序中对象的职责以及它们之间的通信方式 详解:下命令对象只知道如何发送请求,不需要知道如何完成称请求 1.3 适用场景: ◆请求调用者和请求接收者需要解耦,使得调用者和接收者不直接交互 ◆需要抽象出等待执行的行为 1.4 优点: ◆降低耦合

javascript设计模式详解之命令模式

每种设计模式的出现都是为了弥补语言在某方面的不足,解决特定环境下的问题.思想是相通的.只不过不同的设计语言有其特定的实现.对javascript这种动态语言来说,弱类型的特性,与生俱来的多态性,导致某些设计模式不自觉的我们都在使用.只不过没有对应起来罢了.本文就力求以精简的语言去介绍下设计模式这个高大上的概念.相信会在看完某个设计模式之后有原来如此的感慨. 一.基本概念与使用场景: 基本概念: 将请求封装成对象,分离命令接受者和发起者之间的耦合. 命令执行之前在执行对象中传入接受者.主要目的相互

设计模式14:Command 命令模式(行为型模式)

Command 命令模式(行为型模式) 耦合与变化 耦合是软件不能抵御变化的根本性原因.不仅实体对象与实体对象之间存在耦合关系,实体对象与行为操作之间也存在耦合关系. 动机(Motivation) 在软件构建过程中,“行为请求者”与“行为实现者”通常呈现一种“紧耦合”.但在某些场合——比如对行为进行“记录.撤销/重做(undo/redo).事务”等处理,这种无法抵御变化的紧耦合是不合适的. 在这种情况下,如何将“行为请求者”与“行为实现者”解耦?将一组行为抽象为对象,可以实现二者之间的解耦. 意

设计模式学习笔记之命令模式

命令模式 将“请求”封装成对象,以便使用不同的请求.队列或者日志来参数化其他对象.命令模式也支持可撤销的操作. 说明: 1.命令模式将发出请求的对象和执行请求的对象解耦: 2.在被解耦的两者之间是通过命令对象进行沟通的.命令对象封装了接受者和一个或一组动作: 3.调用者通过调用命令对象的execute()发出请求,这会使得接受者的动作被调用: 4.调用者可以接受命令当做参数,甚至在运行时动态地进行: 5.命令可以支持撤销,做法事实现一个undo()方法来回到exexcute()被执行前的状态:

《Head First 设计模式》学习笔记——命令模式

在软件系统,"行为请求者"与"行为实施者"通常存在一个"紧耦合".但在某些场合,比方要对行为进行"记录.撤销/重做.事务"等处理,这样的无法抵御变化的紧耦合是不合适的.在这样的情况下.怎样将"行为请求者"与"行为实现者"解耦?将一组行为抽象为对象,实现二者之间的松耦合.这就是命令模式(Command Pattern)----题记 设计模式 命令模式:将"请求"封装成对