Objective-C 與 SmallTalk

發表於2017-02-19

本文從OC的可變資料和不可變資料作為引子,開始聊聊我眼中的OC和SmallTalk,想到哪兒就聊到哪兒了。本文中的觀點都是個人觀點,如果大家理解不一樣,純屬正常,歡迎討論。關於可變資料和不可變資料就不多聊了,有很多文章已經聊過了

OC中的可變/不可變資料

寫過OC都知道,OC的基礎型別分為Mutable和IMMutable兩種型別,分別對應可變資料和不可變資料。OC在具體實現的時候,使用了類簇這樣的設計模式。

類簇

不可變資料和可變資料內部實際上是一個家族類,多個類各司其職。而暴露給使用者的只有不可變資料和可變資料這兩類。並且可變資料繼承自不可變資料。這樣強調了OC的簡潔性,使呼叫者不需瞭解多個內部的家族類。

NSArrayNSMutableArray就是這樣的典型實現,具體點我。但是這樣也就限制住了擴充性。比如說,我現在想要實現一個數字排序的陣列類,我要怎麼做?很簡單,建立一個繼承自NSArryNSOrderArray,寫一些排序功能的程式碼,初始化後自動給陣列排序,查詢的時候用二分查詢就好了。迄今為止沒有問題,現在我想再實現一個可變型別的NSMutableOrderArray,我思考了一會,懵逼了。NSMutableOrderArray是該繼承自NSMutableArray還是NSOrderArray?如果繼承自NSMutableArray,那我就要重新寫排序程式碼,如果繼承自NSOrderArray,那我就要重新寫可變程式碼。這兩條路,雖然最後可以實現出來NSOrderArrayNSMutableOrderArray,但是繼承關係肯定是亂的。

如果我知道NSArryNSMutableArray內部的家族類的細節,_NSArrayI_NSArrayM,就可以自由組合出我想要的數字排序陣列類。很可惜,作為呼叫者這些細節被遮蔽掉了。

那我該如何實現出來這樣的類?像NSOrderSet一樣,將NSOrderArray繼承自NSObject,從頭再實現一個陣列,然後再將NSMutableOrderArray繼承自NSOrderArray。這個工作量就太大了,還不如每次使用NSArray的時候再排序,犧牲點效能沒什麼關係。在這個過程中說明了,類簇是OC簡潔性和擴充性的權衡了。

如果不同類簇設計,有沒有別的設計方法解決這個問題,多繼承?C++將多繼承加入又拿掉,加入又拿掉。實際上多繼承帶來的麻煩和解決的問題,還不一定誰多,多繼承Pass。AOP?實現出面向切面程式設計的協議,比如”NSIMMutableSequence“、”NSMutableSequence“、”NSOrderSequence“,這三個協議分別對應不可變資料集合、可變資料集合、排序集合協議。實現的類組合想要的協議即可。這樣使簡潔性下降了,自己在實現NSMutableOrderArray的時候,conforms了”NSMutableSequence“和 “NSOrderSequence“後必定要寫幾個關於可變集合的方法。不過,還可以接受,降低了點簡潔性換來了很強的擴充性。我個人覺得是個不錯的設計方式,Swift就更加向這種思想靠攏。

我們還可以從Apple的角度想想OC。你能想象Apple挑了一門複雜的語言,然後你要做一個APP,需要先學1年這個複雜的語言?Excuse me?這樣,Apple早就倒了。

橋接

既然同種資料型別有可變資料和不可變資料之分,這兩者就要提供互相轉換的方法。而OC中的拷貝就是這兩者的橋接。從不可變資料mutableCopy就可以得到可變資料,可變資料copy就可以得到不可變資料。

更多關於拷貝的問題,這篇文章講的很好,推薦一下。copy實際上是對可變資料才有用的操作,對於不可變資料再複製出來一份一模一樣的資料是沒有意義的。對於可變資料copy就是,將狀態消除,製作出一份不可變的“快照”,而mutableCopy是製作出一份一模一樣的可變資料,就像是這個資料開啟了另一條時間線,另一個宇宙。OC中的設計就是這樣的:

如果光看這兩個介面,我可能會這樣設計:

Apple將copymutableCopy放在了NSObject裡面,讓所有子類都可以不再多新增方法也沒有大問題,其中也可能有些記憶體管理上的考慮。但是我個人覺得後者這麼設計,介面會更加明確,思想體現的更加直接。

架構設計

2016年,國外iOS圈開始不斷嘗試IMMutable Model Layer,這其中有FacebookPinterest。Facebook還開源了Remodel加強這一實踐,以後有時間會寫篇文章好好深聊這個東西,此處先留坑。這實際上,也是響應式程式設計思想的體現。國內就不知何時會搞起IMMutable Model Layer這樣的架構設計,也許就在這2017年吧。

不同語言中的可變/不可變資料

Python中的可變資料和不可變資料是根據資料型別定的,list和dict都是可變的,str和tuple都是不可變的。可變資料沒有不可變的版本,不可變資料也沒有可變的版本。

JS中的list、 map都是可變的,原本是沒有不可變版本的。Facebook開源了個immutable.js,提供這些資料的不可變版本。這樣JS中就和OC一樣資料可以分為可變資料和不可變資料了。

既然別的語言都不原生提供可變資料和不可變資料版本,那OC為什麼要搞特殊提供呢?使用過OC的,都聽說過OC的設計思想是基於SmallTalk的。要想了解為啥OC這麼設計,那就得追溯到SmallTalk。

