不要用子類!Swift的核心是面向協(xié)議

本文轉(zhuǎn)自:http://www.cocoachina.com/swift/20150803/12881.html
原作者:Hector Matos
原發(fā)表日期:2015-07-13

<b>本文的代碼示例改用swift3.0际邻,原文使用的是swift2.0</b>

<h2>Swift的核心</h2>
我們可以通過等式的傳遞性來理解swift:
Swift 的核心是面向協(xié)議的編程。
面向協(xié)議的編程的核心是抽象和簡化芍阎。
所有swift的核心就是抽象和簡化

<small>
你可能對我的標(biāo)題感到詫異世曾。我并不是說子類沒有價(jià)值,尤其在使用單一繼承的情況下谴咸,類和子類當(dāng)然是強(qiáng)有力的工具轮听。然而我想說的是骗露,<b>iOS日常開發(fā)的問題是對類和繼承的過度使用</b>。作為面向?qū)ο蟮木幊陶?object-oriented programmer血巍,后面統(tǒng)一換為OOP編程者)我們總是會(huì)自然的去傾向于引用類型和類去解決問題萧锉,但是我個(gè)人還是認(rèn)為應(yīng)該反過來,傾向于<b>用值類型來代替引用類型</b>述寡。我們還是要去寫模塊化的驹暑,可伸縮的并且可重用的代碼,這一點(diǎn)不會(huì)變辨赐。<b>swift 中有強(qiáng)大的值類型就可以幫我們實(shí)現(xiàn)模塊化這一目的优俘,且不會(huì)對引用類型有過度依賴。</b>我認(rèn)為不僅面向協(xié)議的編程 (protocol oriented programming掀序,后面統(tǒng)一為POP) 可以幫我們實(shí)現(xiàn)這點(diǎn)帆焕,另外兩種類型也可以,且都具有抽象和簡化的核心思想不恭,這兩種分別是:面向值的編程和 函數(shù)式編程

先說清楚叶雹,我絕不是這種編程類型 (POP,面向值的編程 和 函數(shù)式編程) 的專家换吧。和你一樣折晦,從MMM時(shí)代(收到內(nèi)存管理)開始我就是一個(gè)OOP編程者。通過自學(xué)沾瓦,從開始就很重視值的抽象和簡化思想满着。我都沒有意識到自己是一個(gè)傾向于函數(shù)式編程的OOP編程者,而且很多時(shí)候都是在用面向值的編程和POP的思路贯莺。這可能是我為什么在第一天就興高采烈的加入了swift的浪潮之中的原因风喇。在WWDC的一整周里,swift的核心理念與我認(rèn)為的該怎樣去編程是如此之契合缕探,這個(gè)感受一直充斥在我腦海中魂莫。通過這篇文章,我希望能幫助你(OOP的編程者)打開思路爹耗,去考慮該如何用更加Non-OOP(非OOP)的方式去解決問題耙考。
</small>

<h2>OOP的問題(和我不得不學(xué)它的原因)</h2>
我會(huì)是第一個(gè)跳出來說的:不用OOP的話做出iOS應(yīng)用很難。Cocoa的核心就是OOP潭兽。沒有OOP的話你根本寫不出來一個(gè)iOS應(yīng)用倦始。有時(shí)候我會(huì)幻想這不是真的。如果你有不同觀點(diǎn)讼溺,趕快證明我是錯(cuò)的吧楣号。我真的需要這樣最易,求你了怒坯,證明我是錯(cuò)的吧炫狱!

不管怎么樣,你總會(huì)遇到必須用對象剔猿、用引用類型解決問題的時(shí)候视译,然后由于Cocoa的規(guī)定而被迫使用類(classes)。這種情況下你碰到的問題都是我們大家熟知并熱愛的

