前言:
每個開發心中都有一個架構的夢,雖然不能像大佬們一樣直接直接給出系統級的架構,但是我們在日常的編碼過程中,也可以慢慢積累一些自己的架構的見解,慢慢提高~
因為在學校自己一個人在寫整個App,加之需求也不明確,時常需求變更(在學校的組織寫專案的通病了),所以編寫過程真的是越寫越糟心,所以,不得已對已經開發的一小部分做了重構,以下是本小白在重構過程中總結的一些見解(不得不說,本科階段講的那些設計模式什麼的,是真的很有用,只是當時根本理解不了這些精髓,等到重構時才發現都可以套原型)。
架構的幾個方向:
- view層的組織和呼叫設計
- 本地持久化
- 網路層設計(網路層會說的比較籠統)
- 動態部署(Web App/Hybrid App/React-Native,這塊也沒咋說,因為目前沒有涉獵)
架構設計的步驟:
- 問題分類,分模組(這個很重要)
- 搞清楚各個模組之間的依賴關係,設計好一套模組的交流規範並設計模組
- 為架構保持一定量的超前性(血的教訓)
- 先實現基礎模組,再組合基礎模組形成初期架構
主要就是:自頂向下設計,自底向上實現,先量化資料再優化
敏捷原則:對擴充套件開放-對修改封閉
什麼樣app的架構叫好架構?
- 程式碼整齊,分類明確:每個模組只負責模組內的事務
- 不用文件,或很少文件,就能讓業務方上手
- 思路和方法要統一,儘量不要多元
- 沒有橫向依賴,萬不得已不出現跨層訪問:(大概就是拓撲排序的原理)
1,當一個需求需要多業務合作開發時,如果直接依賴,會導致某些依賴層上端的業務工程師在前期空轉,依賴層下端的工程師任務繁重,導致延期(就會擠壓QA老鐵的時間,然後再找PM撕*。。。。。別問我是怎麼知道的) 2,當要開闢一個新業務時,如果已有各業務間直接依賴,新業務又依賴某個舊業務,就導致新業務的開發環境搭建困難,因為必須要把所有相關業務都塞入開發環境,新業務才能進行開發。 3,當某一個被其他業務依賴的頁面有所修改時,比如改名,涉及到的修改面就會特別大。影響的是造成任務量和維護成本都上升的結果。
對應解決方法:依賴下沉,假如A、B、C三個模組存在橫向依賴,這樣的話引入新節點D,對A、B、C實現依賴下沉,當A呼叫B的某個頁面的時候,將請求交給Mediater,然後由Mediater通過某種手段獲取到B業務頁面的例項,交還給A就行了。
- 對業務方該限制的地方有限制,該靈活的地方要給業務方創造靈活- - 實現的條件
- 易測試,易擴充
- 保持一定量的超前性
- 介面少,介面引數少
- 高效能
關於不跨層訪問說下:
跨層訪問是指資料流向了跟自己沒有對接關係的模組。有的時候跨層訪問是不可避免的,比如網路底層裡面訊號從2G變成了3G變成了4G,這是有可能需要跨層通知到View的。但這種情況不多,一旦出現就要想盡一切辦法在本層搞定或者交給上層或者下層搞定,儘量不要出現跨層的情況。跨層訪問同樣也會增加耦合度,當某一層需要整體替換的時候,牽涉面就會很大。
易測試性:
儘可能減少依賴關係,便於mock。另外,如果是高度模組化的架構,擴充起來將會是一件非常容易的事情。
架構分層:
梗概:
經常有‘三層架構MVC’這樣的說法,以至於很多人就會認為三層架構就是MVC,MVC就是三層架構。其實不是的。三層架構裡面其實沒有Controller的概念,而且三層架構描述的側重點是模組之間的邏輯關係。MVC有Controller的概念,它描述的側重點在於資料流動方向。
三層架構
所有的模組角色只會有三種:
- 資料管理者
- 資料加工者
- 資料展示者 意思也就是,籠統說來,軟體只會有三層,每一層扮演一個角色。其他的第四層第五層,一般都是這三層裡面的其中之一分出來的,最後都能歸納進這三層的某一層中去,所以用三層架構來描述就比較普遍。
View層設計:
View層的架構一旦實現或定型,在App發版後可修改的餘地就已經非常之小了。因為它跟業務關聯最為緊密,做決策時要拿捏好尺度。
View層架構是影響業務方迭代週期的因素之一:
因為View層架構是最貼近業務的底層架構
view層架構知識點主要包括:
- 良好的編碼/實現規範
- 合適的設計模式(MVC、MVCS、MVVM、VIPER)
- 根據業務情況針對ViewController做好拆分(瘦身),提供一些小工具方便開發
view層程式碼規範:(第4點不一定)
1 viewDidload:做addSubview的事情
2 viewWillAppear:嚴格來說這裡通常不做檢視位置的修改,而用來更新Form資料。原因見下一點:
3 佈局(新增約束)時機:首先,Autolayout發生在viewWillAppear之後,所以我一般選擇放到 - viewWilllayoutSubview或者- viewDidLayoutSubviews中。因為viewWillAppear在每次頁面即將顯示都會呼叫,viewWillLayoutSubviews雖然在lifeCycle裡呼叫順序在viewWillAppear之後,但是隻有在頁面元素需要調整時才會呼叫,避免了Constraints的重複新增
4 viewDidAppear裡面做新增監聽之類的事情
5 屬性的初始化,則交給getter(懶載入)去做,這也就要求:所有的屬性都使用getter和setter,並且getter,setter方法放到.m檔案的最後寫,這樣可以提高開發效率。另外一種思路是將所有屬性都放到 setUpPropertyConfig方法中,然後setUpPropertyConfig放到viewDidLoad中,兩者均可,沒有什麼區別。
#pragma mark - life cycle
- (void)viewDidLoad
{
[super viewDidLoad];
[self.view addSubview:self.label];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.label.frame = CGRectMake(1, 2, 3, 4);
}
#pragma mark - getters and setters
- (UILabel *)label
{
if (_label == nil) {
_label = [[UILabel alloc] init];
_label.text = @"1234";
_label.font = [UIFont systemFontOfSize:12];
... ...
}
return _label;
}
@end
複製程式碼
6 每個delegate方法寫到一塊區域裡面去,使用#pragma mark - UITableViewDelegate進行分割(這個是豬場搬磚看到黃師傅的程式碼學到的。。。之前一直沒有這個意識,感謝)
7 VC裡面儘量不要有私有方法
不是delegate方法的,不是event response(相應使用者操作)方法的,不是life cycle(view didload這些方法)方法的,就是private method了,這些private methods一般是用於日期換算、圖片裁剪啥的這種輔助的小功能。這些小功能一般都是單獨抽出來寫成模組的tool類或者系統Util類。
8 關於View的佈局方法:
無外乎就是 storyboard+xib+程式碼擼的組合
借鑑一下@唐巧的分析指令碼: 傳送門: https://gist.github.com/tangqiaoboy/b149d03cfd0cd0c2f7a1 可見這個本來就是有爭議的。
其實,實現簡單的東西,用Code一樣簡單,實現複雜的東西,Code比StoryBoard更簡單。
所以本渣一般採用: 1,複雜頁面主體手擼程式碼(用的是masonry) 2,簡單、靜態的Cell以及封裝的一些自定義小控制元件使用xib。
還有幾點本人目前能力不夠,不能夠給出正確的見解:
A.是否需要讓業務方統一派生ViewController。 B.
#MVC MVC架構基礎請看象印筆記。
各個模組需要負責的事物:
- M應該做的事: 1,給ViewController提供資料(網路獲取API+本地的快取獲取API) 2,給ViewController儲存資料提供介面(本地快取的儲存/更新新API) 3,提供經過抽象的業務基本元件(一般我會抽一個Manager(包括1,2)出來專門負責),供Controller排程
- C應該做的事:(其中VC自帶的View相當於C所管理的View的一個容器) 1,管理View Container的生命週期 2,負責生成所有的View例項,並放入View Container(就是C.view) 3,監聽來自View與業務有關的事件,通過與Model的合作,來完成對應事件的業務。
- V應該做的事: 1,響應與業務無關的事件,並因此引發動畫效果,點選反饋(如果合適的話,儘量還是放在View去做)等。 2,介面元素表達
下面是MVCS、MVVM兩個MVC設計模式的變種
可能還有一些別的設計模式,但是本人能力有限啊啊啊,所以只先介紹這倆
先說下:胖Model&瘦Model:
-
胖Model:(MVVM的基本思想) 包含了部分弱業務邏輯。胖Model要達到的目的是,Controller從胖Model這裡拿到資料之後,不用額外做操作或者只要做非常少的操作,就能夠將資料直接應用在View上。
-
瘦Model:(MVCS的基本思想) 瘦Model只負責業務資料的表達,所有業務無論強弱一律扔到Controller。瘦Model要達到的目的是,盡一切可能去編寫細粒度Model,然後配套各種helper類或方法來對弱業務做抽象,強業務依舊交給Controller。
前言:首先不管MVVM也好,MVCS也好,他們的共識都是Controller會隨著軟體的成長,變很大很難維護很難測試。只不過兩種架構思路的前提不同,MVCS是認為Controller做了一部分Model的事情,要把它拆出來變成Store,MVVM是認為Controller做了太多資料加工的事情,所以MVVM把資料加工的任務從Controller中解放了出來,使得Controller只需要專注於資料調配的工作,ViewModel則去負責資料加工並通過通知機制讓View響應ViewModel的改變。
MVCS:
從概念上來說,它拆分的部分是Model部分,拆出來一個Store。這個Store專門負責資料存取。但從實際操作的角度上講,它拆開的是Controller。
MVCS使用的前提是,它假設了你是瘦Model,同時資料的儲存和處理都在Controller去做。所以對應到MVCS,它在一開始就是拆分的Controller。因為Controller做了資料儲存的事情,就會變得非常龐大,那麼就把Controller專門負責存取資料的那部分抽離出來,交給另一個物件去做,這個物件就是Store。這麼調整之後,整個結構也就變成了真正意義上的MVCS。
MVVM:
ReactiveCocoa成熟之後,ViewModel和View的訊號機制在iOS下終於有了一個相對優雅的實現(MVC中View和Model是不能直接通訊的,需要Controller做一個協調者的身份)。MVVM本質上也是從MVC中派生出來的思想,MVVM著重想要解決的問題是儘可能地減少Controller的任務。
MVVM是基於胖Model的架構思路建立的,然後在胖Model中拆出兩部分:Model和ViewModel。關於這個觀點我要做一個額外解釋:胖Model做的事情是先為Controller減負,然後由於Model變胖,再在此基礎上拆出ViewModel,跟業界普遍認知的MVVM本質上是為Controller減負這個說法並不矛盾,因為胖Model做的事情也是為Controller減負。
另外,MVVM把資料加工的任務從Controller中解放出來,跟MVVM拆分的是胖Model也不矛盾。要做到解放Controller,首先你得有個胖Model,然後再把這個胖Model拆成Model和ViewModel。
在MVVM中,Controller扮演的角色:
-
MVVM的名稱裡沒有C造成了MVVM不需要Controller的錯覺,其實MVVM是一定需要Controller的參與的,雖然MVVM在一定程度上弱化了Controller的存在感,並且給Controller做了減負瘦身(這也是MVVM的主要目的)。其實MVVM應該是Model-ViewModel-Controller-View這樣的架構,並不是不需要Controller。
-
Controller夾在View和ViewModel之間做事情: 1,最主要事情就是將View和ViewModel進行繫結。在邏輯上,Controller知道應當展示哪個View,Controller也知道應當使用哪個ViewModel,然而View和ViewModel它們之間是互相不知道的,所以Controller就負責控制他們的繫結關係。 2,常規的UI邏輯處理
一句話總結:
在MVC的基礎上,把Controller拆出一個ViewModel專門負責資料處理的事情,就是MVVM。
- 關於MVVM是否必須要使用ReactiveCocoa? 當然不是,只是因為蘋果本身並沒有提供一個比較適合這種情況的繫結方法。雖然有KVO,Notification,block,delegate用來做資料通訊,從而來實現繫結,但都不如ReactiveCocoa提供的RACSignal來的優雅簡單,如果不用ReactiveCocoa,繫結關係可能就做不到那麼鬆散那麼好,但並不影響它還是MVVM。
再深層次的我就不能很好解釋了:如果需要了解,可以細看: https://www.teehanlax.com/blog/model-view-viewmodel-for-ios/
關於專案究竟使用哪種設計模式:
MVC其實是非常高Level的抽象,意思也就是,在MVC體系下還可以再衍生無數的架構方式,但萬變不離其宗的是,它一定符合MVC的規範。 所以我的建議是:
- 只要不是Controller的核心邏輯,都可以考慮拆出去,然後在架構的時候作為一個獨立模組去定義,以及設計實現,但是不要為了拆分而拆分。
- 拆分出的模組儘量提高複用性,降低強業務相關性。
- 拆分的粒度要儘可能大一點,封裝得要透明一些。
網路層:
首先先說下跨層訪問:
關於跨層資料流通:
當存在A<-B<-C這樣的結構時。當C有事件,通過某種方式告知B,然後B執行相應的邏輯。一旦告知方式不合理,讓A有了跨層知道C的事件的可能,你 就很難保證A層業務工程師在將來不會對這個細節作處理。一旦業務工程師在A層產生處理操作,有可能是補充邏輯,也有可能是執行業務,那麼這個細節的相關處理程式碼就會有一部分散落在A層。然而前者是不應該散落在A層的,後者有可能是需求。另外,因為B層是對A層抽象的,執行補充邏輯的時候,有可能和B層針對這個事件的處理邏輯產生衝突,這是我們很不希望看到的。 但有時跨層資料流通也是不可避免的: 比如,訊號從2G變成3G變成4G變成Wi-Fi,這個就是需要跨層資料交流的。
再考慮下文:
資料以什麼方式交付給業務層:
大多數App在網路層所採用的方案主要集中於這三種:Delegate,Notification,Block。 一般都是組合使用,這裡我只能說下個人的選擇,畢竟個人涉獵有限,所以只能結合自身所採用的模式說下好處: 之前在豬場某部門實習,網路層採用的是block為主進行資料交付。
當回撥之後要做的任務在每次回撥時都是一致的情況下,選擇delegate,在回撥之後要做的任務在每次回撥時無法保證一致,選擇block。
- Delegate為主,Notification為輔(蘋果的原生網路請求就是delegate。。但是AFN採用block做回撥)。 所以我一般都是採用AFN做網路(畢竟方便省事),所以一般採用如下形式進行請求:
//請求發起採用AFN的Block,回撥使用delegate方式,這樣在業務方這邊回撥函式就能夠比較統一,便於維護。
[AFNetworkingAPI callApiWithParam:self.param successed:^(Response *response){
if ([self.delegate respondsToSelector:@selector(successWithResponse:)]) {
[self.delegate successedWithResponse:response];
}
} failed:^(Request *request, NSError *error){
if ([self.delegate respondsToSelector:@selector(failedWithResponse:)]) {
[self failedWithRequest:request error:error];
}
}];
複製程式碼
原因:使用Delegate能夠很好地避免跨層訪問,同時限制了響應程式碼的形式,相比Notification而言有更好的可維護性,而Notification則解決了跨層資料流通的相應需求。
但是使用Notification一定要約定好命名規範,不然會引發後期維護的災難。
集約型API呼叫方式和離散型API呼叫方式的選擇:
-
集約型API呼叫其實就是所有API的呼叫只有一個類,然後這個類接收API名字,API引數,以及回撥著陸點(block,或者delegate等各種模式的著陸點)作為引數。然後執行類似startRequest這樣的方法,它就會去根據這些引數起飛去呼叫API了,然後獲得API資料之後再根據指定的著陸點去著陸。
-
離散型API呼叫是這樣的,一個API對應於一個APIManager,然後這個APIManager只需要提供引數就能起飛,API名字、著陸方式都已經整合入APIManager中。
交付什麼樣的資料給業務層?【(Adapator介面卡)其實這就屬於MVVM的VM層做的事了,真的是每一處都是設計模式。。。。】
理想情況是希望API的資料下發之後就能夠不需要進一步處理直接被View所展示。首先要說的是,這種情況非常少。另外,這種做法使得View和API聯絡緊密,也是不應該發生的。 舉個栗子:
先定義一個protocol:
@protocol AdapatorProtocol <NSObject>
- (NSDictionary)reformDataWithManager:(APIManager *)manager;
@end
在Controller裡是這樣:
@property (nonatomic, strong) id< AdapatorProtocol > XAdapator;
#pragma mark - APIManagerDelegate
- (void)apiManagerDidSuccess:(APIManager *)manager
{
NSDictionary *XData = [manager fetchDataWithReformer:self. XAdapator];
[self.XView configWithData:XData];
}
在APIManager裡面,fetchDataWithReformer是這樣:
- (NSDictionary)fetchDataWithReformer:(id< AdapatorProtocol >)adapator{
if (adapator == nil) {
return self.rawData;
} else {
//adapaor進行處理資料
return [adapator reformDataWithManager:self];
}
}
複製程式碼
使用介面卡模式帶來的好處:
- 減輕Controller壓力,降低了程式碼複雜度,同時提高了靈活性,任何時候切換reformer而不必切換業務邏輯就可以應對不同View對資料的需要
- 在處理單View對多API,以及在單API對多View的情況時,reformer提供了非常優雅的手段來響應這種需求,隔離了轉化邏輯和主體業務邏輯,避免了維護災難。
- 轉化邏輯集中,且將轉化次數轉為只有一次。使用資料原型的轉化邏輯至少有兩次,第一次是把JSON對映成對應的原型,第二次是把原型轉變成能被View處理的資料。reformer一步到位。另外,轉化邏輯在Adapator裡面,將來如果API資料有變,就只要去找到對應Adapator然後改掉就好了,方便後期維護
- 業務資料和業務有了適當的隔離。這麼做的話,將來如果業務邏輯有修改,換一個Adapator就好了。如果其他業務也有相同的資料轉化邏輯,其他業務直接拿這個Adapator就可以用了,不用重寫。另外,如果controller有修改(比如UI互動方式改變),可以放心換controller,完全不用擔心業務資料的處理。
網路層優化:
1,使用快取(本地混存+URL快取)進行請求次數的減少,能不發請求的就儘量不發請求,必須要發請求時,能合併請求的就儘量合併請求。 2,需要上傳的日誌,積滿一定數量再上傳 3,一般專案都有多個伺服器,應用啟動的時候獲得本地列表中所有IP的ping值,然後將Dev_URL中的HOST修改為我們找到的最快的IP。另外,這個本地IP列表也會需要通過一個API來維護,一般是每天第一次啟動的時候ping一下,然後更新到本地。 4,比較大的資料壓縮再上傳。
資料持久化:
首先有非常多的方案可供選擇: 1、NSUserDefault: 一般是小規模資料,弱業務相關資料,NSUserDefault變大會影響App啟動的時間,敏感資料不要放NSUserDefault,雖然NSUserDefault的存取真的是很方便。 2、KeyChain:Keychain是蘋果提供的帶有可逆加密的儲存機制,普遍用在各種存密碼的需求上。另外,由於App解除安裝只要系統不重灌,Keychain中的資料依舊能夠得到保留,以及可被iCloud同步的特性,大家都會在這裡儲存使用者唯一標識串。所以有需要加密、需要存iCloud的敏感小資料,一般都會放在Keychain。 3、File:主要包括:
1,Plist 2,archive(歸檔):只適合存一些不經常使用的,大量的資料,讀取之後直接換變為物件/直接將物件儲存(需要支援) 3,Stream(直接存檔案):適合資料較大且經常使用,但是檔案一般都是遍歷才能拿到,所以建議為檔案建立資料庫索引
4、基於資料庫的無數子方案(YYCache,FMDB,sqlite,CoreData[本人不喜歡用])。資料庫中的資料應該都是強業務相關的,並且不能是很大的檔案,比如一個大圖片或者視訊之類的,一般是存檔案,然後資料中存放檔案路徑這樣配合,而推薦直接使用YYCache,一站式服務。
因此,當有需要持久化的需求的時候,我們首先考慮的是應該採用什麼手段去進行持久化。
資料庫記得做執行緒處理(原理跟iOS的屬性的執行緒安全一樣) 比如SQLite庫就推薦使用Serialized(預設):序列佇列訪問,雖然會慢一丟丟,但是方便易維護。
持久層有專門負責對接View層模組或業務的DataCenter,它們之間通過Record來進行互動。DataCenter向上層提供業務友好的介面,這一般都是強業務:比如根據使用者篩選條件返回符合要求的資料等。然後DataCenter在這個介面裡面排程各個Table,做一系列的業務邏輯,最終生成record物件,交付給View層業務。
DataCenter為了要完成View層交付的任務,會涉及資料組裝和跨表的資料操作。資料組裝因為View層要求的不同而不同,因此是強業務。跨表資料操作本質上就是各單表資料操作的組合,DataCenter負責排程這些單表資料操作從而獲得想要的基礎資料用於組裝。那麼,這時候單表的資料操作就屬於弱業務,這些弱業務就由Table對映物件來完成。 Table物件通過QueryCommand來生成相應的SQL語句,並交付給資料庫引擎去查詢獲得資料,然後交付給DataCenter。
差不多就這些,等重構完再補充~