Swift 中的面向協議程式設計:是否優於物件導向程式設計?

SwiftGG翻譯組發表於2018-12-03

作者:Andrew Jaffee,原文連結,原文日期:2018/03/28 譯者:陽仔;校對:numbbbbbLision;定稿:Forelax

在本文中,我們將深入討論 Swift 4 中的面向協議程式設計。這是一個系列兩篇文章中的第二篇。如果你還沒有讀過 前一篇介紹文章,請在繼續閱讀本文之前先閱讀前一篇。

在本文中,我們將探討為什麼 Swift 被認為是一門“面向協議”的語言;對比面向協議程式設計(POP)和麵向物件程式設計(OOP);對比“值語義”和“引用語義”;討論 local reasoning;用協議實現代理模式;用協議代替型別;使用協議多型性;重審我的面向協議的實際程式碼;最終討論為什麼我沒有 100% 使用 POP 程式設計。

關於 WWDC 連結的一點說明 在這一系列關於 POP 的兩篇文章中,我至少新增了三個 Apple Worldwide Developers Conference (WWDC) 視訊的連結。在 Safari 中點選這些連結將直接跳轉至視訊中的具體小節(並往往會從該處開始播放視訊)。如果你使用的不是 Safari,則需要瀏覽視訊,手動跳轉至某一小節,或者檢視該視訊的文字版本。

Swift 為什麼是“面向協議”的語言?

我在 前一篇 POP 介紹文章 中,提到 Apple 聲稱”從核心上說,Swift 是面向協議的”。相信我,確實是這樣的。為什麼呢?在回答這個問題之前,讓我們先來比較幾種程式語言。

我們最好對其他語言也有所瞭解,因為在某些情況下這將會有用處,比如在需要將 C++ 庫連結到 iOS 應用中的時候。我的很多 iOS 和 OSX 的應用連結了 C++ 的庫,因為這些應用有 Windows 平臺的版本。這些年,我支援了許多“平臺無關”的應用。

OOP 語言早已支援了介面。介面和 Swift 中的協議很相似,但並不是完全一樣。

這些語言中的介面指定了遵循該介面的類和(或)結構體必須實現哪些方法和(或)屬性。我這裡使用了“和(或)”,是因為比如 C++ 中沒有介面的概念,而是使用抽象類。並且,一個 C++ struct 可以繼承自一個類。C# 中的介面允許指定其中的屬性和方法,struct 可以遵循介面。Objective-C 中稱“協議”而不是“介面”,協議也可以指定要求實現的方法和屬性,但只有類可以宣告遵循介面,struct 不可以。

這些介面和 Objective-C 中的協議,並沒有方法的具體實現。它們只是指定一些要求,作為遵循該協議的類/結構體實現時的“藍圖”。

協議構成了 Swift 標準庫 的基礎。正如我在 第一篇文章 中所展示,協議是 POP 方法論和正規化的關鍵所在。

Swift 中的協議有其他語言都不支援的特點:協議擴充套件。以下摘自 Apple 官方描述:

協議可以被擴充套件,來給遵循該協議的型別提供方法、初始化方法、下標、計算屬性的具體實現。這就可以允許協議自身定義一些行為,而不是由各個型別自己去實現,或是由一個全域性方法來實現。 通過擴充套件,我們可以為協議所要求的任何方法和計算屬性提供一個預設的實現。如果一個遵循該協議的型別為某個方法或屬性提供了其自己的實現,那麼該實現將會替代協議擴充套件中的實現。

在上一篇文章中,你已經看到我是怎麼使用協議擴充套件的,在本文中你會再次看到。它們是使得 Swift POP 如此強大的祕訣。

在 Swift 出現之前,協議在 iOS 中就已經十分重要。還記得我對 iOS 開發人員多年來採用的 UITableViewDataSource 和 UITableViewDelegate 等協議的 討論 嗎?再想一想你每天寫的 Swift 程式碼吧。

用 Swift 程式設計的時候,不可能不利用 標準庫 中的協議。例如,Array (一個繼承了 10 個協議struct),Bool (一個繼承了 7 個協議struct),Comparable (一個 繼承自另一個協議的協議,並且是很多其他 Swift 型別的繼承先祖),以及 Equatable (一個 很多 Swift 協議和型別的繼承先祖)。