<small>

  • 傳遞class的實(shí)例這個(gè)做法好像總是有種不可思議的能力:你想用一個(gè)實(shí)例的時(shí)候讓這個(gè)實(shí)例的狀態(tài)和你所期望的不一樣归敬。<b>這是由于可變狀態(tài)導(dǎo)致酷含,你這個(gè)對象的另一個(gè)享有者在它覺得合理的地方改變此對象的屬性。</b>
  • 如果不用多繼承的話汪茧,從一個(gè)很棒的class派生出子類從而獲得它的擴(kuò)展功能椅亚,妨礙了你使用另外一些class的更多功能,而且還增加了復(fù)雜性舱污。(舉個(gè)例子來說呀舔,把兩個(gè)UITextField的子類結(jié)合起來生成一個(gè)擁有這兩者功能的UITextField子類,難)
  • 上面一條的另外一個(gè)問題是會(huì)引出意外行為扩灯。如果你遇到了類似上面一條所描述的情況媚赖,你就陷入到了一個(gè)依賴問題中:你連接了兩個(gè)superclass各自特性,對其中一個(gè)superclass的移除改動(dòng)可能會(huì)給另外一個(gè)superclass代理不良影響珠插。這就是class之間緊耦合所帶來的問題
  • 單元測試中的mocking惧磺。有些class在系統(tǒng)中的耦合過于緊密,想完全測試這些class就需要你創(chuàng)建每一個(gè)class的假象表捻撑。我都不用告訴你本質(zhì)上你并沒有真正的測試了這個(gè)class磨隘,你不過是在假裝測試它。這里就不提很多Mocking的庫是用運(yùn)行時(shí)的小把戲來造一個(gè)假的class了顾患。
  • 并發(fā)問題琳拭。這和上面提到的可變狀態(tài)是伴隨出現(xiàn)的。你從多個(gè)線程中同時(shí)改變一個(gè)引用就會(huì)引起這個(gè)問題描验,運(yùn)行時(shí)使對象之間的同步發(fā)生異常白嘁。
  • 很容易導(dǎo)致出現(xiàn)像上帝類(God classes - 承擔(dān)著很多subclasses需要的重要高層級代碼的所有責(zé)任),Blobs(有過多職權(quán)的classes)膘流,Lava Flow(因?yàn)楹刑嗟姆欠ùa導(dǎo)致任何人都不敢碰的classes)等等這些種反面模式
    </small>


    ggg

<h2>POP 面向協(xié)議的編程</h2>
陷入OOP的反面模式特別容易絮缅。多半時(shí)間我們(包括我)就是太懶而不愿意去點(diǎn)File>New File。結(jié)果是在現(xiàn)有class的基礎(chǔ)上添加一個(gè)函數(shù)是如此輕松呼股,我們就不愿意從零開始建一個(gè)新的class了耕魄。如果你一直這么干,而且一直非常懶的從一個(gè)"很重要"的class派生subclass的話彭谁,你就把上帝類/死星類給弄出來了吸奴。實(shí)際上我之前就這么干過:我給一個(gè)app里的每個(gè)view Controller都加了能呈現(xiàn)一個(gè)指向navigationController的navigationBar的error view的功能。唉,我可真蠢则奥。直到要改動(dòng)那個(gè)Error上帝類行為的時(shí)候考润,我不得不把整個(gè)app都改一遍。這不是聰明的做法读处,你真應(yīng)該看看那些bug糊治。

如果使用了POP,這個(gè)Error上帝類很大程度上就能很容易的抽象出來罚舱,以后改進(jìn)它也方便井辜。

這是一個(gè)能展示(之前的方式)有多殘暴的例子:

class PresentErrorViewController: UIViewController {
    var errorViewIsShowing: Bool = false
    func presentError(message: String = "Error!", withArrow shouldShowArrow: Bool = false, backgroundColor: UIColor = UIColor.red, withSize size: CGSize = CGSize.zero, canDismissByTappingAnyWhere canDismiss: Bool = true) {
        // 寫下了復(fù)雜的,脆弱的代碼
    }
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

隨著項(xiàng)目的進(jìn)行管闷,事情馬上變的明了:并不是每一個(gè)UIViewController需要這個(gè)error邏輯粥脚,或者真的需要這個(gè)class 所提供的每一個(gè)功能。我們團(tuán)隊(duì)里任何一個(gè)人都可以請用的在這個(gè)superclass里改點(diǎn)什么包个,從而影響整個(gè)app阿逃。這就讓代碼變得很脆弱。還是代碼呈現(xiàn)出多態(tài)赃蛛。本應(yīng)該有子類覺得它自己的行為恃锉,這里的superclass卻給幫著決定了。下面是在swift3.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具有一個(gè)presentError的默認(rèn)實(shí)現(xiàn)
    func presentError(message: String, withArrow shouldShowArrow: Bool, backgroundColor: UIColor, withSize size: CGSize, canDismissByTappingAnywhere canDismiss: Bool) {
        // 加上呈現(xiàn)error視圖的默認(rèn)實(shí)現(xiàn)
    }
}

class KrakenViewController:UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    func methodThatHasAnError() {
        // ...
        // 拋出error呕臂,原因是Kraken海妖今天吃人會(huì)感到不適破托。
        presentError(message: "", withArrow: true, backgroundColor: UIColor.red, withSize: CGSize.zero, canDismissByTappingAnywhere: true)
        
    }
}

