備忘錄(Memento)模式
備忘錄模式快照物件的內部狀態並將其儲存到外部。換句話說,它將狀態儲存到某處,過會你可以不破壞封裝的情況下恢復物件的狀態,也就是說原來物件中的私有資料仍然是私有的。
如何使用備忘錄模式
在ViewController.m中增加下面的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
- (void)saveCurrentState { // When the user leaves the app and then comes back again, he wants it to be in the exact same state // he left it. In order to do this we need to save the currently displayed album. // Since it's only one piece of information we can use NSUserDefaults. [[NSUserDefaultsstandardUserDefaults] setInteger:currentAlbumIndex forKey:@"currentAlbumIndex"]; } - (void)loadPreviousState { currentAlbumIndex = [[NSUserDefaultsstandardUserDefaults] integerForKey:@"currentAlbumIndex"]; [self showDataForAlbumAtIndex:currentAlbumIndex]; } |
saveCurrentState 儲存當前的專輯索引到NSUserDefaults,NSUserDefaults是IOS提供的儲存應用設定資訊和資料的地方。
loadPreviousState 載入之前儲存的索引。這裡其實不是備忘錄模式完整的實現,但是你已經瞭解到它了。
現在,在ViewController.m的viewDidLoad方法中,在scroller初始化之前增加下面的程式碼:
1 |
[self loadPreviousState]; |
它將在應用啟動的時候載入原先儲存的狀態。但是在什麼時候來儲存應用的狀態呢?你將使用通知來實現它。當應用進入後臺的時候,IOS會傳送UIApplicationDidEnterBackgroundNotification通知,你可以使用這個通知去儲存狀態,這是不是很方便?
在viewDidLoad中增加下面的程式碼:
1 |
[[NSNotificationCenterdefaultCenter] addObserver:self selector:@selector(saveCurrentState) name:UIApplicationDidEnterBackgroundNotification object:nil]; |
現在,當應用進入後臺的時候,ViewController將通過saveCurrentState方法自動儲存當前的狀態。
現在增加下面的程式碼:
1 2 3 4 5 6 7 |
- (void)dealloc { [[NSNotificationCenterdefaultCenter] removeObserver:self]; } |
這將確保當ViewController被銷燬的時候移除觀察者。
構建和執行你的應用,導航到一個專輯,然後通過Command+Shift+H(模擬器的情況下)將app傳送到後臺,然後關閉app。再一次開啟app,檢查原先選擇的專輯是不是被顯示在中間:
看起來專輯資料是正確的,但是中間的檢視卻沒有顯示正確的專輯。出了什麼情況?這是可選方法initialViewIndexForHorizontalScroller的目的所在。因為這個方法沒有在委託中實現,這樣的話初始化檢視總是第一個檢視。
為了修正這個問題,在ViewController.m中增加下面的程式碼:
1 2 3 4 5 6 7 |
- (NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller *)scroller { return currentAlbumIndex; } |
現在HorizontalScroller的第一個檢視終於設定為了currentAlbumIndex指定的檢視。這使得app在下次使用的時候還保留了上次使用的狀態。
再一次執行你的app,和之前一樣滾動專輯,停止應用,重啟,確保上面的問題已經修復了:
![](https://i.iter01.com/images/91d17a02ad26db7e200d359c8276365d361a92304464789f3590122af34de3bd.png)
如果你檢視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行為如下所示:
1 |
@interfaceAlbum : NSObject |
在Album.m中增加如下的兩個方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
- (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.year forKey:@"year"]; [aCoder encodeObject:self.title forKey:@"album"]; [aCoder encodeObject:self.artist forKey:@"artist"]; [aCoder encodeObject:self.coverUrl forKey:@"cover_url"]; [aCoder encodeObject:self.genre forKey:@"genre"]; } - (id)initWithCoder:(NSCoder *)aDecoder { self = [super init]; if (self) { _year = [aDecoder decodeObjectForKey:@"year"]; _title = [aDecoder decodeObjectForKey:@"album"]; _artist = [aDecoder decodeObjectForKey:@"artist"]; _coverUrl = [aDecoder decodeObjectForKey:@"cover_url"]; _genre = [aDecoder decodeObjectForKey:@"genre"]; } return self; } |
你可以在歸檔一個類的例項物件的時候呼叫encodeWithCoder:,相反的當你要從歸檔中重建Album例項的時候,你可以呼叫initWithCoder:,這樣做是不是很簡單,但是它是一種強大的機制哦。
在PersistencyManager.h中,增加下面的簽名(方法原型):
1 |
- (void)saveAlbums; |
這個正是儲存專輯的方法。
現在在PersistencyManager.m中,增加方法的實現:
1 2 3 4 5 6 7 8 9 10 11 |
- (void)saveAlbums { NSString *filename = [NSHomeDirectory() stringByAppendingString:@"/Documents/albums.bin"]; NSData *data = [NSKeyedArchiverarchivedDataWithRootObject:albums]; [data writeToFile:filename atomically:YES]; } |
NSKeyedArchiver歸檔專輯資料到albums.bin檔案中。
當你在歸檔一個物件的時候,歸檔器會遞迴的歸檔物件包含的子物件以及子物件的子物件等等。在本例中,歸檔開始自一個名為albums的陣列,因為NSArry和Album兩者都支援NSCoding協議,因此陣列中每個物件都會被歸檔.
現在用下面的程式碼取代PersistencyManager.m中的init方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
- (id)init { self = [super init]; if (self) { NSData *data = [NSDatadataWithContentsOfFile:[NSHomeDirectory() stringByAppendingString:@"/Documents/albums.bin"]]; albums = [NSKeyedUnarchiverunarchiveObjectWithData:data]; if (albums == nil) { 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"]]]; [self saveAlbums]; } } return self; } |
新的程式碼中,如果專輯資料存在,NSKeyedUnarchiver會從檔案中載入專輯資料,如果專輯資料不存在,它會建立專輯資料並立即儲存它以便下次啟動的時候使用。
你想在每次app進入後臺的時候都儲存專輯資料。現在這可能看起來不是很必要,但是如果過會你想增加一個修改專輯資料的選項呢?那時候你就想確保所有的改變都會被儲存。
在Library.h中增加下面的程式碼:
1 |
- (void)saveAlbums; |
因為主應用通過LibraryAPI訪問所有的服務,這樣就要求PersistencyManager知道它負責儲存專輯資料。
現在在LibraryAPI.m實現中增加方法實現:
1 2 3 4 5 6 7 |
- (void)saveAlbums { [persistencyManager saveAlbums]; } |
這個方法將呼叫LibraryAPI儲存資料的請求委託給PersistencyManager處理。在ViewController.m中saveCurrentState方法末尾,增加如下的程式碼:
1 |
[[LibraryAPI sharedInstance] saveAlbums]; |
無論何時ViewController儲存應用狀態的時候,上面的程式碼使用LibraryAPI觸發專輯資料的儲存。
構建你的應用,檢查每個資源是否被正確編譯。
不幸的是,沒有一個簡單的方式去檢查資料持久化的正確性。你可以通過Finder在應用的Documents目錄檢視到專輯資料檔案已經被建立,但是為了能看到任何其它的變化,你還需要增加改變專輯資料的功能。
但是並不僅僅是改變資料,如果你需要刪除不想要的專輯資料呢?另外,是不是可以很漂亮的來增加一個撤銷刪除的功能呢?
這就到了我們討論下個設計模式(命令模式)的機會了。
命令模式
命令模式將一個請求封裝為一個物件。封裝以後的請求會比原生的請求更加靈活,因為這些封裝後的請求可以在多個物件之間傳遞,儲存以便以後使用,還可以動態的修改,或者放進一個佇列中。蘋果通過Target-Action機制和Invocation實現命令模式。
你可以通過蘋果的官方線上文件閱讀更多關於Target-Action的內容,至於Invocation,它採用了NSInvocation類,這個類包含了一個目標物件,方法選擇器,以及一些引數。這個物件可以動態的修改並且可以按需執行。實踐中它是一個命令模式很好的例子。它解耦了傳送物件和接受物件,並且可以儲存一個或者多個請求。
如何使用命令模式
在你深入瞭解invocation之前,你需要首先來設定一個支援撤銷操作的大體骨架。所以你需要定義一個UIToolBar和用作撤銷堆疊的NSMutableArray。
在ViewController.m的擴充套件中,在你定義其它變數的地方定義如下的變數:
1 2 3 4 5 |
UIToolbar *toolbar; // We will use this array as a stack to push and pop operation for the undo option NSMutableArray *undoStack; |
這裡我們建立了包含新增按鈕的工具欄,同時還建立了一個用作命令儲存佇列的陣列。
在viewDidLoad方法的第二個註釋之前,增加下面的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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大小的程式碼:
1 2 3 4 5 6 7 8 9 |
- (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中增加三個方法來管理專輯:增加,刪除,撤銷。
第一個方法是增加一個新的專輯:
1 2 3 4 5 6 7 8 9 10 11 |
- (void)addAlbum:(Album*)album atIndex:(int)index { [[LibraryAPI sharedInstance] addAlbum:album atIndex:index]; currentAlbumIndex = index; [self reloadScroller]; } |
在這裡你增加專輯,並設定當前專輯索引,然後重新載入滾動檢視。
接下來是刪除方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
- (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:&currentAlbumIndex 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.
最後,增加下面的撤銷方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
- (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模式,委託模式,協議,門面模式,觀察者模式,備忘錄模式以及命令模式。
你最終的程式碼是鬆耦合,可複用以及可讀的。如果另外一個開發者閱讀你的程式碼,他們會馬上理解程式碼邏輯以及每個類都做了什麼。
我們並不是說要在你寫的每句程式碼中使用設計模式。相反,我們要清楚的意識到可以用設計模式解決一些特定的問題,特別是在設計之初。他們會讓作為開發者的生涯更加輕鬆,同時你的程式碼也將變的更加漂亮。