花一些時間閱覽 Swift 標準庫,跟隨連結檢視所有型別、協議、操作符、全域性變數、函式。一定要看幾乎所有頁面都會有的 “Inheritance” 一節,並點選 “VIEW PROTOCOL HIERARCHY ->” 連結。你將會看到很多協議,協議的定義,以及協議繼承關係的圖表。

記住很重要的一點:大部分 iOS(以及 OSX)SDK 中的程式碼都是以類繼承的層次結構實現的。我相信很多我們使用的核心框架仍然是用 Objective-C(以及一些 C++ 和 C)編寫的,例如 FundationUIKit。拿 UIKit 中的 UIbutton 舉例。利用 Apple 官方文件頁面中的“繼承自”連結,我們可以從葉節點 UIButton 一直沿著繼承鏈向上查詢到根節點 NSObjectUIButtonUIControlUIViewUIResponderNSObject。可以形象表示為:

Swift 中的面向協議程式設計:是否優於物件導向程式設計?

POP 和 OOP

OOP 的優點已經被開發者們討論得很多了,所以在這裡我只想簡單列舉一下。如果想了解詳盡的內容,可以參考 我寫的這篇有關 OOP 在 Swift 中的實現的具體介紹

注意:如果你讀這篇文章的時候還不瞭解 OOP,我建議你在考慮學習 POP 之前,先學習 OOP。

OOP 的優點包括可重用性,繼承,可維護性,對複雜性的隱藏(封裝),抽象性,多型性,對一個類的屬性和方法的訪問許可權控制。我這裡可能還有所遺漏,因為開發者們已經總結出太多 OOP 的優點了。

簡單地說,OOP 和 POP 都擁有大部分上述的特點,主要的一點不同在於:類只能繼承自其它一個類,但協議可以繼承自多個協議。

正是由於 OOP 的繼承的特點,我們在開發中最好把繼承關係限制為單繼承。因為多繼承會使程式碼很快變得一團亂。

然而,協議卻可以繼承自一個或多個不同的協議。

為什麼需要推動面向協議程式設計呢?當我們建立起一個龐大的類層次結構的時候,許多的屬性和方法會被繼承。開發者更傾向於把一些通用功能增加到頂層的——主要是高層的父類中(並且會一直加下去)。中層和底層的類的職責會更加明確和具體。新的功能會被放到父類中,這經常會使得父類充滿了許多額外的,無關的職責,變得“被汙染”或是“臃腫”。中層和底層的類也因此繼承了很多它們並不需要的功能。

這些有關 OOP 的擔憂並非成文的規定。一個優秀的開發者可以躲避很多剛才提到的陷阱。這需要時間、實踐和經驗。例如,開發者可以這樣解決父類功能臃腫的問題:將其他類的例項新增為當前類的成員變數,而非繼承這些類(也就是使用組合代替繼承)。

在 Swift 中使用 POP 還有一個好處:不僅僅是類,值型別也可以遵循協議,比如 structenum。我們在下面將會討論使用值型別的一些優點。

但我的確對遵循多協議的做法有一些顧慮。我們是否只是將程式碼的複雜性和難度轉移成另一種形式了呢?即,將 OOP 繼承中的“垂直”的複雜性轉移成了 POP 繼承中的“水平”的複雜性了呢?

將之前展示的 UIButton 的類繼承結構和 Swift 中的 Array 所遵循的協議進行對比:

Swift 中的面向協議程式設計:是否優於物件導向程式設計?

影象來源:swiftdoc.org/v3.1/type/A…

Local reasoning 對這兩種情況都不適用,因為個體和關係太多了。

值語義 vs. 引用語義

正如我上一篇文章所提到的,Apple 正在大力推廣 POP 和值語義的相關概念(他們還正在推廣另一個與 POP 相關的正規化,下文會講到)。上一次,我向你們展示了程式碼,這次依然會用程式碼來明確展示“引用語義”和“值語義”的不同意義。請參閱我 上一週的文章 中的 ObjectThatFlies 協議,以及今天文章中的 ListFIFOLIFO 以及相關協議。