看,這里發(fā)生了很炫酷的事情歧蒋。我們不僅消除了上帝類存在土砂,還讓代碼更加的模塊化并增強(qiáng)了它的擴(kuò)展性。通過創(chuàng)建一個(gè) ErrorPopoverRenderer協(xié)議谜洽,就會(huì)讓任何則遵循了該協(xié)議的class具有呈現(xiàn)出一個(gè)ErrorView的能力萝映。還不止這些,我們的KrakenViewController class 不用必須實(shí)現(xiàn)presentError這個(gè)函數(shù)阐虚,因?yàn)槲覀償U(kuò)展了UIViewController序臂,讓它提供了一個(gè)默認(rèn)實(shí)現(xiàn)。

唉不過等下实束!這有個(gè)問題奥秆!我們每次想要呈現(xiàn)一個(gè)ErrorView的時(shí)候都不想要去實(shí)現(xiàn)每一個(gè)參數(shù)。這就有點(diǎn)兒讓人不爽了咸灿,因?yàn)槲覀儾荒茉賞rotocol協(xié)議函數(shù)聲明中為參數(shù)提供默認(rèn)值构订。

我還挺喜歡這些參數(shù)的!更槽糕的是在讓買賣根據(jù)模塊化特征的過程中我們引入了復(fù)雜度避矢。還是繼續(xù)吧悼瘾,用swift3.0中新加的一個(gè)小妙招來多少的補(bǔ)償一下:

protocol ErrorPopoverRenderer {
    func presentError()
}

extension ErrorPopoverRenderer where Self: UIViewController {
    func presentError() {
        // 在這里加默認(rèn)實(shí)現(xiàn)囊榜,并提供ErrorView的默認(rèn)參數(shù)。
    }
}
class KrakenViewController: UIViewController, ErrorPopoverRenderer {
    func methodThatHasAnError() {
        //...
        // 拋出error亥宿,原因是Kraken海妖今天吃人會(huì)感到不適
        presentError()
    }
}

好了卸勺,現(xiàn)在看起來已經(jīng)很不錯(cuò)了。我們不僅消除了這些煩人的參數(shù)箩绍,還用swift3.0的新特性在protocol的層級上用Self給了presentError一個(gè)默認(rèn)實(shí)現(xiàn)。用Self意味著當(dāng)且晉檔協(xié)議的遵循者是繼承自UIViewController的情況下尺上,這個(gè)擴(kuò)展才會(huì)有效材蛛。這就讓我們能夠把ErrorPopoverRenderer真的當(dāng)做是一個(gè)UIViewController,而不需要對后者做擴(kuò)展怎抛!更棒的是卑吭,從現(xiàn)在開始,Swift的運(yùn)行時(shí)是以靜態(tài)調(diào)度而非動(dòng)態(tài)調(diào)度去調(diào)用presentError()方法马绝。大致的意思就是我們在函數(shù)調(diào)用點(diǎn)給presentError()方法增強(qiáng)了一點(diǎn)性能豆赏。

唉,不過還是有個(gè)問題富稻。到這里我們POP的旅途暫時(shí)告一段落掷邦,但對于它的完善依舊不會(huì)停止。我們的問題是如果只想對一部分參數(shù)使用默認(rèn)值椭赋,對生效的不用默認(rèn)值該怎么做抚岗?在這方面POP的話基本幫不上什么忙,但是我們可以尋求另外一種方法∧恼現(xiàn)在宣蔚,我們使用面向值的編程(VOP)吧。

<h2>面向值的編程(Value-oriented-programming)</h2>
看到了吧认境,POP和VOP總是伴隨出現(xiàn)胚委。在WWDC視頻中,Crusty提出了一下大膽的論斷:我們用struct 和 enum 類型就可以做到一切class能做到的事叉信。我很大程度上同意這點(diǎn)亩冬,但是沒這么極端。依我看硼身,protocol本質(zhì)上是吧VOP粘合在一起的膠水鉴未,這點(diǎn)我和Crusty吃相同太大。實(shí)際上既然我們說的了Swift的核心理念以及VOP鸠姨,我想給你們看看從Andy Matuschak的精彩訪談中關(guān)于Swift中的VOP

的話題里面摘出來的一張極好的圖:


2.png

能看出來Swift的標(biāo)準(zhǔn)庫中铜秆,僅有的4個(gè)class,和余下的95個(gè)struct和enum的實(shí)例共同構(gòu)建了Swift功能的核心讶迁。

