Swift 中的面向協議編程:引言

做者:Andrew Jaffee,原文連接,原文日期:2018-03-20 譯者:灰s;校對:numbbbbbWAMaker;定稿:Pancfphp

對於開發者來講,複雜性是最大的敵人,所以我會去了解那些能夠幫助我管理混亂的新技術。Swift 中的「面向協議編程」(POP)是最近(至少自2015年以來)引發普遍關注的「熱門」方法之一。在這裏咱們將使用 Swift 4。在我本身編寫代碼時,發現 POP 頗有前途。更吸引人的是,Apple 宣稱 「Swift 的核心是面對協議的」。我想在一個正式的報告中分享關於 POP 的經驗,一篇關於這個新興技術清晰而簡潔的教程。html

我將解釋關鍵概念,提供大量代碼示例,沒法避免的將 POP 和 OOP (面向對象編程)進行比較,並對面向流行編程(FOP?)的人羣所聲稱的 POP 是解決全部問題的靈丹妙藥這一說法進行潑冷水。git

面向協議編程是一個很棒的新工具,值得添加到你現有的編程工具庫中,可是沒有什麼能夠代替那些經久不衰的基本功,就像將大的函數拆分紅若干個小函數,將大的代碼文件拆分紅若干個小的文件,使用有意義的變量名,在敲代碼以前花時間設計架構,合理而一致的使用間距和縮進,將相關的屬性和行爲分配到類和結構體中 - 遵循這些常識可讓世界變得不一樣。若是你編寫的代碼沒法被同事理解,那它就是無用的代碼。github

學習和採用像 POP 這樣的新技術並不須要絕對的惟一。POP 和 OOP 不只能夠共存,還能夠互相協助。對於大多數開發者包括我本身,掌握 POP 須要時間和耐心。由於 POP 真的很重要,因此我將教程分紅兩篇文章。本文將主要介紹和解釋 Swift 的協議和 POP。第二篇文章將深刻研究 POP 的高級應用方式(好比從協議開始構建應用程序的功能),範型協議,從引用類型到值類型轉變背後的動機,列舉 POP 的利弊,列舉 OOP 的利弊,比較 OOP 和 POP,闡述爲何「Swift 是面向協議的」,而且深刻研究一個被稱爲 「局部推理」 的概念,它被認爲是經過使用 POP 加強的。此次咱們只會粗略涉及一些高級主題。算法

引言

做爲軟件開發者,管理複雜性本質上是咱們最應該關注的問題。當咱們嘗試學習 POP 這項新技術時,你可能沒法從時間的投資中看到即時回報。可是,就像你對個人認識有個過程同樣,你將會了解 POP 處理複雜性的方法,同時爲你提供另外一種工具來控制軟件系統中固有的混亂。編程

我聽到愈來愈多關於 POP 的討論,可是卻不多看到使用這種方式編寫的產品代碼,換句話說,我尚未看到有不少人從協議而不是類開始建立應用程序的功能。這不只僅是由於人類有抗拒改變的傾向。學習一種全新的範式並將其付諸實踐,提及來容易作起來難。在我編寫新應用程序時,逐漸發現本身開始使用 POP 來設計和實現功能 — 有組織的且天然而然的。swift

伴隨着新潮流帶來的刺激,不少人都在談論用 POP 取代 OOP。我認爲除非像 Swift 這樣的 POP 語言被普遍改進,不然這是不可能發生的 — 也或許根本就不會發生。我是個實用主義者,而不是追求時髦的人。在開發新的 Swift 項目時,我發現本身的行爲是一種折衷的方法。我在合理的地方利用 OOP,而用 POP 更合適的地方也不會死腦筋的必定要使用 OOP,這樣反而瞭解到這兩種模式並不相互排斥。我把這兩種技術結合在一塊兒。在本期由兩部分組成的 POP 教程中,你將瞭解我在說什麼。安全