Apple 工程師 Alex 說我們 “應當使用值型別和協議來讓應用變得更好”。Apple sample playground 中,一節題為“理解值型別”的程式碼文件這麼說:

標準庫中的序列和容器使用了值型別,這讓寫程式碼變得更加容易。每一個變數都有一個獨立的值,對這個值的引用並非共享的。例如,當你向一個函式傳遞一個陣列,這個函式並不會導致呼叫方對這個陣列的拷貝突然被改變。

這當然對所有使用值語義的資料型別都是適用的。我強烈建議你下載並完整閱覽整個 playground。

我並不是要拋棄類這個使用引用語義的概念。我做不到。我自己已經寫了太多的基於類的程式碼。我幫助我的客戶整理了數百萬行基於類的程式碼。我同意值型別一般來說比引用型別安全。當我寫新的程式碼,或是重構已有程式碼的時候,我會考慮在某些個案中積極嘗試。

引用語義下,類例項(引用)會導致 “意料之外的資料共享”。也有人稱之為“意料之外的改變”。有一些 方法 可以最小化引用語義的副作用,不過我還是會越來越多地使用值語義。

值語義能夠使變數避免受到無法預計的更改,這實在很棒。因為“每個變數有一個獨立的值,對這個值的引用是不共享的“,我們能夠避免這種無法預計的更改導致的副作用。

因為 Swift 中的 struct 是一種值型別,並且能夠遵循協議,蘋果也在大力推進 POP 以取代 OOP,在 面向協議和值程式設計 你可以找到這背後的原因。

Local reasoning

讓我們探討一個很棒的主題,Apple 稱之為 “Local reasoning”。這是由 Apple 一位叫 Alex 的工程師在 WWDC 2016 - Session 419,“UIKit 應用中的面向協議和值程式設計”中提出的。這也是 Apple 與 POP 同時大力推動的概念。

我認為這不是個新鮮的概念。許多年以前,教授、同事、導師、開發者們都在討論這些:永遠不要寫高度超過一個螢幕的函式(即不長於一頁,或許更短);將大的函式拆解成若干小的函式;將大的程式碼檔案拆解成若干小的程式碼檔案;使用有意義的變數名;在寫程式碼之前花點時間去設計程式碼;保持空格和縮排風格的一致性;將相關的屬性和行為整合成類和/或結構體;將相關的類和/或結構體整合進框架或庫中。但 Apple 在解釋 POP 的時候,正式提出了這個概念。Alex 告訴我們:

Local reasoning 意味著,當你看你面前的程式碼的時候,你不需要去思考,剩下的程式碼怎樣去和這個函式進行互動。也許你之前已經有過這種感覺。例如,當你剛加入一個新的團隊,有大量的程式碼要去看,同時上下文的資訊也非常匱乏,你能明白某一個函式的作用嗎?做到 Local reasoning 的能力很重要,因為這能夠使得維護程式碼、編寫程式碼、測試程式碼變得更加容易。

哈哈,你曾經有過這種感覺嗎?我曾經讀過一些其他人寫的真的很好的程式碼。我也曾經寫過一些易讀性非常好的程式碼。說實話,在 30 年的工作經驗中,我要去支援和升級的絕大部分現存的程式碼都不會讓我感受到 Alex 所描述的這種感覺。相反,我經常會變得非常困惑,因為當我看一段程式碼的時候,我往往對這段程式碼的作用毫無頭緒。

Swift 語言的原始碼是開源的。請快速瀏覽一遍 下列函式,也不要花上三個小時去試圖理解它:

public mutating func next() -> Any? {
    if index + 1 > count {
        index = 0
	// 確保沒有 self 的成員變數被捕獲
        let enumeratedObject = enumerable
        var localState = state
        var localObjects = objects
        
        (count, useObjectsBuffer) = withUnsafeMutablePointer(to: &localObjects) {
            let buffer = AutoreleasingUnsafeMutablePointer<AnyObject?>($0)
            return withUnsafeMutablePointer(to: &localState) { (statePtr: UnsafeMutablePointer<NSFastEnumerationState>) -> (Int, Bool) in
                let result = enumeratedObject.countByEnumerating(with: statePtr, objects: buffer, count: 16)
                if statePtr.pointee.itemsPtr == buffer {
		    // 大多數 cocoa 類會返回它們自己的內部指標快取,不使用預設的路徑獲取值。也有例外的情況,比如 NSDictionary 和 NSSet。
		    return (result, true)
                } else {
                    // 這裡是通常情形,比如 NSArray。
		    return (result, false)
                }
            }
        }
        
        state = localState // 重置 state 的值
        objects = localObjects // 將物件指標拷貝回 self 
        
        if count == 0 { return nil }
    }
    defer { index += 1 }
    if !useObjectsBuffer {
        return state.itemsPtr![index]
    } else {
        switch index {
        case 0: return objects.0!.takeUnretainedValue()
        case 1: return objects.1!.takeUnretainedValue()
        case 2: return objects.2!.takeUnretainedValue()
        case 3: return objects.3!.takeUnretainedValue()
        case 4: return objects.4!.takeUnretainedValue()
        case 5: return objects.5!.takeUnretainedValue()
        case 6: return objects.6!.takeUnretainedValue()
        case 7: return objects.7!.takeUnretainedValue()
        case 8: return objects.8!.takeUnretainedValue()
        case 9: return objects.9!.takeUnretainedValue()
        case 10: return objects.10!.takeUnretainedValue()
        case 11: return objects.11!.takeUnretainedValue()
        case 12: return objects.12!.takeUnretainedValue()
        case 13: return objects.13!.takeUnretainedValue()
        case 14: return objects.14!.takeUnretainedValue()
        case 15: return objects.15!.takeUnretainedValue()
        default: fatalError("Access beyond storage buffer")
        }
    }
}
複製程式碼

在你瀏覽過一遍之後,說實話,你能理解這段程式碼嗎?我並沒有。我不得不花些時間多讀幾遍,並查閱函式定義之類的程式碼。以我的經驗,這種程式碼是普遍存在的,並且不可避免需要經常修補的。

現在,讓我們考慮理解一種 Swift 型別(不是一個函式)。檢視 Swift 中的 Array定義。我的天,它繼承了 10 個協議:

  • BidirectionalCollection
  • Collection
  • CustomDebugStringConvertible
  • CustomReflectable
  • CustomStringConvertible
  • ExpressibleByArrayLiteral
  • MutableCollection
  • RandomAccessCollection
  • RangeReplaceableCollection
  • Sequence

點選下方的“VIEW PROTOCOL HIERARCHY ->”連結按鈕——天哪,看這一坨麵條一樣的線條

如果你是在開發一個新專案,並且整個團隊能夠遵循一套最佳開發指導方案的話,要做到 Local reasoning 會容易很多。少量程式碼的重構也是做到 local reasoning 的較好的機會。對我來說,像其他大部分事情一樣,程式碼的重構需要慎重和仔細,要做到適度。

牢記:你幾乎一直要面對非常複雜的業務邏輯,這些邏輯如果寫成程式碼,並且要讓一個團隊新人流暢讀懂,需要他/她接收一些業務知識的訓練和指導。他/她很可能需要查詢一些函式、類、結構體、列舉值、變數的定義。

代理和協議

代理模式是 iOS 中廣泛使用的模式,其中一個必需的組成部分就是協議。在這裡我們不需要再去重複。你可以閱讀我有關該主題的 AppCoda 部落格

協議型別以及協議多型性

在這些主題上我不準備花太多時間。我已經講過很多有關協議的知識,並向你展示了大量程式碼。作為任務,我想讓你自己研究一下,Swift 協議型別(就像在代理中一樣)的重要性,它們能給我們帶來的靈活性,以及它們所展示的多型性。

協議型別 在我 關於代理的文章 中,我定義了一個屬性:

var delegate: LogoDownloaderDelegate?
複製程式碼

其中 LogoDownloaderDelegate 是一個協議。然後,我呼叫了這個協議的一個方法。