Andy如此闡述道:用Swift編程的時(shí)候我們要考慮用一層很薄的對象層连茧,和一個(gè)很厚的值類型層核蘸。Class是有它們的地方啸驯,但是我想盡最大程度的去認(rèn)為它們的位置只應(yīng)該處于對象層中的一個(gè)很高的級別上客扎,在這里通過操縱值類型層中的邏輯來管理各種行為。

"把邏輯和行為分開"——Andy Matuschak

和你所了解的一樣罚斗,值類型被賦給一個(gè)變量或者常量徙鱼,抑或是傳給函數(shù)做參數(shù)時(shí)是它的值被拷貝的。這就讓值類型在任何時(shí)候只有一個(gè)享有者针姿,從而降低復(fù)雜度袱吆。和引用類型相反,在賦值過程中引用類型會(huì)有很多享有者距淫,其中一部分你甚至都沒意識到绞绒。在任何時(shí)間點(diǎn)使用引用的話會(huì)帶來一些副作用:引用的享有者會(huì)搗蛋,在背后偷偷改變這個(gè)引用榕暇。Class = 高復(fù)雜度蓬衡,值 = 低復(fù)雜度。

通過利用值類型的簡約特性彤枢,咱們實(shí)現(xiàn)一下之前提過的默認(rèn)參數(shù)的設(shè)計(jì)吧狰晚。我們用的是 Brian Gesiak的value options paradigm方法:

struct ErrorOptions {
    let message: String
    let showArrow: Bool
    let size: CGSize
    let candismissByTap: Bool
    let backgroundColor: UIColor
    init(message: String = "Error", shouldShowArrow: Bool = true, backgroundColor: UIColor = UIColor.red, size: CGSize = CGSize.zero, canDismissByTappingAnywhere canDismiss: Bool = true) {
        self.message = message
        self.backgroundColor = backgroundColor
        self.size = size
        self.candismissByTap = canDismiss
        self.showArrow = shouldShowArrow
    }
    
}

使用上面的選項(xiàng)型struct(是值類型!)就使我們的POP帶上了一些VOP的色彩缴啡,如下:


protocol ErrorPopoverRenderer {
    func presentError(_ errorOptions: ErrorOptions)
}

extension ErrorPopoverRenderer where Self: UIViewController {
    func presentError(_ errorOptions: ErrorOptions) {
        // 在這里加默認(rèn)實(shí)現(xiàn)家肯,并提供ErrorView的默認(rèn)參數(shù)。
    }
}
class KrakenViewController: UIViewController, ErrorPopoverRenderer {
    func methodThatHasAnError() {
        //...
        // 拋出error盟猖,原因是Kraken海妖今天吃人會(huì)感到不適
        presentError(ErrorOptions(message: "Oh noes! I didn't get to eat the Human!", size: CGSize(width: 1000.0, height: 20)))
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末讨衣,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子式镐,更是在濱河造成了極大的恐慌反镇,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件娘汞,死亡現(xiàn)場離奇詭異歹茶,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)你弦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進(jìn)店門惊豺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人禽作,你說我怎么就攤上這事尸昧。” “怎么了旷偿?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵烹俗,是天一觀的道長爆侣。 經(jīng)常有香客問我,道長幢妄,這世上最難降的妖魔是什么兔仰? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮蕉鸳,結(jié)果婚禮上乎赴,老公的妹妹穿的比我還像新娘。我一直安慰自己潮尝,他們只是感情好榕吼,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著衍锚,像睡著了一般友题。 火紅的嫁衣襯著肌膚如雪嗤堰。 梳的紋絲不亂的頭發(fā)上戴质,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天,我揣著相機(jī)與錄音踢匣,去河邊找鬼告匠。 笑死,一個(gè)胖子當(dāng)著我的面吹牛离唬,可吹牛的內(nèi)容都是我干的后专。 我是一名探鬼主播,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼输莺,長吁一口氣:“原來是場噩夢啊……” “哼戚哎!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起嫂用,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤型凳,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后嘱函,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體甘畅,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年往弓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了疏唾。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,001評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡函似,死狀恐怖槐脏,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情撇寞,我是刑警寧澤准给,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布泄朴,位于F島的核電站,受9級特大地震影響露氮,放射性物質(zhì)發(fā)生泄漏祖灰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一畔规、第九天 我趴在偏房一處隱蔽的房頂上張望局扶。 院中可真熱鬧,春花似錦叁扫、人聲如沸三妈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽畴蒲。三九已至,卻和暖如春对室,著一層夾襖步出監(jiān)牢的瞬間模燥,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工掩宜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蔫骂,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓牺汤,卻偏偏與公主長得像辽旋,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子檐迟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評論 2 355

推薦閱讀更多精彩內(nèi)容