我投入到 OOP 中已經有好久了。1990 年,我買了一個零售版本的 Turbo Pascal。在使用了 OOP 大約一年後,我開始設計、開發和發佈面向對象的應用程序產品。我成了一個忠粉。當我發現能夠擴展加強本身的類,簡直興奮的飛起。隨着時間的推移,Microsoft 和 Apple 等公司開始開發基於 OOP 的大型代碼庫,如 Microsoft Foundation Classes(MFC)和 .NET,以及 iOS 和 OS X SDK。如今,開發人員在開發新應用程序時不多須要從新造輪子。沒有完美的方法,OOP 也有一些缺點,可是優勢仍然大於缺點。咱們將花一些時間來比較 OOP 和 POP。架構

理解協議

當開發人員設計一個新的 iOS 應用程序的基本結構時,他們幾乎老是從 FoundationUIKit 等框架中的現有 開始。我能想到的幾乎全部應用程序都須要某種用戶界面導航系統。用戶須要一些進入應用程序的入口點和引導他們使用應用程序功能的路標。能夠瀏覽一下你的 iPhone 或 iPad 上的應用程序。app

當這些應用程序打開時,你看到了什麼?我打賭你看到的是 UITableViewControllerUICollectionViewControllerUIPageViewController 的子類。

當你第一次建立新的 iOS 項目時,全部人都必須認識下面的代碼片斷,例如,一個新的 iOS 項目基於 Xcode 中的 Single View App(單視圖應用) 模板:

...
import UIKit

class ViewController: UIViewController
{
...  
複製代碼

部分開發人員將在這裏停下來,建立徹底定製的接口,但大多數人將採起另外一個步驟。

當 iOS 開發者開發新的應用程序時,最多見的特徵就是 OOP,那麼 POP 在這裏扮演什麼角色呢?

你知道我將怎樣繼續麼?想象大多數開發人員的下一個主要步驟是什麼。那就是遵循協議(並實現 委託,但咱們已經討論過了)。

讓我給大家看一個例子使其便於理解。我相信大家不少人都用過 UITableView。雖然這不是一個關於 UITableView 的教程,可是你應該知道在 UIViewController 中將其實現時,協議扮演着重要的角色。在向 UIViewController 中添加 UITableView時,UIViewController 必須遵循 UITableViewDataSourceUITableViewDelegate 協議,就像這樣:

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate  
複製代碼

簡而言之,遵循 UITableViewDataSource 容許你用數據填充全部的 UITableViewCell,好比給用戶提供導航的菜單項名稱。採用 UITableViewDelegate,你能夠對用戶與 UITableView 的交互進行更細粒度的控制,好比在用戶點擊特定的 UITableViewCell 時執行適當的操做。

定義

我發現,在進行技術性定義和討論以前,理解經常使用的術語定義能夠幫助讀者更好地理解某個主題。首先,讓咱們 考慮 「協議」一詞的通俗定義

……管理國家事務或外交領域的正式程序或規則體系。……
在任何團體、組織或形勢下,公認或已制定的程序或行爲準則。……
進行科學實驗時的程序……

Apple 的「Swift 編程語言(Swift 4.0.3)」 文檔中的聲明

協議定義了適合特定任務或功能的方法、屬性和其餘需求的藍圖。而後,類、結構體或枚舉能夠遵循該協議來提供這些需求的實際實現。任何知足協議要求的類型都被稱爲遵循該協議。

協議是最重要的工具之一,咱們必須給軟件固有的混亂帶來一些秩序。協議使咱們可以要求一個或多個類和結構體包含特定的最小且必需的屬性,和/或提供特定的最小且必需的實現/方法。經過 協議擴展,咱們能夠爲一些或全部協議的方法提供默認實現。

遵循協議

下面,咱們將使自定義的 Person遵循採用)Apple 自帶 Equatable 協議。

遵循 Equatable 協議之後可使用等於運算符(==)來判斷是否相等,使用不等於運算符(!=)來判斷是否不等。Swift 標準庫中的大部分基礎類型都遵循了 Equatable 協議……

class Person : Equatable
{
    var name:String
    var weight:Int
    var sex:String
    
    init(weight:Int, name:String, sex:String)
    {
        self.name = name
        self.weight = weight
        self.sex = sex
    }
    
    static func == (lhs: Person, rhs: Person) -> Bool
    {
        if lhs.weight == rhs.weight &&
            lhs.name == rhs.name &&
            lhs.sex == rhs.sex
        {
            return true
        }
        else
        {
            return false
        }
    }
}
複製代碼

Apple 規定,「自定義類型聲明它們採用特定的協議,須要將協議的名稱放在類型名稱以後,以冒號分隔,做爲其定義的一部分。」這也正是我所作的:

class Person : Equatable
複製代碼

你能夠將協議理解爲專門針對 classstructenum約定承諾。我經過 Equatable 協議使自定義的 Person 類遵照了一個約定,Person 類***承諾***經過現實 Equatable 協議須要的方法或成員變量來履行該約定,即將其實現。

Equatable 協議***並無實現任何東西***。它只是指明瞭***採用(遵循)*** Equatable 協議的 classstruct,或者 enum ***必須實現***的方法和/或成員變量。有一些協議經過 extensions 實現了功能,稍後咱們會進行討論。我不會花太多時間來說述關於 enum 的 POP 用法。我將它做爲練習留給你。

定義協議

理解協議最好的方式是經過例子。我將本身構建一個 Equatable 來向你展現協議的用法:

protocol IsEqual
{
    static func == (lhs: Self, rhs: Self) -> Bool
    
    static func != (lhs: Self, rhs: Self) -> Bool
}
複製代碼

請記住,個人「IsEqual」協議並無對 ==!= 運算符進行實現。「IsEqual」須要協議的遵循者***實現他們本身的*** ==!= 運算符。

全部定義協議屬性和方法的規則都在 Apple 的 Swift 文檔 中進行了總結。好比,在協議中定義屬性永遠不要用 let 關鍵字。只讀屬性規定使用 var 關鍵字,並在後面單獨跟上 { get }。若是有一個方法改變了一個或多個屬性,你須要標記它爲 mutating。你須要知道爲何我重寫的 ==!= 操做符被定義爲 static。若是你不知道,找出緣由將會是一個很好的練習。

爲了向你展現個人 IsEqual(或者 Equatable)這樣的協議具備普遍的適用性,咱們將使用它在下面構建一個類。可是在咱們開始以前,讓咱們先討論一下「引用類型」與「值類型」。

引用類型與值類型

在繼續以前,您應該閱讀 Apple 關於 「值和引用類型」 的文章。它將讓你思考引用類型和值類型。我故意不在這裏講太多細節,由於我想讓大家思考並理解這個很是重要的概念。它太太重要,以致於針對 POP 引用/值類型的討論同時出如今這些地方:

  1. WWDC 2015 展現的 「Protocol-Oriented Programming in Swift」
  2. WWDC 2015 展現的 「Building Better Apps with Value Types in Swift」
  3. WWDC 2016 展現的 「Protocol and Value Oriented Programming in UIKit Apps」

我會給你一個提示和做業……假設你有多個指向同一個類實例的引用,用於修改或「改變」屬性。這些引用指向相同的數據塊,所以將其稱爲「共享」數據並不誇張。在某些狀況下,共享數據可能會致使問題,以下面的示例所示。這是否表示咱們要將全部的代碼改爲值類型?**並非!**就像 Apple 的一個工程師指出:「例如,以 Window 爲例。複製一個 Window 是什麼意思?」 查看下面的代碼,並思考這個問題。

引用類型

下面的代碼片斷來自 Xcode playground,在建立對象副本而後更改屬性時,會遇到一個有趣的難題。你能找到問題麼?咱們將在下一篇文章中討論這個問題。

這段代碼同時也演示了協議的定義和 extension

// 引用類型:每一個人都使用類很長時間了 
// -- 想一想 COCOA 中進行的全部隱式複製。

protocol ObjectThatFlies
{
    var flightTerminology: String { get }
    func fly() // 不須要提供實現,除非我想
}

extension ObjectThatFlies
{
    func fly() -> Void
    {
        let myType = String(describing: type(of: self))
        let flightTerminologyForType = myType + " " + flightTerminology + "\n"
        print(flightTerminologyForType)
    }
}

class Bird : ObjectThatFlies
{
    var flightTerminology: String = "flies WITH feathers, and flaps wings differently than bats"
}

class Bat : ObjectThatFlies
{
    var flightTerminology: String = "flies WITHOUT feathers, and flaps wings differently than birds"
}

// 引用類型

let bat = Bat()
bat.fly()
// "Bat flies WITHOUT feathers, and flaps wings differently than birds"

let bird = Bird()
bird.fly()
// "Bird flies WITH feathers, and flaps wings differently than bats"

var batCopy = bat
batCopy.fly()
// "Bird flies WITH feathers, and flaps wings differently than bats"

batCopy.flightTerminology = ""
batCopy.fly()
// 控制檯輸出 "Bat"

bat.fly()
// 控制檯輸出 "Bat"
複製代碼

來自前面代碼片斷的控制檯輸出

Bat flies WITHOUT feathers, and flaps wings differently than birds

Bird flies WITH feathers, and flaps wings differently than bats

Bird flies WITH feathers, and flaps wings differently than bats

Bat

Bat
複製代碼

值類型

在接下來的 Swift 代碼片斷中,咱們使用 struct 替代 class。在這裏,代碼看起來更安全,而 Apple 彷佛在推廣值類型和 POP。注意,他們目前尚未放棄 class

// 這是範式轉變的起點,不只僅是協議,還有值類型

protocol ObjectThatFlies
{
    var flightTerminology: String { get }
    func fly() // 不須要提供實現,除非我想
}

extension ObjectThatFlies
{
    func fly() -> Void
    {
        let myType = String(describing: type(of: self))
        let flightTerminologyForType = myType + " " + flightTerminology + "\n"
        print(flightTerminologyForType)
    }
}

struct Bird : ObjectThatFlies
{
    var flightTerminology: String = "flies WITH feathers, and flaps wings differently than bats"
}

struct Bat : ObjectThatFlies
{
    var flightTerminology: String = "flies WITHOUT feathers, and flaps wings differently than birds"
}

// 值類型

let bat = Bat()
bat.fly()
// "Bat flies WITHOUT feathers, and flaps wings differently than birds"

let bird = Bird()
bird.fly()
// "Bird flies WITH feathers, and flaps wings differently than bats"

var batCopy = bat
batCopy.fly()
// "Bird flies WITH feathers, and flaps wings differently than bats"

// 我在這裏對 Bat 實例所作的事情是顯而易見的
batCopy.flightTerminology = ""
batCopy.fly()
// 控制檯輸出 "Bat"

// 可是,由於咱們使用的是值類型,因此 Bat 實例的原始數據並無由於以前的操做而被篡改。
bat.fly()
// "Bat flies WITHOUT feathers, and flaps wings differently than birds"
複製代碼

來自前面代碼片斷的控制檯輸出

Bat flies WITHOUT feathers, and flaps wings differently than birds

Bird flies WITH feathers, and flaps wings differently than bats

Bat flies WITHOUT feathers, and flaps wings differently than birds

Bat 

Bat flies WITHOUT feathers, and flaps wings differently than birds
複製代碼

示例代碼

我寫了一些面向協議的代碼。請通讀代碼,閱讀內聯註釋,閱讀附帶的文章,跟隨個人超連接,並充分理解我在作什麼。你將在下一篇關於 POP 的文章中用到它。

採用多種協議

剛開始寫這篇文章的時候,我很貪心,想要自定義一個協議,使它能***同時***體現 Apple 的內置協議 EquatableComparable

protocol IsEqualAndComparable
{

    static func == (lhs: Self, rhs: Self) -> Bool

    static func != (lhs: Self, rhs: Self) -> Bool
    
    static func > (lhs: Self, rhs: Self) -> Bool
    
    static func < (lhs: Self, rhs: Self) -> Bool
    
    static func >= (lhs: Self, rhs: Self) -> Bool
    
    static func <= (lhs: Self, rhs: Self) -> Bool

}
複製代碼

我意識到應該將它們分開,使個人代碼儘量靈活。爲何不呢?Apple 聲明同一個類,結構體,枚舉能夠遵循多個協議,就像接下來咱們將看到的同樣。下面是我提出的兩個協議:

protocol IsEqual
{
    static func == (lhs: Self, rhs: Self) -> Bool
    
    static func != (lhs: Self, rhs: Self) -> Bool
}

protocol Comparable
{
    static func > (lhs: Self, rhs: Self) -> Bool
    
    static func < (lhs: Self, rhs: Self) -> Bool
    
    static func >= (lhs: Self, rhs: Self) -> Bool
    
    static func <= (lhs: Self, rhs: Self) -> Bool
}
複製代碼

記住你的算法

你須要磨練的一項重要技能是編程的算法,並將它們轉換爲代碼。我保證在未來的某一天,會有人給你一個複雜過程的口頭描述並要求你對它進行編碼。用人類語言描述某些步驟,以後用軟件將其實現,它們之間通常都會有很大的差距。當我想要將 IsEqualComparable 應用於表示直線(向量)的類時,我意識到了這一點。我記得計算一個直線的長度是基於勾股定理的(參考 這裏這裏),而且對向量使用 ==!=<><=,和 >= 這些運算符進行比較時,直線的長度是必須的。個人 Line 類早晚會派上用場,例如,在一個繪圖應用程序或遊戲中,用戶點擊屏幕上的兩個位置,在兩點之間建立一條線。

自定義類採用多個協議

這是個人 Line 類,它採用了兩個協議,IsEqualComparable(以下)。這是多繼承的一種形式!

class Line : IsEqual, Comparable
{
    var beginPoint:CGPoint
    var endPoint:CGPoint
    
    init()
    {
        beginPoint = CGPoint(x: 0, y: 0);
        endPoint = CGPoint(x: 0, y: 0);
    }

    init(beginPoint:CGPoint, endPoint:CGPoint)
    {
        self.beginPoint = CGPoint( x: beginPoint.x, y: beginPoint.y )
        self.endPoint = CGPoint( x: endPoint.x, y: endPoint.y )
    }
    
    // 線長的計算基於勾股定理。
    func length () -> CGFloat
    {
        let length = sqrt( pow(endPoint.x - beginPoint.x, 2) + pow(endPoint.y - beginPoint.y, 2) )
        return length
    }

    static func == (leftHandSideLine: Line, rightHandSideLine: Line) -> Bool
    {
        return (leftHandSideLine.length() == rightHandSideLine.length())
    }

    static func != (leftHandSideLine: Line, rightHandSideLine: Line) -> Bool
    {
        return (leftHandSideLine.length() != rightHandSideLine.length())
    }
    
    static func > (leftHandSideLine: Line, rightHandSideLine: Line) -> Bool
    {
        return (leftHandSideLine.length() > rightHandSideLine.length())
    }
    
    static func < (leftHandSideLine: Line, rightHandSideLine: Line) -> Bool
    {
        return (leftHandSideLine.length() < rightHandSideLine.length())
    }
    
    static func >= (leftHandSideLine: Line, rightHandSideLine: Line) -> Bool
    {
        return (leftHandSideLine.length() >= rightHandSideLine.length())
    }
    
    static func <= (leftHandSideLine: Line, rightHandSideLine: Line) -> Bool
    {
        return (leftHandSideLine.length() <= rightHandSideLine.length())
    }

} // 類的結束行:IsEqual, Comparable
複製代碼

驗證你的算法

我使用電子製表軟件 Apple Numbers,並準備了兩個向量的可視化表示,對 Line 類的 length() 方法作了一些基本測試:

這裏是我根據上面圖表中的點,寫的測試代碼:

let x1 = CGPoint(x: 0, y: 0)
let y1 = CGPoint(x: 2, y: 2)
let line1 = Line(beginPoint: x1, endPoint: y1)
line1.length()
// returns 2.82842712474619

let x2 = CGPoint(x: 3, y: 2)
let y2 = CGPoint(x: 5, y: 4)
let line2 = Line(beginPoint: x2, endPoint: y2)
line2.length()
// returns 2.82842712474619

line1 == line2
// returns true
line1 != line2
// returns false
line1 > line2
// returns false
line1 <= line2
// returns true
複製代碼

使用 Xcode 「Single View」 playground 模版測試/原型化 UI

你是否知道可使用 Xcode 9 Single View playground 模板來原型化和測試用戶界面(UI)?它很是棒 - 能夠節省大量時間並快速原型化的工具。爲了更好的測試個人 Line 類,我建立了這樣一個 playground。做業:在我解釋以前,我想讓你本身試一下。向你展現個人 playground 代碼、模擬器輸出和個人 Swift 測試語句。

這裏是個人 playground 代碼:

import UIKit
import PlaygroundSupport

class LineDrawingView: UIView
{
    override func draw(_ rect: CGRect)
    {
        let currGraphicsContext = UIGraphicsGetCurrentContext()
        currGraphicsContext?.setLineWidth(2.0)
        currGraphicsContext?.setStrokeColor(UIColor.blue.cgColor)
        currGraphicsContext?.move(to: CGPoint(x: 40, y: 400))
        currGraphicsContext?.addLine(to: CGPoint(x: 320, y: 40))
        currGraphicsContext?.strokePath()
        
        currGraphicsContext?.setLineWidth(4.0)
        currGraphicsContext?.setStrokeColor(UIColor.red.cgColor)
        currGraphicsContext?.move(to: CGPoint(x: 40, y: 400))
        currGraphicsContext?.addLine(to: CGPoint(x: 320, y: 60))
        currGraphicsContext?.strokePath()
        
        currGraphicsContext?.setLineWidth(6.0)
        currGraphicsContext?.setStrokeColor(UIColor.green.cgColor)
        currGraphicsContext?.move(to: CGPoint(x: 40, y: 400))
        currGraphicsContext?.addLine(to: CGPoint(x: 250, y: 80))
        currGraphicsContext?.strokePath()
    }
}

class MyViewController : UIViewController
{
    override func loadView()
    {
        let view = LineDrawingView()
        view.backgroundColor = .white

        self.view = view
    }
}

// 在實時視圖窗口中顯示視圖控制器
PlaygroundPage.current.liveView = MyViewController()
複製代碼

這是我在 playground 模擬器上的視圖輸出:

下面是測試個人 Line 類型實例與我在 playground 上所畫向量匹配的 Swift 代碼:

let xxBlue = CGPoint(x: 40, y: 400)
let yyBlue = CGPoint(x: 320, y: 40)
let lineBlue = Line(beginPoint: xxBlue, endPoint: yyBlue)

let xxRed = CGPoint(x: 40, y: 400)
let yyRed = CGPoint(x: 320, y: 60)
let lineRed = Line(beginPoint: xxRed, endPoint: yyRed)
lineRed.length()
// returns 440.454310910905

lineBlue != lineRed
// returns true
lineBlue > lineRed
// returns true
lineBlue >= lineRed
// returns true

let xxGreen = CGPoint(x: 40, y: 400)
let yyGreen = CGPoint(x: 250, y: 80)
let lineGreen = Line(beginPoint: xxGreen, endPoint: yyGreen)
lineGreen.length()
// returns 382.753184180093
lineGreen < lineBlue
// returns true
lineGreen <= lineRed
// returns true
lineGreen > lineBlue
// returns false
lineGreen >= lineBlue
// returns false
lineGreen == lineGreen
// returns true
複製代碼

總結

我但願你喜歡今天的文章,而且很是期待閱讀本文的「第二部分」。記住,咱們將深刻研究使用 POP 的先進應用程序,範型協議,從引用類型到值類型背後的動機,列舉 POP 的優缺點,列舉 OOP 的優缺點,比較 OOP 和 POP,肯定爲何「Swift 是面向協議的」,並深刻研究稱爲「局部推理」的概念。

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 swift.gg

相關文章
相關標籤/搜索