協議多型性 正如在物件導向中一樣,我們可以通過遵循父協議的資料型別,來與多種遵循同一個協議族的子協議的資料型別進行互動。用程式碼舉例來說明:

protocol Top {
    var protocolName: String { get }
}

protocol Middle: Top {

}

protocol Bottom: Middle {

}

struct TopStruct : Top {
    var protocolName: String = "TopStruct"
}

struct MiddleStruct : Middle {
    var protocolName: String = "MiddleStruct"
}

struct BottomStruct : Bottom {
    var protocolName: String = "BottomStruct"
}

let top = TopStruct()
let middle = MiddleStruct()
let bottom = BottomStruct()

var topStruct: Top
topStruct = bottom
print("\(topStruct)\n")
// 輸出 "BottomStruct(protocolName: "BottomStruct")"

topStruct = middle
print("\(topStruct)\n")
// 輸出 "MiddleStruct(protocolName: "MiddleStruct")"

topStruct = top
print("\(topStruct)\n")
// 輸出 "TopStruct(protocolName: "TopStruct")"

let protocolStructs:[Top] = [top,middle,bottom]

for protocolStruct in protocolStructs {
    print("\(protocolStruct)\n")
}
複製程式碼

如果你執行一下 Playground 中的程式碼,以下是終端的輸出結果:

BottomStruct(protocolName: "BottomStruct")

MiddleStruct(protocolName: "MiddleStruct")

TopStruct(protocolName: "TopStruct")

TopStruct(protocolName: "TopStruct")

MiddleStruct(protocolName: "MiddleStruct")

BottomStruct(protocolName: "BottomStruct")
複製程式碼

真實的 UIKit 應用中的協議

現在,讓我們來看一些實質性的東西,寫一些 Swift 4 的程式碼——這些程式碼是在我自己的應用中真實使用的。這些程式碼應當能使你開始思考用協議來構建和/或擴充你的程式碼。這也就是我在這兩篇文章中一直在描述的,“面向協議程式設計”,或者 POP。

我選擇向你展示如何去擴充套件或者說是延伸(隨便哪種說法)UIKit 的類,因為 1) 你很可能非常習慣使用它們 2) 擴充套件 iOS SDK 中的類,比如 UIView,是比用你自己的類更加困難一些的。

所有 UIView 的擴充套件程式碼都是用 Xcode 9 工程中的 Single View App 模板寫的。

我使用預設協議擴充套件來對 UIView 進行擴充套件——這麼做的關鍵是一種 Apple 稱之為 “條件遵循” 的做法(也可以看 這裡)。因為我只想對 UIView 這個類進行擴充套件,我們可以讓編譯器來把這個變成一項強制要求。

我經常使用 UIView 作為一個容器來組織螢幕上的其他 UI 元素。也有時候,我會用這些容器檢視來更好地檢視、感覺、排布我的 UI 檢視。

這裡是一張 GIF 圖片,展示了使用下面建立的三個協議來自定義 UIView 的外觀的結果:

Swift 中的面向協議程式設計:是否優於物件導向程式設計?

注意,這裡我也遵守了 ”Local reasoning“ 的原則。我每一個基於協議的函式都控制在一螢幕之內。我希望你能閱讀每一個函式,因為它們並沒有太多程式碼量,但卻很有效。

為 UIView 新增一個預設的邊框

假設我希望獲得很多擁有相同邊框的 UIView 例項——例如在一個支援顏色主題的應用中那樣。一個這樣的例子就是上面那張圖片中,最上面那個綠色的檢視。

protocol SimpleViewWithBorder {}

// 安全的:"addBorder" 方法只會被新增到 UIView 的例項。
extension SimpleViewWithBorder where Self : UIView {
    func addBorder() -> Void {
        layer.borderColor = UIColor.green.cgColor
        layer.borderWidth = 10.0
    }
}

class SimpleUIViewWithBorder : UIView, SimpleViewWithBorder {
}
複製程式碼

要建立、配置、顯示一個 SimpleUIViewWithBorder 的例項,我在我的 ViewController 子類中的 IBAction 中寫了如下程式碼:

@IBAction func addViewButtonTapped(_ sender: Any) {
    let customFrame0 = CGRect(x: 110, y: 100, width: 100, height: 100)
    let customView0 = SimpleUIViewWithBorder(frame: customFrame0)
    customView0.addBorder()
    self.view.addSubview(customView0)
複製程式碼

我不需要為這個 UIView 的子類去建立一個特殊的初始化方法。

為 UIView 新增一個預設的背景色

假設我希望很多 UIView 的例項都有相同的背景色。一個這樣的例子是上圖中,中間的藍色檢視。注意,我向可配置的 UIView 又更進了一步。

protocol ViewWithBackground {
    var customBackgroundColor: UIColor { get }
}

extension ViewWithBackground where Self : UIView {
    func addBackgroundColor() -> Void {
        backgroundColor = customBackgroundColor
    }
}

class UIViewWithBackground : UIView, ViewWithBackground {
    let customBackgroundColor: UIColor = .blue
}
複製程式碼

要建立、配置、展示一個 UIViewWithBackground 的例項,我在我的 ViewController 子類中的 IBAction 中寫了如下程式碼:

let customFrame1 = CGRect(x: 110, y: 210, width: 100, height: 100)
let customView1 = UIViewWithBackground(frame: customFrame1)
customView1.addBackgroundColor()
self.view.addSubview(customView1)
複製程式碼

我不需要為這個 UIView 的子類去建立一個特殊的初始化方法。

為 UIView 新增一個可配置的邊框顏色

現在,我希望能夠配置 UIView 邊框的顏色和寬度。用下列實現程式碼,我可以隨意建立不同邊框顏色、寬度的檢視。這樣的一個例子是上圖中,最下面的紅色檢視。向我的協議中去新增可配置的屬性有一點代價,我需要能夠初始化這些屬性,因此,我為我的協議新增了一個 init 方法。這意味著,我也可以呼叫 UIView 的初始化方法。讀完程式碼,你就會明白:

protocol ViewWithBorder {
    var borderColor: UIColor { get }
    var borderThickness: CGFloat { get }
    init(borderColor: UIColor, borderThickness: CGFloat, frame: CGRect)
}

extension ViewWithBorder where Self : UIView {
    func addBorder() -> Void {
        layer.borderColor = borderColor.cgColor
        layer.borderWidth = borderThickness
    }
}

class UIViewWithBorder : UIView, ViewWithBorder {
    let borderColor: UIColor
    let borderThickness: CGFloat

    // UIView 的必要初始化方法
    required init(borderColor: UIColor, borderThickness: CGFloat, frame: CGRect) {
        self.borderColor = borderColor
        self.borderThickness = borderThickness
        super.init(frame: frame)
    }

    // UIView 的必要初始化方法
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
複製程式碼

要建立、配置、顯示一個 UIViewWithBorder 的例項,我在我的 ViewController 子類中的 IBAction 中寫了如下程式碼:

    let customFrame2 = CGRect(x: 110, y: 320, width: 100, height: 100)
    let customView2 = UIViewWithBorder(borderColor: .red, borderThickness: 10.0, frame: customFrame2)
    customView2.addBorder()
    self.view.addSubview(customView2)
複製程式碼

我不想做的事

我不想去建立像這樣的程式碼:

extension UIView {
    func addBorder() {  ...  }
    func addBackgroundColor() {  ...  }
}
複製程式碼

這樣也許在一些情況下是有效的,但我感覺這種實現太粗泛了,容易喪失很多細顆粒度的控制。這種實現也容易使得這種構造方法變成 UIView 相關擴充套件方法的垃圾場,換句話說,程式碼容易變得臃腫。隨著方法越來越多,程式碼也變得越來越難以閱讀和維護。

在上述所有基於 UIKit 的協議中,我都使用了 UIView 的子類——引用型別。子類化能夠讓我能直接訪問父類 UIView 中的任何內容,讓我的程式碼清晰、簡短、易讀。如果我使用的是 struct,我的程式碼會變得更加冗長,至於為什麼,留給你們當做練習。

我做的事情

時刻記住,所有這些預設協議 extensions 可以在類擴充套件中覆蓋。用一個例子和圖片來解釋:

protocol SimpleViewWithBorder {}

extension SimpleViewWithBorder where Self : UIView {
    func addBorder() -> Void {
        layer.borderColor = UIColor.green.cgColor
        layer.borderWidth = 10.0
    }
}

class SimpleUIViewWithBorder : UIView, SimpleViewWithBorder {
    // 覆蓋 extension 中的預設實現
    func addBorder() -> Void {
        layer.borderColor = UIColor.darkGray.cgColor
        layer.borderWidth = 20.0
    }
}
複製程式碼

注意我在 SimpleUIViewWithBorder 中的註釋。看下圖中最上面的檢視:

Swift 中的面向協議程式設計:是否優於物件導向程式設計?

真實的,基於協議的泛型資料結構

我非常驕傲在我自己的應用中,我能夠寫儘量少的 POP 程式碼,來建立完整功能的泛型的棧和佇列的資料結構。想了解有關 Swift 中的泛型,請閱讀我 AppCoda 中的 文章

請注意,我使用協議繼承來幫助我利用抽象的 List 協議去建立更加具體的 FIFOLIFO 協議。然後,我利用協議擴充套件來實現 QueueStack 值型別。你可以在下面的 Xcode 9 playground 中看到這些 struct 的例項。

我想向你展示的是如何像 Apple 建議的一樣,通過其他協議來實現自己自定義的協議,因此,我建立了 ListSubscriptListPrintForwardsListPrintBackwardsListCount協議。它們現在還很簡單,但在一個實際的應用中將會展現出其作用。

這種繼承多個其他協議的做法可以讓開發者為現有程式碼增加新的功能,而且不會因為太多額外不相關的功能對程式碼造成”汙染“或”臃腫“。這些協議中,每一個都是獨立的。如果是作為類被新增到繼承層級中葉級以上的話,根據它們所處的位置,這些功能將會至少自動被其他一些類繼承。

關於 POP,我已經講了足夠多來幫助你閱讀和理解程式碼。再給出一個我是如何讓我的資料結構支援泛型的提示:關聯型別的定義

當定義一個協議的時候,有時可以宣告一個或多個關聯型別,作為協議定義的一部分。一個關聯型別提供了一個佔位名,用來表示協議中的一種型別。這個關聯型別真正的資料型別直到該協議被使用的時候才確定。使用 associatedtype 關鍵字來指明一個關聯型別。

程式碼如下:

protocol ListSubscript {
    associatedtype AnyType
    
    var elements : [AnyType] { get }
}

extension ListSubscript {
    subscript(i: Int) -> Any {
        return elements[i]
    }
}

protocol ListPrintForwards {
    associatedtype AnyType

    var elements : [AnyType] { get }
}

extension ListPrintForwards {
    func showList() {
        if elements.count > 0 {
            var line = ""
            var index = 1

                        for element in elements {
                line += "\(element) "
                index += 1
            }
            print("\(line)\n")
        } else {
            print("EMPTY\n")
        }
    }
}

protocol ListPrintBackward {
    associatedtype AnyType

    var elements : [AnyType] { get }
}

extension ListPrintBackwards {
    func showList() {
        if elements.count > 0 {
            var line = ""
            var index = 1

            for element in elements.reversed() {
                line += "\(element) "
                index += 1
            }
            print("\(line)\n")
        } else {
            print("EMPTY\n")
        }
    }
}

protocol ListCount {
    associatedtype AnyType

    var elements : [AnyType] { get }
}

extension ListCount {
    func count() -> Int {
        return elements.count
    }
}

protocol List {
    associatedtype AnyType

    var elements : [AnyType] { get set }

    mutating func remove() -> AnyType

    mutating func add(_ element: AnyType)
}

extension List {
    mutating func add(_ element: AnyType) {
        elements.append(element)
    }
}

protocol FIFO : List, ListCount, ListPrintForwards, ListSubscript {

}

extension FIFO {
    mutating func remove() -> AnyType {
        if elements.count > 0 {
            return elements.removeFirst()
        } else {
            return "******EMPTY******" as! AnyType
        }
    }
}

struct Queue<AnyType>: FIFO {
    var elements: [AnyType] = []
}

var queue = Queue<Any>()
queue.add("Bob")
queue.showList()
queue.add(1)
queue.showList()
queue.add(3.0)
_ = queue[0] // 該下標輸出 "Bob"
_ = queue.count()
queue.showList()
queue.remove()
queue.showList()
queue.remove()
queue.showList()
queue.remove()
queue.showList()
_ = queue.count()

protocol LIFO : List, ListCount, ListPrintBackwards, ListSubscript {
}

extension LIFO {
    mutating func remove() -> AnyType {
        if elements.count > 0 {
            return elements.removeLast()
        } else {
            return "******EMPTY******" as! AnyType
        }
    }    
}

struct Stack<AnyType>: LIFO {
    var elements: [AnyType] = []
}

var stack = Stack<Any>()
stack.add("Bob")
stack.showList()
stack.add(1)
stack.showList()
stack.add(3.0)
_ = stack[0] // 該下標輸出 3
_ = stack.count()
stack.showList()
stack.remove()
stack.showList()
stack.remove()
stack.showList()
stack.remove()
stack.showList()
_ = stack.count()
複製程式碼

這一段程式碼片段在控制檯輸出如下:

Bob

Bob 1

Bob 1 3.0

1 3.0

3.0

EMPTY

Bob

1 Bob

3.0 1 Bob

1 Bob

Bob

EMPTY
複製程式碼

我沒有 100% 使用 POP

在 WWDC 有關 POP 的視訊之一中,一位工程師/講師說 ”在 Swift 中我們有一種說法,不要從一個類開始寫程式碼,從一個協議開始“。嘛~也許吧。這傢伙開始了有關如何使用協議來寫一個二分查詢的冗長的討論。我有點懷疑,這是不是我許多讀者印象最深的部分。看完你失眠了嗎?

這有點像是為了尋找一個 POP 解決方案而人為設計出的一個問題。也許問題是實際的,也許這種解決方案有優點,我也不知道。我的時間很寶貴,沒有時間浪費在這種象牙塔理論上。如果讀懂一段程式碼需要超過 5 分鐘的時間,我就覺得這段程式碼違背了 Apple 的 ”local reasoning“ 原則。

如果你和我一樣也是一個軟體開發者,最好始終對新的方法論保持一個開放的心態,並且始終將控制複雜度作為你的主要工作重心。我絕不反對賺錢,但看得更高更遠一點是有好處的。記住,Apple 是一家公司,一家大公司,主要使命是賺大錢,上週五的市值已經接近 8370 億美元,擁有數千億的現金和現金等價物。他們想讓每個人都使用 Swift,而這些公司吸引人到自家生態系統的方法之一就是提供別人都提供不了的產品和服務。是的,Swift 是開源的,但 Apple 從 App Store 賺了大錢,因此應用正是讓所有 Apple 裝置變得有用的關鍵,許許多多的開發者正在向 Swift 遷移。

我覺得沒有任何理由只用 POP 進行程式設計。我認為 POP 和我使用的其他許多技術,甚至是 OOP 一樣,都有一些問題。我們是在對現實建模,或者至少說,我們是在對現實進行擬合。沒有完美的解決方案。所以,將 POP 作為你的開發工具箱中的一種吧,就像人們長年以來總結出的其他優秀的方案一樣。

結論

30 年的開發經驗,讓我能夠平心靜氣地說,**你應該瞭解協議和 POP。**開始設計並書寫你自己的 POP 程式碼吧。

我已經花費了不少時間試用 POP,並且已經將這篇文章中的協議使用在了我自己的應用中,比如 SimpleViewWithBorderViewWithBackgroundViewWithBorderListFIFOLIFO。POP 威力無窮。

正如我在前一篇文章中提到的,學習並接受一種新方法,比如 POP,並不是一個非對即錯的事情。POP 和 OOP 不僅能並存,還能夠互相補充。

所以,開始試驗、練習、學習吧。最後,盡情享受生活和工作吧。

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 swift.gg

相關文章