本篇文章翻譯自:IF YOU'RE SUBCLASSING, YOU'RE DOING IT WRONG.
原作者:Hector Matos
原發(fā)表日期:2015-07-13
Swift的核心
我們可以通過等式的傳遞性來理解swift:
- Swift的核心是面向協(xié)議的編程。
- 面向協(xié)議的編程的核心是抽象(abstraction)和簡化(simplicity)。
- 所以swift的核心就是抽象和簡化。
你可能對我的標題感到詫異。我并不是說子類沒有價值,尤其在使用單一繼承(single inheritance)的情況下,類和子類當然是強有力的工具。然而我想說的是,iOS日常開發(fā)的問題是對類和繼承的過度使用。作為面向?qū)ο蟮木幊陶撸╫bject-oriented programmer,后面統(tǒng)一替換為OOP編程者;object-oriented programming后面統(tǒng)一簡寫為OOP)我們總是會自然的傾向于使用引用類型和類去解決問題,但是我個人還是認為應該反過來,傾向于用值類型代替引用類型。我們還是要去寫模塊化的,可伸縮的并且可重用的代碼,這一點不會變。swift中強大的值類型就可以幫我們實現(xiàn)此目的,且不需要對引用類型有過強的依賴。我認為不僅面向協(xié)議的編程(protocol oriented programming,后統(tǒng)一替換為POP)可以幫我們實現(xiàn)這點,另外2種編程類型也可以,且都具有抽象和簡化的核心思想,這兩種分別是:面向值的編程(value-oriented programming,后面統(tǒng)一替換成VOP)和函數(shù)式編程(functional programming)。
先說清楚,我絕不是這些種編程類型(POP,VOP和函數(shù)式編程)的專家。和你一樣,從MMM時代(manual memory management - 手動內(nèi)存管理)開始我就是一個OOP編程者。通過自學,從開始我就很重視值抽象(value abstraction)和簡化的思想。我都沒有意識到自己是一個傾向于函數(shù)式編程(functional programming)的OOP編程者,而且很多時候用的都是VOP和POP的思路。這可能是我為什么在第一天就興高采烈的加入了swift的浪潮之中的原因。在WWDC的一整周里,swift的核心理念與我認為的該怎樣去編程是如此之契合,這個感受一直充斥在我腦海中。通過這篇文章,我希望能幫助你(OOP的編程者)打開思路,去考慮該如何用更加Non-OOP(非OOP)的方式去解決問題。
OOP的問題(和我不得不去學它的原因)
我會是第一個跳出來說的:不用OOP的話做出iOS應用很難。Cocoa的核心就是OOP。沒有OOP的話你根本寫不出來一個iOS應用。有時候我會幻想這不是真的。如果你有不同觀點,趕快證明我是錯的吧。我真的需要這樣,求你了,證明我是錯的吧!
不管怎么樣,你總會遇到必須用對象、用引用類型解決問題的時候,然后由于Cocoa的規(guī)定而被迫使用類(classes)。這種情況下你碰到的問題都是我們大家熟知并熱愛的:
- 傳遞class的實例這個做法好像總是有種不可思議的能力:你想用一個實例的時候,讓這個實例的狀態(tài)(state)和你所期望的不一樣。(這是由于可變狀態(tài)(mutable state)導致,你這個對象的另一個享有者在它覺得合理的時候能夠改變此對象的屬性。)
- 如果不用多繼承的話,從一個很棒的class派生出子類從而獲得它的擴展功能妨礙了你使用另外一些很棒的class的更多更能,而且還增加了復雜性。(舉例來說,試著去把2個
UITextField
的子類結(jié)合起來,生成一個擁有這2者特性的超級UITextField
吧。) - 上面一條的另外一個問題是會引出意外行為(unexpected behavior)。如果你遇見了類似上面一條所描述的情況,你就陷入到了一個依賴問題中:你連接了2個superclass各自的特性,對于其中一個superclass的一處改動可能會給另外一個superclass帶來不良影響。這就是被周知的class之間緊耦合(tight coupling)所帶來的問題。
- 單元測試中的mocking。有些classes在系統(tǒng)中的環(huán)境狀態(tài)下耦合過于緊密,想完全測試這些classes就需要你創(chuàng)建每個class的假表象。我都不用告訴你本質(zhì)上你并沒有真正的測試了這個class,你不過是在假裝測試它。這里就不提很多Mocking的庫是用運行時的小把戲來造一個假的class了。
- 并發(fā)(Concurrency)問題。這和上面提到的可變狀態(tài)是伴隨出現(xiàn)的。你從多個線程中同時改變一個引用就會引起這個問題,在運行時使對象之間的同步發(fā)生異常,這點也真的不用和你說了。
-
很容易導致出現(xiàn)像上帝類(God classes - 承擔著很多subclasses需要的重要高層級代碼的所有責任),Blobs(有過多職權(quán)的classes),Lava Flow(因為含有太多的非法代碼導致任何人都不敢碰的classes)等等這些種反面模式(anti patterns)。
image
POP 面向協(xié)議的編程
陷入OOP的反面模式特別容易。多半時間我們(包括我)就是太懶而不愿意去點File>New File。結(jié)果是在現(xiàn)有class的基礎上添加一個函數(shù)是如此輕松,我們就不愿意從零開始建一個新的class了。如果你一直這么干,而且一直非常懶的從一個"很重要"的class派生subclass的話,你就把上帝類/死星類給弄出來了。實際上我之前就這么干過:我給一個app里的每個view Controller
都加了能呈現(xiàn)一個指向navigationController
的navigationBar
的error view的功能。唉,我可真蠢。直到要改動那個Error上帝類行為的時候,我不得不把整個app都改一遍。這不是聰明的做法,你真應該看看那些bug。
如果使用了POP,這個Error上帝類很大程度上就能很容易的抽象出來,以后改進它也方便。(順便說下如果你想學POP,我極力推薦你去看這個視頻)。想想就會覺得好笑,因為在這個視頻中Apple自己都說:
"從一個protocol開始,別從class開始。"
<small> Dave Abrahams: 毀你三觀教授</small>
這是一個能展示(之前的方式)有多殘暴的例子:
class PresentErrorViewController: UIViewController {
var errorViewIsShowing: Bool = false
func presentError(message: String = “Error!", withArrow shouldShowArrow: Bool = false, backgroundColor: UIColor = ColorSalmon, withSize size: CGSize = CGSizeZero, canDismissByTappingAnywhere canDismiss: Bool = true) {
//寫下了復雜的,脆弱的代碼
}
}
//說一下,有100個class繼承了這個class
EveryViewControllerInApp: PresentErrorViewController {}
隨著項目的進行事情馬上變的明了:并不是每一個UIViewController
需要這個error邏輯,或是真的需要這個class所提供的每一個功能。我團隊里任何一個人都可以輕易的在這個superclass里改點兒什么,從而影響整個app。這就讓代碼變得脆弱。還使得代碼呈現(xiàn)出了多態(tài)。當本應該是由子類決定它自己的行為,這里的superclass卻給幫著決定了。下面是在swift 2.0中我們?nèi)绾斡肞OP來更好的構(gòu)建這段代碼:
protocol ErrorPopoverRenderer {
func presentError(message: String, withArrow shouldShowArrow: Bool, backgroundColor: UIColor, withSize size: CGSize, canDismissByTappingAnywhere canDismiss: Bool)
}
extension UIViewController: ErrorPopoverRenderer { //使所有遵從于ErrorPopoverRenderer協(xié)議的UIViewController具有一個presentError的默認實現(xiàn)
func presentError(message: String, withArrow shouldShowArrow: Bool, backgroundColor: UIColor, withSize size: CGSize, canDismissByTappingAnywhere canDismiss: Bool) {
//加上呈現(xiàn)error視圖的默認實現(xiàn)
}
}
class KrakenViewController: UIViewController, ErrorPopoverRenderer { //Drop the God class and make KrakenViewController conform to the new ErrorPopoverRenderer Protocol.
func methodThatHasAnError() {
//…
//拋出error,原因是Kraken海妖今天吃人會感到不適。
presentError(/*blah blah blah 好多參數(shù)*/)
}
}
看,這里發(fā)生了很炫酷的事情。我們不僅消除了上帝類的存在,還讓代碼更加的模塊化并增強了它的擴展性。通過創(chuàng)建一個 ErrorPopoverRenderer
協(xié)議,就會讓任何遵循了該協(xié)議的class具有呈現(xiàn)出一個ErrorView
的能力。還不止這些,我們的KrakenViewController
class不用必須實現(xiàn)presentError
這個函數(shù),因為我們擴展了UIViewController
,讓它提供了一個默認實現(xiàn)。
唉不過等下!這有個問題!我們每次想要呈現(xiàn)一個ErrorView的時候都必須要去實現(xiàn)每一個參數(shù)。這就有點兒讓人不爽了,因為我們不能在protocol協(xié)議函數(shù)聲明中為參數(shù)提供默認值。
我還挺喜歡這些參數(shù)的!更糟的是在讓代碼更具模塊化特征的過程中我們引入了復雜度。還是繼續(xù)吧,用swift 2.0中新加的一個小妙招來多少的補償一下:
protocol ErrorPopoverRenderer {
func presentError()
}
extension ErrorPopoverRenderer where Self: UIViewController {
func presentError() {
//在這里加默認實現(xiàn),并提供ErrorView的默認參數(shù)。
}
}
class KrakenViewController: UIViewController, ErrorPopoverRenderer {
func methodThatHasAnError() {
//…
//拋出error,原因是Kraken海妖今天吃人會感到不適。
presentError() //Woohoo! 沒有參數(shù)了!我們現(xiàn)在有默認實現(xiàn)了!
}
}
好了,現(xiàn)在看起來已經(jīng)很不錯了。我們不僅消除了這些煩人的參數(shù),還用swift 2.0的新特性在protocol的層級上用Self
給了presentError
一個默認實現(xiàn)。用Self
意味著當且僅當協(xié)議的遵循者是繼承自UIViewController
的情況下,這個擴展才會有效。這就讓我們能夠把ErrorPopoverRenderer
真的當做是一個UIViewController
,而甚至不需要對后者做擴展!更棒的是,從現(xiàn)在開始,Swift的運行時是以靜態(tài)調(diào)度而非動態(tài)調(diào)度去調(diào)用presentError()
方法。大致的意思就是我們在函數(shù)調(diào)用點給presentError()
方法增強了一點性能。
哎,不過還是有個問題。到這里我們POP的旅途暫時告一段落,但對它的完善依舊不會停止。我們的問題就是如果只想對一部分參數(shù)使用默認值,對剩下的不用默認值該怎么做?在這方面用POP的話基本幫不上什么忙,但是我們可以尋求另外一種方法?,F(xiàn)在,我們使用VOP吧。
VALUE-ORIENTED PROGRAMMING
看到了吧,POP和VOP總是伴隨出現(xiàn)。在上面的WWDC視頻鏈接中,Crusty提出了一些大膽的論斷:我們用struct
和enum
類型就可以做到一切class能做到的事。我很大程度上同意這點,但沒這么極端。依我看,protocol本質(zhì)上是把VOP粘合在一起的膠水,這點我和Crusty持相同態(tài)度。實際上既然我們說到了Swift的核心理念以及VOP,我想給你們看看從Andy Matuschak的精彩訪談中關于Swift中的VOP
的話題里面摘出來的一張極好的圖:
能看出來Swift的標準庫中,僅有的4個class,和余下的95個struct和enum的實例共同構(gòu)建了Swift功能的核心。
Andy如此闡述道:用Swift編程的時候我們要去考慮用一層很薄的對象層,和一層很厚的值類型層。Class是有它們的地方,但是我想盡最大程度的去認為它們的位置只應該處于對象層中的一個很高的級別上,在這里通過操縱值類型層中的邏輯來管理各種行為。
"把邏輯和行為分開"
-Andy Matuschak
和你所了解的一樣,值類型被賦給一個變量或者常量,抑或是傳給函數(shù)做參數(shù)時是它的值被拷貝的。這就讓值類型在任何時候只有一個享有者,從而降低復雜度。和引用類型相反,在賦值過程中引用類型會有很多享有者,其中一部分你甚至都沒意識到。在任何時間點使用引用的話會帶來一些副作用:引用的享有者會搗蛋,在背后偷偷改變這個引用。Class = 高復雜度,值 = 低復雜度。
通過利用值類型的簡約特性,咱們實現(xiàn)一下之前提過的默認參數(shù)的設計吧。我們用的是 Brian Gesiak的value options paradigm方法:
struct Color {
let red: Double
let green: Double
let blue: Double
init(red: Double = 0.0, green: Double = 0.0, blue: Double = 0.0) {
self.red = red
self.green = green
self.blue = blue
}
}
struct ErrorOptions {
let message: String
let showArrow: Bool
let backgroundColor: UIColor
let size: CGSize
let canDismissByTap: Bool
init(message: String = "Error!", shouldShowArrow: Bool = true, backgroundColor: Color = Color(), size: CGSize = CGSizeZero, canDismissByTappingAnywhere canDismiss: Bool = true) {
self.message = message
self.showArrow = shouldShowArrow
self.backgroundColor = backgroundColor
self.size = size
self.canDismissByTap = canDismiss
}
}
使用上面的選項型struct
(是值類型?。┚褪刮覀兊腜OP帶上了一些VOP的色彩,如下:
protocol ErrorPopoverRenderer {
func presentError(errorOptions: ErrorOptions)
}
extension ErrorPopoverRenderer where Self: UIViewController {
func presentError(errorOptions = ErrorOptions()) {
//在這里加默認實現(xiàn),并提供ErrorView的默認參數(shù)。
}
}
class KrakenViewController: UIViewController, ErrorPopoverRenderer {
func failedToEatHuman() {
//…
//拋出error,原因是Kraken海妖今天吃人會感到不適。
presentError(ErrorOptions(message: "Oh noes! I didn't get to eat the Human!", size: CGSize(width: 1000.0, height: 200.0))) //Woohoo! 沒有參數(shù)了!我們現(xiàn)在有默認實現(xiàn)了!
}
}
如你所見,對于用view controller
做error處理,我們給與它了一種完全抽象的,可伸縮的和模塊化的方式,還不用強迫所有的view controller
去繼承一個上帝類。當你有一個具有不同功能的上帝類的時候,上面的例子尤其能幫到你。除此之外,用這種方式去實現(xiàn)類似上面error功能的其他功能時,你把實現(xiàn)該功能的代碼放哪兒都行,不必做太多的重構(gòu)或者改變代碼框架。
函數(shù)式編程
咱們來解決這個。我也剛開始接觸函數(shù)式編程,不過我知道一點:這種范式(paradigm)要求一種鼓勵編程者去避免可變數(shù)據(jù)(mutable data)和改變狀態(tài)(changing state)的編程方式。和數(shù)學函數(shù)類似,函數(shù)式編程是由一些輸出結(jié)果僅取決于輸入?yún)?shù)的函數(shù)組成,而且函數(shù)的輸出結(jié)果不會被本體之外的相依性(dependency)所影響。這就是眾所周知的"data in, data out",意思是每次傳進來一個值,這個值傳出去的時候和傳進來時候總要是一樣的。想想單元測試就明白了!
如果我們用函數(shù)式的思想去寫代碼,就可以把VOP與函數(shù)式編程結(jié)合,利用其中的諸多優(yōu)點,這些優(yōu)點包括但不僅限于:
- 完全線程安全的代碼(值類型變量在并發(fā)代碼中被分配時是被拷貝的,意思是另一個線程更改不了與它平行線程中的變量)。
- 更詳盡的單元測試
- 不再需要在單元測試中用mock(用了值類型的變量就不用再重建一個必須使用mock對象的環(huán)境,只為了去測試僅僅少部分的功能。本質(zhì)上通過初始化一個從任意依賴關系中抽象出來的特性,你可以重建任何你想要的東西。)
- 代碼更簡潔(說實話,能和瓷器一樣精致)。
- 讓你身邊的小伙伴驚呆
- 很炫酷
- 讓Kraken瘋狂的崇拜你
什么時候用子類
什么時候應該用子類呢?答案是當你沒選擇的時候。比如:
- 當系統(tǒng)要求的時候。許多Cocoa的API要求你使用class,你不應該非要用值類型來跟系統(tǒng)對著干。
UIViewController
是要派生子類的,要不然你的app就啥都沒有了。別跟系統(tǒng)對著干! - 當你需要有東西來幫你管理在其他class實例之間的值類型變量,而且還需要與這些值類型變量通信的時候。對于這種情況Andy Matuschak給了一個很好的例子:用一個class把一個值類型的繪圖系統(tǒng)計算好的值取過來,傳遞給一個Cocoa的class來把這個繪圖系統(tǒng)繪制到屏幕上。
- 當你需要或者想在許多享有者之間做隱式共享的時候。此種情況的例子是Core Data。數(shù)據(jù)持久化變幻無常,用Core Data的時候,使用子類給諸多需要同步的享有者做同步就很有效。但是要小心并發(fā)問題!這是你處理此類問題的時候必須要做的取舍。
- 當你不知道對于引用類型來說它的拷貝意味著什么的時候。你會拷貝一個單例么(singleton)?不會。你會拷貝一個
UIViewController
么?不會。一個window
?絕對不會。(你其實可以,這是你的特權(quán)。) - 當一個實例的聲明周期與外部效應(external effect)綁定的時候,或者就只是需要一個穩(wěn)定個體(stable identity)的時候。單例就是特別典型的例子。
結(jié)論
作為OOP的編程者我們已經(jīng)習慣了用class來解決問題。長期以來我們開發(fā)了很多模式來彌補引用類型所帶來的弊端。我的觀點是在編程中換一種思路可以有效的減輕對這類折衷方案的使用。如果我們真的重視可伸縮性和可重用性,就得接受模塊化的編程才是正道。使用值類型并結(jié)合Swift 2.0中新增并改進了的protocol特性就會輕松的達到這個目的。雖然之前OOP的思維方式會使我們比較難用VOP和POP的方式來思考,但是在swift中寫的多了,VOP和POP的模式就會開始成為我們的第二天性。我們的大腦可能得需要我們多寫一些代碼才能適應這種思維方式,但我相信iOS社區(qū)作為一個整體能接納這些做法,從而極大的降低我們?nèi)粘=鉀Q問題的難度。Swift的核心是一個極為強大的值類型系統(tǒng),坦白說,我們應該一開始就用VOP的思想磨練自己來發(fā)揚這個值系統(tǒng)的優(yōu)勢。但愿這篇文章能多多少少的幫助到你,讓你每天寫出來更加詳盡的,天生安全的代碼。