文章出處



文章列表
由 @krq_tiger(http://weibo.com/xmuzyq)翻譯,如果你發現有什么錯誤,請與我聯系謝謝。
備忘錄(Memento)模式
備忘錄模式快照對象的內部狀態并將其保存到外部。換句話說,它將狀態保存到某處,過會你可以不破壞封裝的情況下恢復對象的狀態,也就是說原來對象中的私有數據仍然是私有的。
如何使用備忘錄模式
在ViewController.m中增加下面的方法:
- - (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初始化之前增加下面的代碼:
- [self loadPreviousState];
它將在應用啟動的時候加載原先保存的狀態。但是在什么時候來保存應用的狀態呢?你將使用通知來實現它。當應用進入后臺的時候,IOS會發送UIApplicationDidEnterBackgroundNotification通知,你可以使用這個通知去保存狀態,這是不是很方便?
在viewDidLoad中增加下面的代碼:
- [[NSNotificationCenterdefaultCenter] addObserver:self selector:@selector(saveCurrentState) name:UIApplicationDidEnterBackgroundNotification object:nil];
現在,當應用進入后臺的時候,ViewController將通過saveCurrentState方法自動保存當前的狀態。
現在增加下面的代碼:
- - (void)dealloc
- {
- [[NSNotificationCenterdefaultCenter] removeObserver:self];
- }
這將確保當ViewController被銷毀的時候移除觀察者。
構建和運行你的應用,導航到一個專輯,然后通過Command+Shift+H(模擬器的情況下)將app發送到后臺,然后關閉app。再一次打開app,檢查原先選擇的專輯是不是被顯示在中間:

看起來專輯數據是正確的,但是中間的視圖卻沒有顯示正確的專輯。出了什么情況?這是可選方法initialViewIndexForHorizontalScroller的目的所在。因為這個方法沒有在委托中實現,這樣的話初始化視圖總是第一個視圖。
為了修正這個問題,在ViewController.m中增加下面的代碼:
- - (NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller *)scroller
- {
- return currentAlbumIndex;
- }
現在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行為如下所示:
- @interfaceAlbum : NSObject<NSCoding>
在Album.m中增加如下的兩個方法:
- - (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中,增加下面的簽名(方法原型):
- - (void)saveAlbums;
這個正是保存專輯的方法。
現在在PersistencyManager.m中,增加方法的實現:
- - (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方法:
- - (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中增加下面的代碼:
- - (void)saveAlbums;
因為主應用通過LibraryAPI訪問所有的服務,這樣就要求PersistencyManager知道它負責保存專輯數據。
現在在LibraryAPI.m實現中增加方法實現:
- - (void)saveAlbums
- {
- [persistencyManager saveAlbums];
- }
這個方法將調用LibraryAPI保存數據的請求委托給PersistencyManager處理。在ViewController.m中saveCurrentState方法末尾,增加如下的代碼:
- [[LibraryAPI sharedInstance] saveAlbums];
無論何時ViewController保存應用狀態的時候,上面的代碼使用LibraryAPI觸發專輯數據的保存。
構建你的應用,檢查每個資源是否被正確編譯。
不幸的是,沒有一個簡單的方式去檢查數據持久化的正確性。你可以通過Finder在應用的Documents目錄查看到專輯數據文件已經被創建,但是為了能看到任何其它的變化,你還需要增加改變專輯數據的功能。
但是并不僅僅是改變數據,如果你需要刪除不想要的專輯數據呢?另外,是不是可以很漂亮的來增加一個撤銷刪除的功能呢?
這就到了我們討論下個設計模式(命令模式)的機會了。
命令模式
命令模式將一個請求封裝為一個對象。封裝以后的請求會比原生的請求更加靈活,因為這些封裝后的請求可以在多個對象之間傳遞,存儲以便以后使用,還可以動態的修改,或者放進一個隊列中。蘋果通過Target-Action機制和Invocation實現命令模式。
你可以通過蘋果的官方在線文檔閱讀更多關于Target-Action的內容,至于Invocation,它采用了NSInvocation類,這個類包含了一個目標對象,方法選擇器,以及一些參數。這個對象可以動態的修改并且可以按需執行。實踐中它是一個命令模式很好的例子。它解耦了發送對象和接受對象,并且可以保存一個或者多個請求。
如何使用命令模式
在你深入了解invocation之前,你需要首先來設置一個支持撤銷操作的大體骨架。所以你需要定義一個UIToolBar和用作撤銷堆棧的NSMutableArray。
在ViewController.m的擴展中,在你定義其它變量的地方定義如下的變量:
- UIToolbar *toolbar;
- // We will use this array as a stack to push and pop operation for the undo option
- NSMutableArray *undoStack;
這里我們創建了包含新增按鈕的工具欄,同時還創建了一個用作命令存儲隊列的數組。
在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模式,委托模式,協議,門面模式,觀察者模式,備忘錄模式以及命令模式。
你最終的代碼是松耦合,可復用以及可讀的。如果另外一個開發者閱讀你的代碼,他們會馬上理解代碼邏輯以及每個類都做了什么。
我們并不是說要在你寫的每句代碼中使用設計模式。相反,我們要清楚的意識到可以用設計模式解決一些特定的問題,特別是在設計之初。他們會讓作為開發者的生涯更加輕松,同時你的代碼也將變的更加漂亮。
文章列表
全站熱搜