我們至今所寫的 iOS 程式碼都是遵循 OOP 這種程式設計正規化,以物件來臨摹和表達我們對於世界的理解。在設計類的時候,恪守 SOLID 五個原則會讓我們的程式碼更易擴充和維護。SOLID 中的 O 代表的是 Open/closed principle,這篇文章所要探討的不僅僅是類設計中的 Open 和 Closed,而是要站在更廣闊的視角來看待程式碼中的開放與封閉。
前言
我們作為程式碼工作者,不能僅僅滿足於寫出能執行的程式碼,還是注意時刻提高自身的姿勢水平。具體來說,就是加強對於「內功心法」的學習,逐步提升寫程式碼的抽象和設計能力。
程式設計師是理工教的一大分支,我們向來以嚴密的邏輯推導能力為立身之本,我們很容易發現文科生思維中存在的邏輯不連貫,不縝密,不嚴格,我們擅長以 if, else, for, switch 等精巧的關鍵字來闡述邏輯和流程。用程式碼來表達的流程看上去確實很酷,很科學,很真理,可在數學家眼裡,我們大部分程式設計師所寫的程式碼其實「漏洞百出」,和「嚴密」二字幾乎不怎麼沾邊,看起來並不比文科生高明多少。問題出在哪呢?姿勢水平還不夠。
Open vs Closed
我們先以 Open/closed principle 為切入點,對於程式碼的開放和封閉來建立初步的印象。Wikipedia 定義如下:
In object-oriented programming, the open/closed principle states “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
這一原則要求我們設計的功能單元,對於功能擴充是開放的,而對於程式碼修改則是封閉的。不知道大家對於這種抽象的描述作何感想,Peak君初次看到的時候,腦中只感覺一團雲霧繚繞,怎麼能讓馬兒跑,又不用吃草?
這段玄之又玄的描述,體現到程式碼中後,不過就是一些日常所用的語言技巧了,我們可以從多個角度去理解和實現程式碼的開放性和封閉性。
繼承,簡單的繼承關係就可以體現 open/closed principle,如果我們設計好一個父類,這個父類在設計之初就已經有了清晰完備的功能定義,並向天起誓以後絕不修改這個父類一行程式碼,那麼我們可以說這個父類已經 closed 了。想要擴充功能怎麼辦?新建一個子類繼承自這個父類,在子類中新增我們所需要的新功能,這樣就做到了 open。一言蔽之,父類對於程式碼修改是封閉的,而對於子類的功能擴充是 open 的。實際工程中,多少人能忍得住不修改父類呢?
多型,多型配合介面使用也能體現 open/closed principle,我們在設計功能單元的時候,只定義介面,而不規定具體實現的細節。在類或者模組交付的時候,我們繼續向天起誓以後絕不修改介面中的定義,那麼介面就是 closed 了。但是後期我們可能需要修改具體實現的細節,需要擴充功能,於是我們替換一個實現了該介面的另一個類,這個新的類實現對於程式碼修改是 open 的。簡而言之,介面對於程式碼修改是封閉的,實現對於程式碼修改是開放的。這也是為什麼,我們在寫 iOS 程式碼的時候,需要大量運用 protocol。
這麼看來,開放和封閉的定義還是很清晰的,二者針對的物件不同就可以合理共存。不過我們為什麼既要封閉又要開放呢?因為封閉的事物是靜態的,穩定的,安全的,不寫一行程式碼就不會有 bug 不是嗎?可是我們所做的每一個工程都是處於變化的狀態,每一個新 feature 都是為了迎合不斷變化的市場需求,所以 open 是不可避免的,怎麼辦呢?讓 open 與 closed 並存,讓穩定的部分不變,在 closed 的程式碼基礎之上去做擴充,去 open 新的程式碼。
Algebraic data types
聊完了我們熟悉的繼承和多型,下面我們進入一個稍微陌生一些的領地:Algebraic data types。
Algebraic data types 是純函數語言程式設計語言 Haskell 中的一種型別定義,這是一個看上去簡單,實際上令初學者極其費解的技術概念。之所以費解,是由於它主要應用在資料模型的定義,和我們平常寫業務所用的 int, float 這種 data type 完全不是一回事。
Algebraic data types 可以簡單的理解為一些 data type 的集合,這裡的 data type 就是我們傳統意義上的資料型別,比如 bool, int, double 等等,在這個 data type 的集合之上,Algebraic data types 提供一些特定的代數操作,可以對 data type 集合裡的每個 data type 執行邏輯。代數操作通常為兩種:sum 和 product。很抽象是不是?到底有什麼用?我們對應到 iOS 中的程式碼來理解下。
比如我們日常所用的 BOOL 型別:
1 2 |
BOOL isValid = true; isValid = false; |
isValid 的值要麼是 true 要麼是 false,是二選一的關係,所以 isValid 的值有兩種可能性,即 true 和 false 相加,所以 BOOL 型別可以理解成一種 sum type。
再看 CGPoint :
1 2 3 4 |
struct CGPoint { CGFloat x; CGFloat y; }; |
x 和 y 同時存在於 CGPoint 這個型別當中,不是二選一,而是一種類似於組合同時存在的關係,我們把 CGPoint 這種由兩個子 type 所共同構成的 data type 稱之為 product type。
你可能發現了,所謂的 sum type 和 product type 就是對 data type 集合中的元素進行 and 或者 or 操作,從而拼裝出各種可能的組合。OOP 下的 data type(比如我們自定義的 class)強調的是對於 property 和 function 的封裝,而 Algebraic data types 完全換了一個視角,看重的是 data type 的組合方式。當我們以遞迴的方式使用 Algebraic data types 來描述各種 data 的時候,就開啟了一扇新世界的大門。
sum type 和 product type 都是 Algebraic data types。按照這種規則定義的 data type 到底有什麼用處?好處有很多,其中之一和這篇文章的主題相關。Algebraic data types 有個重要的特性:Algebraic data types 對於自身 data type 集合中的每個 type 的處理是以窮舉的方式,而且 data type 集合中的一旦定義好之後是不允許修改的,closed!這一點和我們在 OOP 下自定義的 Model Class 非常不同,Class 是允許被繼承來擴充功能的,而 Algebraic data types 一旦定義好就已經 closed 了。
比如 isValid 如果定義包含 true 和 false 之後,是不允許新增 half-true 的,同時所有對於 isValid 的操作要窮舉 true 和 false 兩種可能性。
Algebraic data types 的 closed 和 exhaustive 特性可以讓程式碼更加穩定,當然這種特性需要語言層面的支援,Objective C 並沒有相關的特性,但我們可以在程式碼設計中借鑑其思想。
我們在平時寫業務的時候,經常需要設計各種各樣的 model 類。Facebook 在 2016 年開源了一個專門用來管理和生成 model 的 framework,叫做 Remodel。這個庫功能強大而且全面,其中之一就是生成符合 Algebraic data types 特性的 model。以如下程式碼為例,描述的是一個具有多種型別的訊息 model:
1 2 3 4 5 6 7 8 9 10 11 |
@interface MessageContent : NSObject + (instancetype)imageWithPhoto:(Photo *)photo; + (instancetype)stickerWithStickerId:(NSInteger)stickerId; + (instancetype)textWithBody:(NSString *)body; - (void)matchImage:(MessageContentImageMatchHandler)imageMatchHandler sticker:(MessageContentStickerMatchHandler)stickerMatchHandler text:(MessageContentTextMatchHandler)textMatchHandler; @end |
MessageContent 有三種可能的型別,image, sticker, text。MessageContent 提供的 match 方法以窮舉的方式來處理所有可能的場景,對於 MessageContent 的使用者來說,一定不會漏處理任何一種可能性,強制 model 的使用者考慮所有的場景。
這種做法的好處是程式碼一旦生成就極其穩定可靠,不允許修改,closed。缺點也很明顯,一旦業務要求我們增加一種新的 type,比如 MessageContent 為 voice 的語音訊息,會難以下手,因為一旦修改就必須改變 match 方法簽名,以窮舉的方式新增一種 type 處理,程式碼的改動牽涉面必然很廣。
所以你看,到底是設計成 closed 還是 open 的,其實是一次根據業務場景的取捨,在變與不變之間做權衡。這裡介紹 Algebraic data types 目的在於說明,我們在做程式碼設計的時候,closed 和 exhaustive 的設計方式會讓我們的程式碼更加可靠和穩定。
Optional in Swift
剛開始學習 Swift 的時候,不知道大家有沒有好奇過為什麼要引入 optional 這樣一個新型別,optional 使用的場景也非常之多,有很多的文件去介紹在不同的語法下 optional 如何使用,可為什麼要 optional 呢?和我們用 Objective C 時判斷是否為 nil 有什麼區別呢?
我們先看下面一段函式:
1 2 3 4 |
- (User*)getLuckyUser { //perform some calculation... return _user; } |
這段很常見的程式碼沒有考慮一種場景,就是 _user 為 nil 的情況。你可能會說函式返回 nil ,函式的呼叫方自己去判斷就可以了。當然如果返回 nil,在 Objective C 的 runtime 裡,給 nil 物件傳送訊息也是安全的,這種安全只是表示不會 crash,但有可能原本應該執行的邏輯就沒有繼續下去了,從這一角度去看,nil 物件是對業務不安全的。而且我們把這種 nil 的 case 所造成的影響延遲到了 run time 。
更合理的做法是在編譯時就考慮 nil 這種 case。optional 正是為此而生,如果我們定義返回值為 optional,那麼 optional 的使用方就一定要考慮值不存在的場景,如果漏處理了為 nil 的場景,就會編譯器報錯,這樣不光不會 crash,而且對業務邏輯來說也是安全的。
感覺靈敏的同學可能發現了,optional 型別和上面提到的 Algebraic data types 中的 sum type 非常相像,它表達的也是一種 or 的關係,即值要麼存在,要麼為 nil。當我們使用 Algebraic data types 來描述 data 的時候,語言本身會強制我們做 exhaustive checking,去考慮 data 的所有可能性。這是另一個 Swift 比 Objective C 更安全的有力證據,Swift 吸收了函數語言程式設計語言中的很多優秀特性。
總結就是,當我們使用 optional 來寫業務的時候,Swift 會強制我們去考慮 data 的各種可能性,這樣寫出來的函式,其邏輯就是完整的,全面的。
總結
還有不少能體現 open 和 closed 設計思想的例子,比如 java 中的 final 關鍵字,又比如設計模式中的 Visitor Pattern,大家也可以聯想下類似的例子。我個人比較喜歡寫這類隨意遐想的文章,暢想不同技術概念之間在設計思想上的關聯,加以總結和鞏固。好啦,囉囉嗦嗦說了一堆抽象的概念,讀到此處還沒有放棄的朋友們辛苦了,為你們的耐心,乾杯!
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!