iOS APP 架構漫談
最近看了一些有關server的東西,一些很簡單的東西,不外乎是一些文檔規范,另外結合最近看的wwdc的一些video,覺得對軟件架構(software architecture)認識又清楚了一些,這里記錄下來。
software architecture 聽上去是一個很大的概念,實際上也包括很多東西,里面的爭議也很多。在我看來軟件架構最好放在小的場景中理解。
問題1
我們有2個頁面。
- 頁面A:主頁面
- 頁面B:詳情頁面 </ul>
2個頁面分別顯示一個數字,這個數字應該相同。詳情會修改這個數字,這里我們發現,詳情頁面和主頁面數字不一樣。
數據不一致
問題1 解決方法A
這里首先的感覺就是,詳情頁面返回,主頁面數據沒有刷新,導致數據不一致。 那么Fix這個Bug的方法,就是在主頁面出現的時候刷新界面
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.displayLabel.text = [[CUDataDAO selectData].data stringValue];
}</code></pre>
現在來看,還不錯。但是,我們調用selectData的次數則變得非常非常多。數據不是經常變化的。
問題1 解決方法B
我們發現既然數據的改變是在頁面B進行的,那么頁面B修改這個數據的時候,應該把數據變化”通知”給頁面A,那么我們寫了一個Delegate
<code>@protocol CUDetailViewControllerDelegate - (void)detailVC:(CUDetailViewController )vc dataChanged:(NSNumber )data;
@end</nsobject></code></pre>
在頁面B修改數據之后,通過delegate 通知給頁面A。
- (IBAction)changeButtonClicked:(id)sender {
int value = arc4random() % 100;
[CUDataDAO setData:value];
self.displayLabel.text = [@(value) stringValue];
if ([self.delegate respondsToSelector:@selector(detailVC:dataChanged:)]) {
[self.delegate detailVC:self dataChanged:@(value)];
}
}</code></pre>
到此場景1得到了不錯的解決。
問題2
這時我們增加了另一個頁面C。這個場景會稍微抽象一點,我們定義了3個數據
- 頁面A的數據dataA
- 頁面B的數據dataB
- 頁面C的數據dataC
</ul>
問題1中 dataA = dataB。在問題2中dataA = dataB + dataC;
問題2 解決方法C
也就是說頁面C的修改,也會影響頁面A的數據,那么我們是不是也要寫一個XXXXDelegate呢?
這時我們的大腦嗅出了一些不好的味道,如果再來個什么dataD,dataE,我們要寫這么多的Delegate么?對于多對一”通知”這種味道,很自然的想到了不用Delegate,而是用NSNotification
來做。讓我們未雨綢繆一下,定義一個Notificaiton
NSString *const kCUDataChangedNotification = @"CUDataChangedNotification";
[[NSNotificationCenter defaultCenter] postNotificationName:kCUDataChangedNotification
object:nil
userInfo:nil];</code></pre>
那這個變化broadcast到listener,看上去是一個很贊的idea。
問題3
過了一段時間,我們發現問題2的方法有一個Bug,當界面停在頁面B的時候,切換到頁面C,修改數據,B中再返回時,數據和頁面A的數據不一致。
數據不一致
那也可以類比解決方法B,得到了下面的方法
解決方法D
既然A和B的數據不一致,而A的數據比B的新,那么保留一個B的指針,然后A變化的時候,更新B就好了。
- (void)handleDataChangedNotification {
[self updateLabel];
[self.vc updateLabel];
}
// In a storyboard-based application, you will often want to do a little preparation before navigation
(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:@"push"]) {
CUDetailViewController *vc = [segue destinationViewController];
if ([vc isKindOfClass:[CUDetailViewController class]]) {
self.vc = vc;
}
}
}</code></pre>
問題4
頁面C實在是太簡單了,這次我們希望在頁面C中顯示頁面A的數據。因為上次我們就產生了一個數據不一致的問題,這次我們注意到了,那么怎么修改呢?
解決方法E
在看了看整個APP各種通知之后,覺得挺麻煩,準備用一個取巧的方法。可以類比解決方法A。在頁面C出現的時候,刷新數據,至于什么性能問題,不管了,先fix bug。
- (void)viewWillAppear:(BOOL)animated {
[self updateLabel];
}
(void)updateLabel {
int dataB = [[CUDataDAO selectData].data intValue];
int dataC = [[CUDataDAO selectOtherData].data intValue];
self.dataLabel.text = [@(dataB + dataC) stringValue];
}</code></pre>
問題5
這時的數據需要不斷的變化,我們在CUDataDAO
加了一個timer 模擬數據變化,數據變化的原因可能是server push 一些數據。client 本地數據庫更新了數據,需要在頁面A、B、C中顯示。
頁面C的數據又不一致了。。。。
問題到底在哪里呢
走到這里,我們需要重新思考為什么這個問題會不斷的重復出現呢?software architecture
就是來解決這個問題的。但是在提出一個合理的方案之前,先思考一個概念。
我們把數據庫中的數據,顯示到屏幕上,或是傳遞給View時,這個過程其實是對data 做了一次copy。而且只要不是通過引用或是指針這些方式,通過值傳遞的方式都是對data做了一次copy。而這個copy的過程,非常類似Cache。

