我們都知道,全局變量應該盡量少用或不用,因為它會帶來兩個明顯的問題:耦合和不確定性。有了它,單元測試就不好進行,即使通過了測試,也不能確保這個全局變量變了之後是否能通過測試。 我們經常使用的單例就有全局變量的意味:外部可以直接拿來用,並且可以在任何地方被修改。
為了加快開發速度,往往會以功能實現優先,其中的一個「方法」就是提供可變對象,比如像 OC 裡的 NSMutableDictionary。前兩天正好遇到一個與此有關的 case,可以拿出來說一下。
我們的網絡層發送請求時,默認會帶上一些系統參數,比如 iOS 系統版本,app 版本等。同時如果用戶已經登錄了,也會帶上一些用戶信息,比如 token。為了方便復用,我們每次請求都會把已有的參數放在一個自定義的網絡請求類,假設這個類的名字叫APIClient。同時又允許外部動態添加一些請求參數,比如用戶信息,是否啟用調試等。
出於方便考慮,我們給 APIClient 類加了一個 NSMutableDictionary 屬性 builtinParameters,這樣外部只要拿到 APIClient 的單例,然後往這個屬性裡面添加額外的參數就可以了。APIClient 裡會把這些參數組裝成 querystring 發送給服務端。
就這樣正常運行了一段時間,忽然有一天發現用戶登出後,Ta原先的一些登錄信息還是被發送給了服務端。因為這個網絡請求類並沒有做過改動,所以排查起來沒什麼頭緒。經過多次抓包和跟蹤後,終於定位到了問題的原因:builtinParameters 這個屬性在外部被改變了。更細致的原因跟一次重構有關,這裡就不展開了。
所以可變對象會給調試和維護帶來麻煩,尤其是這些對象多起來後,更是不好處理。
「可變對象」就像男人的承諾:不可信,不知道什麼時候會因為什麼原因發生改變。
「不可變對象」就不一樣了,拿到的是什麼,就是什麼,不會改變,除非被換成了一個新的。
但「這世界唯一不變的就是變化」,不可變對象如何來應對這個充滿變數的環境呢?
先來看一下這個「動畫」
通過連續快速地翻頁來形成動畫的假象,這主要是利用了人眼的視覺停留。
有點扯遠了,但這跟「不可變對象」可變化,還挺像的,這些圖像是靜態的,不變的,但這本書讓這些圖像變了起來。這本書可以是一個類,其中的圖片可以是一個 ivar,外部可以給這個 ivar 設置新的 value,這樣對於 class 來說,就可以放心地使用這個 ivar,不用擔心什麼時候這個 ivar 自身會發生變化,比如 [dict addObject:]。
再來看看 ReactJS+Flux 是如何使用 Immutable Objects 的。
先來說說 Flux,用一張圖就能差不多描述清楚了
Flux 的一個特點是,數據是單向流動的,就像漏斗一樣。
Dispatcher 是一個「分發器」,它的職責是接受所有的 Action,簡單組裝後,扔給 Store,其他的事情就不管了。
Store 是一個數據中心,當 Store 接收到 Dispatcher 過來的 Action 時,會根據這些 Action,生成新的 States,然後再把它傳給 View。
View 拿到這些新的 States 後,會有選擇的進行組件的更新。
這裡的 States 就是一個不可變對象,Store 不會去修改 States 的某個屬性,而是生成一個新的。但是生成一個新的成本不是會很大?是的,所以可以利用 Copy on Write 等技術進行優化。
接下來看看 ReactJS 拿到這個新的 property 後會如何處理,先來看一張圖
View 會對新的 property 和當前的 property 做比較,如果數據是一致的,那就什麼也不做(就像 C2 一樣),它下面的節點也不用比較了;如果數據不一致,再往下找,一直找到那[幾]個需要更新的節點。
這整個過程沒有使用到 Mutable Objects,但照樣 Getting Things Done。
小結
Immutable Objects 和 Mutable Objects 有各自的使用場景,後者可以作為前者的容器。比如 Facebook 在他們的架構文章中提到,他們的 Model 類是只讀的,但 Model 寄生的對象可以更新 Model。我們可能習慣了使用可變對象,因為各種教程/編程書籍上都是這麼寫的,但合理地使用「不可變對象」有時會帶來更好的效果。
References
How Immutable State Helped Facebook to Improve Its iOS App Architecture
Controlling Complexity in Swift
Simple Made Easy
Domain Driven Design