SmallTalk

smallTalk_immutable

原句在這裡,這是1993年給出的一份SmallTalk演變歷史。關於Local State在這一段:

This is probably a good place to comment on the difference between what we thought of as OOP-style and the superficial encapsulation called “abstract data types” that was just starting to be investigated in academic circles. Our early “LISP-pair” definition is an example of an abstract data type because it preserves the “field access” and “field rebinding” that is the hallmark of a data structure. Considerable work in the 60s was concerned with generalizing such structures [DSP ]. The “official” computer science world started to regard Simula as a possible vehicle for defining abstract data types (even by one of its inventors [Dahl 1970]), and it formed much of the later backbone of ADA. This led to the ubiquitous stack data-type example in hundreds of papers. To put it mildly, we were quite amazed at this, since to us, what Simula had whispered was something much stronger than simply reimplementing a weak and ad hoc idea. What I got from Simula was that you could now replace bindings and assignment with goals. The last thing you wanted any programmer to do is mess with internal state even if presented figuratively. Instead, the objects should be presented as sites of higher level behaviors more appropriate for use as dynamic components*.

文中主要說的是SmallTalk從LISP-Pair中主要學習的地方就是重新繫結和賦值(重新繫結和賦值實際上就是不可變資料,SICP中有提),並且不希望程式設計師亂用狀態。使用物件導向程式設計應呈現為,將高層次的抽象行為動態的繫結到物件身上。

還有下面這一段:

Where does the special efficiency of object-oriented design come from? This is a good question given that it can be viewed as a slightly different way to apply procedures to data-structures. Part of the effect comes from a much clearer way to represent a complex system. Here, the constraints are as useful as the generalities. Four techniques used together—persistent state, polymorphism, instantiation, and methods-as-goals for the object—account for much of the power. None of these require an “object-oriented language” to be employed—ALGOL 68 can almost be turned to this style—an OOPL merely focuses the designer’s mind in a particular fruitful direction. However, doing encapsulation right is a commitment not just to abstraction of state, but to eliminate state oriented metaphors from programming.

尤其是最後一句話,SmallTalk提出的物件導向程式設計思想,不僅要抽象出狀態,還要盡力幹掉狀態。這就明朗了,SmallTalk主張的是多使用不可變資料幹掉狀態。所以,OC要分為可變和不可變型別。能用不可變型別就用不可變型別,必須牽扯到狀態時,再用可變資料。其實,這也就看出來了這兩年提出的響應式程式設計、Facebook提出的immutable model layer都是SmallTalk早在幾十年前主張的東西,只不過我們沒有注重這種思想程式設計。然後在合適的時機,被人挖出來,重新強調一下。

既然聊到這裡,就接著來聊一聊SmallTalk吧,SmallTalk只有兩個核心思想:

  • 一切皆物件(Object),包括3、4這樣的整形數字,包括bool型別。如果訊息有引數,我想你已經猜到了,跟在:後面。
  • 過程抽象即發訊息,其中包括3+4這樣的簡單算術,可被解釋為給3傳送一個4作為引數的+訊息。

關於這點還有更醍醐灌頂的,甚至包括條件語句(if)和迴圈(for)都是向物件發訊息。if語句的一般形式是這樣的

這解釋為向bool表示式的結果true或者false物件,傳送ifTrue:ifFalse:訊息。如果是true就執行ifTrue的引數,如果false就執行ifFalse的引數。而[ ]表示的引數就是我們熟悉的塊!SmallTalk中的塊用[ ]表示,並且也是一個物件。

這一切都基於SmallTalk的思想:過程抽象即發訊息。並且會呈現出在寫SmallTalk語言的時候,都是這樣的形式:。那SmallTalk如何解釋?這些訊息是有結合優先順序的,以下為優先順序從高到低:

  • 一元訊息:沒有引數的字母訊息,比如無引數的方法。
  • 二元訊息:非字母的訊息,比如+。
  • 關鍵字訊息:有引數的字母訊息,比如有引數的方法。

比如,年底你拿到了年終獎,又遇到了個心儀的女孩,然後你早上從床上醒來,發現這只是一場夢:

這句結合的優先順序是這樣:

解釋結合的時候,還有個有趣的一點。基本算術加減乘除,都是訊息,而且都是二元訊息,優先順序是一樣的。如果這樣寫,不會得到你想要的結果:

要想要得到你想要的結果,必須要手動加括號:

如果將OC與SmallTalk做下對比,你會發現,OC可不是一切皆物件,也不是一切皆發訊息。當然,OC也有與SmallTalk一致的地方:

  • self不管在什麼上下文中,永遠代表訊息的接受者。super代表將覆蓋的父類方法傳送給訊息的接受者。
  • 只有物件可以訪問自己的變數,即變數私有。
  • 子類可以覆蓋父類方法,但不能重新定義變數。

到這裡,再重溫一下這句話:使用物件導向程式設計應呈現為,將高層次的抽象行為動態的繫結到物件身上。SmallTalk整個語言呈現出,不就是這句話最好的證明嗎?

是不是很驚訝SmallTalk的純粹?不過,SmallTalk並不是工業級語言,這也代表著SmallTalk沒用那麼多的妥協和trade-off,是門理想語言。SmallTalk作為提出物件導向程式設計概念的語言,更多的是為後來物件導向程式設計工業級語言鋪好了路。

引用

程式設計語言概念和結構(原書第2版)

相關文章