通常建立一個Cache會遇到2種問題。
- Cache情況A: 與original Data 數據不一致,沒有及時更新
- Cache情況B: 重復建立Cache
讓我們用這個思路來看我們的解決方案
解決方法A
這是一個非常典型的Cache情況B
。數據庫的數據并沒有變化,但我們卻多次重復計算cache
解決方法B
頁面之間的關系可以用下面來描述

這里我們隱隱能夠感覺到問題,A的數據變化依賴于2個地方。不急,再往后看
解決方法C

解決方法D

事情變得更糟了
解決方法E
和解決方法A類似,同樣的重復計算Cache問題。
實際上問題還會更糟
現在還是一個簡單的Model,如果project變得很大,那么就會變成這個樣子

每一個X
都可能是一個Bug。
我們似乎已經找到問題了
《Advanced iOS Application Architecture and Patterns》 中,把這個圖叫做information flow。我們的直覺會告訴我們,這個信息的傳遞,應該是自上而下的樹或是森林,而且最好是一個層次平衡結構,要清晰,每一個位置都有相對于的職責。那我們就需要制定一個規則。
在想這個規則之前,如果把上面的圖背后的數據忘記,我們感覺這很類似內存模型。當然內存模型會比較復雜。但是我們可以借鑒很多”內存管理中的規則”,比如誰創建,誰銷毀。同樣,在我們的information flow中,我們希望誰創建Cache,誰更新Cache變化
DAO的數據庫似乎很難做這件事情,我們引入了一個新的元素dataSource
(當然他本身又是DAO的一個Cache)。其中A、B、C3個都會顯示數據,那么他們應該在一個層級,其中B、C會修改數據,他們會把這個數據返回給dataSource
,而通過dataSource
來把這個變化通知到A、B、C。

這樣帶來的好處很明顯,我們再添加一個D,也不會對其他地方的數據產生任何影響,我們的Unit Test、Mock也更加好寫。
我們之前的思路錯在哪里呢?
從局部來看,我們之前的思路都沒有任何問題,但是整體來看卻把問題隱藏化。關鍵的問題是在于沒有找到Truth
,找到問題真正的地方。而找到真正的地方,需要我們在大腦中有一個清晰的information flow
或是data flow
。了解之間元素的相互關系,才能建立一個個的層。才能坐到真正的解耦,解耦并不是僅僅一個個的Manager
,更重要的是建立一套清晰的flow機制,或是消息機制,如果沒有一套flow,中間引入的各種各樣的方法,即便使用了各種設計模式,整個software 依然是深度耦合。
疑問
這個APP看上去交互非常復雜
上面的model,有些同學還可能覺得這是交互上面的問題,這個交互看上去非常的復雜,不是一個好設計。
我這里列舉一個實際的例子:
A頁面要創建動畫,動畫背后包括很多數據,這些數據會在B,C甚至更多的頁面,或是后臺被修改。動畫本身實際上體現在View,而這些view可能不僅僅在A中有,B,C可能也會有部分的View。
單例怎么樣
當然我們可以用單例的法子。單例是個魔鬼,被很多濫用,這個場景用單例,其實僅僅是把全局變量合理的封裝在了單例下,因為這份數據,并沒有任何理由要一定是一份copy。
recap
在了解這個概念后,再看一些server的架構,規則時,也會更容易理解這些層之間的關系。包括
- 為什么要規定那些層之間,不能相互調用,不能有靜態方法。
- 一個層之間的model,不能有重疊功能,不能連表查詢。
- 在哪個層才能調用另一個服務,而調用這個服務還必須要通過統一的接口
software architecture 涵蓋的東西非常多。這篇只是一個引子,介紹了設計之前的準備工作。但是在實際過程中,我們的模型可能要比我這里寫的還要復雜很多。下一篇會介紹一種策略用來處理更加復雜模型的情況。
最后附上一個完整功能的 demo code
參考
《Advanced iOS Application Architecture and Patterns》
</div>
來自:http://studentdeng.github.io/blog/2014/08/29/ios-architecture/