非死book model 庫 Remodel 觀感
Linus Torvalds 有句名言:”Bad programmers worry about the code. Good programmers worry about data structures and their relationships.”
雖然不知道自己算不算的上是 “Good programmer”,但我對數據的重要性是深有體會,之前也寫過幾篇與 model 相關的技術文章,最近看一些歷史問題代碼的時候,又想起一些和 model 相關的知識點,覺得可以結合 Remodel 這個 framework 再談一下 iOS 項目里 model 處理。
在開始之前,大家可以做個小測驗,回想下最近的一個項目里,大概有多少個 model 的定義存在,位于不同 module 的 model 之間又是如何轉換交互的。如果對業務熟悉,那么有多少 model 存在應該了如指掌,如果 App 的結構清晰,理清 model 的層次和流向就不會太難。
Remodel 是 非死book 去年開源的項目,主要解決兩個大方向的問題,一是 model 相關的大量重復代碼,二是降低 model 在架構上所附帶的代碼耦合。
第一個問題對有些項目來說可能都不是問題,有些工程師對于 model 的處理和一般 class 對象沒有太大的區別,無非是按需要增加 property 和與之相關的函數。model 與一般對象之間最大的差別在于 model 是信息的載體,其中又包含若干數據類型,而數據一旦存在于較長的時間跨度和較大的空間范圍,model 就有了狀態,狀態之間或有依賴,業務邏輯也大多是圍繞狀態展開,狀態維護出錯必然會導致各類奇怪 bug。
真要寫好一個 model 類,不可避免的會寫大量的重復代碼,對于 model 的約束越多,代碼就越可控,代碼量也會隨之增加。
避免手寫重復代碼
按照 非死book 總結,一個 model 可能會包含如下代碼:
重載 description 代碼,debug 時方便調試。
- (NSString *)description
{
return [NSString stringWithFormat:@"%@ - \n\t userId: %tu; \n\t nickname: %@; \n\t imageUrl: %@; \n", [super description], _userId, _nickname, _imageUrl];
}
實現 NSCoding protocol,方便持久化到 disk 中。
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeInteger:_userId forKey:@"userIdKey"];
[aCoder encodeObject:_nickname forKey:@"nicknameKey"];
[aCoder encodeObject:_imageUrl forKey:@"imageUrlKey"];
}
實現 immutable model。
數據的 immutability 是個大話題,牽涉到整個 App 的架構和數據的流動。具體到 model 上,我們一般將 property 設置為 readonly,提供專門的 init 方法來初始化對象,將改變對象狀態的機會限制在創建的時候,對象一旦創建好,即使在多個 module 之間傳遞使用,也不會隨意發生狀態的變化,如果要改變 model 中某個 property 的值,只能創建一個新的對象,然后再覆蓋 cache 中的舊 model 對象。所以這里往往需要寫一個到多個便捷的 init 方法來創建對象。(之前還寫過一篇 關于 model 創建的文章 )
@interface User : NSObject
@property (nonatomic, readonly) NSUInteger userId;
@property (nonatomic, readonly) NSString *nickname;
@property (nonatomic, readonly) NSURL *imageUrl;
- (instancetype)initWithUserId:(NSUInteger)userId
nickname:(NSString *)nickname
imageUrl:(NSURL *)imageUrl;
@end
到這里已經不難發覺一個 model 中,重復書寫 property 的場景有不少,依據代碼架構的不同,實際還存在更多的場景,比如我們為了避免共享 model 對象的同一個內存拷貝,往往需要實現 NSCopying protocol 來方便創建語義上等同的對象:
@protocol NSCopying
- (id)copyWithZone:(nullable NSZone *)zone;
@end
又比如,不同的 module 之間為了降低耦合度,往往針對同一份業務數據,有自己的 model 定義,此時我們需要 model 之間相互轉化的 init 方法:
@interface User : NSObject
@property (nonatomic, readonly) NSUInteger userId;
@property (nonatomic, readonly) NSString *nickname;
@property (nonatomic, readonly) NSURL *imageUrl;
- (instancetype)initWithNetUser:(NetUser*)user;
@end
一旦大量的代碼需要重復書寫,不但無謂的損耗了程序員精力,出錯的概率也會隨之增加。Remodel 解決這個問題的方式,是通過腳本來生成相關代碼,工程師只需要定義 model 的 prototype,在加上一些特殊的功能性標簽,即可生成不會出錯的標準化的 model 類,這里頭技術含量并不算高,但用腳本提升效率降低錯誤率的工程化思維方式很值得學習。實際上,我們平時針對新業務,新建一個 Controller 的時候,總會有不少代碼是機械式重復的,MVP 也好,MVVM 也好,一旦定義好設計,使用腳本自動生成一個 Controller 模塊里通用的代碼,能提升我們平時的開發效率。
降低 model 所帶來的耦合度
Remodel 的另一個設計理念是,使用 simple model 來降低耦合度。
我們可以先看一下 model 的職責。一個 model 到底應該承擔多少職責,一直是個存在爭議的話題。一種極端是只包含數據的 model(只有少量的 property),另一中極端是除了數據之外,還包含大量與之相關的業務邏輯代碼,形成 fat model(包含大量 property 和函數)。我看過的更多的真實場景是工程師隨意而為,按個人喜好隨意在 model 中添加自認為相關的業務屬性和邏輯代碼,最后慢慢也走向 fat model 的極端形式。
這個問題的另一個問法是:我們的業務代碼放在那里更好?依我所見可以歸為三類:
- 放在 model 中。
- 放在 Controller 或者 Presenter 中。
- 放在獨立的功能模塊中,比如 xxService,xxManager。
放在哪個位置更優,從設計的角度來說難有統一的標準,但以下兩點,按我的經驗,是可以特意避免的錯誤做法:
三個位置隨意放置業務代碼,沒有統一的規范。這種做法最大的壞處是,程序員 A 接手程序員 B 代碼的時候,需要一段閱讀時間來適應,或者程序員 A 回過頭看自己寫的時間久遠代碼的時候,也需要熱身的時間,不能直接上手 debug。如果一個團隊規范清晰,代碼結構合理,那么所有成員寫的代碼,無論是在代碼風格,還是流程處理上都是高度接近的。
業務代碼過度集中在一個位置,形成 fat class。這種做法的壞處,參與過成熟項目的同學應該都能體會,我肉眼所見的記錄是一個 Controller 文件里有大概 1.5w 行代碼。fat class 不僅閱讀困難,而且會帶來代碼結構的持續惡化。從這個角度來說,我們應該盡量讓 model 里的代碼量少一些,至于是只包含數據,還是允許少量業務函數存在,我認為二者皆可,但我個人更傾向于后者,讓 model 承擔少量和數據緊密相關的業務代碼。
比如一個 User 類中可以包含如下邏輯:
@interface User : NSObject
@property (nonatomic, readonly) NSUInteger userId;
@property (nonatomic, readonly) NSString *nickname;
@property (nonatomic, readonly) NSURL *imageUrl;
- (BOOL)isNicknameValid;
- (BOOL)isUserAvatarDownloaded;
@end
但什么樣的邏輯是和數據緊密相關的呢,這的確是一個偏主觀的判斷,十分考驗團隊成員之間的默契,團隊成員磨合的越久,大家對于什么樣的代碼該放到什么樣的位置,就越容易達成一致的意見。
Remodel 提倡在代碼的設計上,使用 simple model,所謂的 simple model 是將數據與邏輯隔離開來,讓 model 只承擔信息載體的職責,將與之相關的邏輯放到專門的功能類當中。這種做法就是我上面提到的第一種極端(非貶義詞)。好處是數據與行為分離開來,可以工程師 A 定義數據,工程師 B 寫與之相關的業務代碼,并行開發,提升效率,而且業務代碼都在特定的功能類里,定位也容易,不用去一個個 model 中搜索。比如一個 Message 對象定義好后,如果要發送消息,就建立一個 MessageSender 來執行相關邏輯。
這種 simple model 的做法也存在一些潛在的問題。首先是功能類的管理,功能類如何命名呢?別笑,我見過非常多的命名困難戶,甚至包含一些邏輯能力強悍的老司機,就是無法取出一個簡潔貼切的好名字,很多人都只能在 helper,manager,util,handler 這里面挑選。MessageManager 和 MessageSender 哪個更好是顯而易見的。另外我們到底需要多少個與 model 相關的功能類呢?MessageSender 之后是不是還有 MessageKeeper,MessageFormatter 等等 MessageXXX,這個粒度如何把控呢?再者這些功能類如何分門別類的放置在合理的位置,也會是一個問題。
model 從面向對象的角度來看的話,它是應該承擔一些行為代碼的,Martin Fowler 有一段關于 POJO 的闡述,說的是類似的意思:
An acronym for: Plain Old Java Object .
The term was coined while Rebecca Parsons, Josh MacKenzie and I were preparing for a talk at a conference in September 2000. In the talk we were pointing out the many benefits of encoding business logic into regular java objects rather than using Entity Beans. We wondered why people were so against using regular objects in their systems and concluded that it was because simple objects lacked a fancy name. So we gave them one, and it’s caught on very nicely.
我個人也習慣在 model 中寫一些簡單的行為代碼。讓 model 適當的承擔一些行為代碼,可以降低其他業務類的壓力,均衡復雜度。
Remodel 之所以使用 simple model 的另一個原因,我個人猜測是由于通過工具生成的 model 類,其中的業務代碼會被覆蓋,二者比較難以在設計上共存。
Plugin 拓展
Remodel 本身只提供了最常用的代碼生成工具,開發人員完全可以在 Remodel 的基礎之上,實現符合自身架構特色的代碼生成方式。
Remodel 允許開發者使用 TypeScript,以 Plugin 的方式去對 Remodel 做深度定制。
寫代碼的順序
寫代碼有時候看起來和蓋樓特別相似,蓋樓都是先打地基,而后自下而上的逐步完善。在處理新項目架構的時候,特別是對 model 重度依賴的項目(業務模塊多、大量和 server 的數據交互、大量的數據持久化需求、數據之間存有依賴關系),我個人習慣都是先和產品深度討論業務形態,之后設計好 model,處理好 model 之間的關聯,再往上寫 model 的持久化,和 server 的數據交互等等,”地基”搭好之后,再從 Controller 開始寫,逐個攻克業務模塊。這種做法也是強調數據的重要性,把 model 定義好,處理好 model 之間的關聯,model 的轉化與流向, 這些工作預先做好了,基礎就牢固,業務做起來也不容易出錯。
結束語
關于 Remodel 就介紹到這。設計上的東西比較容易產生爭議,以上的觀點都是一家之言,僅供參考之用。WWDC 2017 馬上就要開幕啦,到時候會以 iOS 工程師的視角寫幾篇短文和大家分享下。
來自:http://mrpeak.cn/blog/remodel/