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

相关文章
相关标签/搜索