你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> iOS代碼設計中的開放與封閉

iOS代碼設計中的開放與封閉

編輯:IOS開發基礎

我們至今所寫的 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 類型:

BOOL isValid = true; 
isValid = false;

isValid 的值要麼是 true 要麼是 false,是二選一的關系,所以 isValid 的值有兩種可能性,即 true 和 false 相加,所以 BOOL 類型可以理解成一種 sum type。

再看 CGPoint :

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:

@interface MessageContent : NSObject (NSCopying, NSCoding)(因識別問題,此處圓括號代替尖括號)
+ (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 有什麼區別呢?

我們先看下面一段函數:

- (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,大家也可以聯想下類似的例子。我個人比較喜歡寫這類隨意遐想的文章,暢想不同技術概念之間在設計思想上的關聯,加以總結和鞏固。好啦,啰啰嗦嗦說了一堆抽象的概念,讀到此處還沒有放棄的朋友們辛苦了,為你們的耐心,干杯!

歡迎關注公眾號:MrPeakTech

qr.jpg

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved