Swift编程思想(一) - 面向协议编程

面向协议编程

你可能听过类似的概念:面向对象编程函数式编程泛型编程,再加上苹果新提出的面向协议编程,这些统统可以理解为是一种编程范式。所谓编程范式,是隐藏在编程语言背后的思想,代表着语言的作者想要用怎样的方式去解决怎样的问题。
不同的编程范式反应在现实世界中,就是不同的编程语言适用于不同的领域和环境,比如在面向对象编程思想中,开发者用对象来描述万事万物并试图用对象来解决所有可能的问题。编程范式都有其各自的偏好和使用限制,所以越来越多的现代编程语言开始支持多范式,使语言自身更强壮也更具适用性。
面向协议编程是在面向对象编程基础上演变而来,将程序设计过程中遇到的数据类型的抽取(抽象)由使用基类进行抽取改为使用协议进行抽取。更简单点举个例子来说,一个猫类、一个狗类,我们很容易想到抽取一个描述动物的基类,这就是面向对象编程。当然也会有人想到抽取一个动物通用的协议,这就是面向协议编程了。
而在 Swift 语言中,协议被赋予了更多的功能和更广阔的使用空间,为协议增加了扩展功能,使其能够胜任绝大多数情况下数据类型的抽象,所以苹果开始声称 Swift 是一门支持面向协议编程的语言。

协议基础

官方文档的定义:

协议为方法、属性、以及其他特定的任务需求或功能定义蓝图。协议可被类、结构体、或枚举类型采纳以提供所需功能的具体实现。满足了协议中需求的任意类型都叫做遵循了该协议。

协议的定义

protocol Food { }

用关键词 protocol ,声明一个名为 Food 的协议。

定义协议属性

protocol Pet {
var name: String { get set }
var master: String { get }
static var species: String { get }
}

协议中定义属性表示遵循该协议的类型具备了某种属性。

  • 只能使用 var 关键字声明;
  • 需要明确规定该属性是可读的 {get} 、 还是可读可写的 {get set}
  • 为了保证通用,协议中必须用 static 定义类型方法、类型属性、类型下标,因为 class 只能用在类中,不能用于结构体等;
  • 属性不能赋初始值;
struct Dog: Pet {
var name: String
var master: String
static var species: String = "哺乳动物"

var color: UIColor? = nil
}

var dog = Dog(name: "旺财", master: "小明")
dog.master = "张三" // 更改了主人

定义一个继承协议的结构体 Dog ,并新增了一个color属性。

  • static 修饰的类属性必须有初始值或实现了 get set 方法('static var' declaration requires an initializer expression or getter/setter specifier
  • set 为什么不报错?
if dog.master == "小明" {
dog.master = "张三"
}

master 属性在协议中被定义为只读属性 get,为什么上面的代码还可以 set

协议中的“只读”属性修饰的是协议这种“类型”的实例。

let pet: Pet = dog
pet.master = "李四"

虽然我们并不能像创建类的实例那样直接创建协议的实例,但是我们可以通过“赋值”得到一个协议的实例。此时 就会报错 Cannot assign to property: 'master' is a get-only property
Dog 中新增的 Pet 中没有的属性 var color: UIColor? = nil ,将不会出现在 pet 中。

协议属性可以给默认值么?

不能给协议属性设置默认值,因为默认值被看做是一种实现;
可以通过其他方法达到给协议默认值的效果。

protocol Person {
    var species: String { get }
}

extension Person {
    var species: String {
        get {
            return "人类"
        }
    }
}

但是此时的的这个属性只允许 get,不允许 set。可以通过一个临时变量记录改变值达到目的。

protocol Person {
    var identifier: String { set get }
}

private var _identifier: String = ""

extension Person {
    var identifier: String {
        get {
            return _identifier
        }
        set {
            _identifier = newValue
        }
    }
}

一般情况下不建议这么做,因为: 协议主要是用来统一接口的,对于相似的业务做抽象,具体实现可以在具体业务里面通过别的方式去实现。既然抽象了, 就不应该负责具体表现。

定义协议方法

Swift 中的协议可以定义类方法或实例方法,在遵守该协议的类型中,具体的实现方法的细节,通过类或实例调用。

protocol Pet {
var name: String { get set }
var master: String { get }
static var species: String { get }

// 新增的协议方法
static func sleep()
mutating func changeName()
}

struct Dog: Pet {
var name: String
var master: String
static var species: String = "哺乳动物"
var color: UIColor? = nil

static func sleep() {
print("要休息了")
}
mutating func changeName() {
name = "大黄"
}
}
  • 声明的协议方法的参数不能有默认值
 func changeName(name: String = "大黄") // Swift认为默认值也是一种变相的实现
  • 结构体中的方法修改属性时,需要在方法前面加上关键字mutating ,表示该属性属性能被修改。这样的方法叫 异变方法

协议中的初始化器

每一个宠物在被领养的时候,主人就已经确定了:

// 在上面的代码中新增
protocol Pet {
init(master: String)
}

struct Dog: Pet {
required init(master: String) {
self.master = master
}
}

此时会报错 在不初始化所有存储属性的情况下从初始化器中返回所有属性。 ( Return from initializer without initializing all stored properties )。加上 self.name = "" 就可以了。

class Cat: Pet {
required init(master: String) {
self.master = master
self.name = ""
}
}

Cat类遵守了该协议,初始化器必须用required关键字修饰初始化器的具体实现。

继承与遵守协议

class SomeClass: NSObject, OneProtocol, TwoProtocol { }

因为 Swift 中类的继承是单一的,但是类可以遵守多个协议,因此为了突出其单一父类的特殊性,应该将继承的父类放在最前面,将遵守的协议依次放在后面

多个协议方法名冲突

protocol ProtocolOne {
func method() -> Int
}
protocol ProtocolTwo {
func method() -> String
}

struct PersonStruct: ProtocolOne, ProtocolTwo {
func method() -> Int {
return 1
}
func method() -> String {
return "Hello World"
}
}

let ps = PersonStruct()
//尝试调用返回值为Int的方法
let num = ps.method()
//尝试调用返回值为String的方法
let string = ps.method()

let num = (ps as ProtocolOne).method()
let string = (ps as ProtocolTwo).method()

编译器无法知道同名 method() 方法到底是哪个协议中的方法,因此需要指定调用特定协议的 method() 方法 。

协议方法的可选实现

  • 方法一:通过 optional 实现可选
@objc protocol OptionalProtocol {
@objc optional func optionalMethod()
func requiredMethod()
}
  • 方法二: 通过 extension 做默认处理
protocol OptionalProtocol {
func optionalMethod()
func requiredMethod()
}

extension OptionalProtocol {
func optionalMethod() {

}
}

协议的继承、聚合

协议的继承

协议可以继承一个或者多个其他协议并且可以在它继承的基础之上添加更多要求。协议继承的语法与类继承的语法相似,选择列出多个继承的协议,使用逗号分隔。

protocol OneProtocol { }
protocol TwoProtocol { }
protocol ThreeProtocol: OneProtocol, TwoProtocol { }

产生了一个 新协议 ,该协议拥有 OneProtocolTwoProtocol 的方法或属性。需要实现 OneProtocolTwoProtocol必须的方法或属性。

协议的聚合

使用形如OneProtocol & TwoProtocol的形式实现协议聚合(组合)复合多个协议到一个要求里

protocol OneProtocol { }
protocol TwoProtocol { }
typealias FourProtocol = OneProtocol & TwoProtocol

聚合出来的不是新的协议,只是一个代指,代指这些协议的集合。

协议的继承和聚合的区别

首先协议的继承是定义了一个全新的协议,我们是希望它能够“大展拳脚”得到普遍使用。而协议的聚合不一样,它并没有定义新的固定协议类型,相反,它只是定义一个临时的拥有所有聚合中协议要求组成的局部协议,很可能是“一次性需求”,使用协议的聚合保证了代码的简洁性、易读性,同时去除了定义不必要的新类型的繁琐,并且定义和使用的地方如此接近,见明知意,也被称为匿名协议聚合。但是使用了匿名协议聚合能够表达的信息就少了一些,所以需要开发者斟酌使用。

协议的检查

if pig is Pet {
print("遵守了 Pet 协议")
}

检查 pig 是否是遵守了 Pet 协议类型的实例。

协议的指定

protocol ClassProtocol: class { }

struct Test: ClassProtocol { } // 报错

使用关键字 class 使定义的协议只能被类遵守。如果有枚举或结构体尝试遵守会报错 Non-class type 'Test' cannot conform to class protocol 'ClassProtocol'

协议作为参数

func update(param: FourProtocol) { }

将协议作为参数,表明遵守了该协议的实例可作为参数。

协议的关联类型

协议的关联类型指的是根据使用场景的变化,如果协议中某些属性存在 逻辑相同的而类型不同的情况,可以使用关键字 associatedtype 来为这些属性的类型声明“关联类型”。

protocol LengthMeasurable {
associatedtype LengthType
var length: LengthType { get }
func printMethod()
}

struct Pencil: LengthMeasurable {
typealias LengthType = CGFloat
var length: CGFloat
func printMethod() {
print("铅笔的长度为 \(length) 厘米")
}
}

struct Bridge: LengthMeasurable {
typealias LengthType = Int
var length: Int
func printMethod() {
print("桥梁的的长度为 \(length) 米")
}
}

LengthMeasurable 协议中用 associatedtype 定义了一个 类型泛型 。在实现协议的时候,定义具体的类型。这样就可以适配各种物体长度的测量。

associatedtype & typealias 的区别

  • associatedtype: 在定义协议时,可以用来声明一个或多个类型作为协议定义的一部分,叫关联类型。这种关联类型为协议中的某个类型提供了自定义名字,其代表的实际类型或实际意义在协议被实现时才会被指定。
  • typealias: 是给 现有 的类型(包括系统和自定义的)进行重新命名,然后就可以用该别名来代替原来的类型,已达到改善程序可读性,而且可以自实际编程中根据业务来重新命名,可以表达实际意义。

协议的扩展

设想一个这样的场景: 有一个人参加比赛,三个评委打分。比赛结束,求这个人的平均分。

protocol Score {
var name: String { get set }
var firstJudge: CGFloat { get set }
var secondJudge: CGFloat { get set }
var thirdJudge: CGFloat { get set }

func averageScore() -> String
}

struct Xiaoming: Score {
var firstJudge: CGFloat
var secondJudge: CGFloat
var thirdJudge: CGFloat

func averageScore() -> String {
let average = (firstJudge + secondJudge + thirdJudge) / 3
return "\(name)的得分为\(average)"
}
}
let xiaoming = Xiaoming(name: "小明", firstJudge: 80, secondJudge: 90, thirdJudge: 100)
let average = xiaoming.averageScore()

这场比赛,如果有 10 个人参加,计算平均值的方法,就需要写 10 次。代码重复严重。可以通过 协议的扩展解决问题

extension Score {
func averageScore() -> String {
let average = (firstJudge + secondJudge + thirdJudge) / 3
return "\(name)的得分为\(average)"
}
}

averageScore 默认进行了实现,不需要遵守者必须实现了。

这个时候,比赛承办方想统计每个人的最高分,应该怎么办呢?

extension Score {
func maxScore() -> CGFloat {
return max(firstJudge, secondJudge, thirdJudge)
}
}
let maxScore = xiaoming.maxScore()

比赛承办方对比赛结果的输出不太满意。想把 小明的得分为 90.0 前面统一加上前缀。

// 对系统协议进行扩展
extension CustomStringConvertible {
var customDescription: String {
return "新希望学校春季运动会运动会得分为::" + description
}
}
print(xiaoming.averageScore().customDescription)

总结:

  • 通过协议的扩展提供协议中某些属性和方法的默认实现。
  • 将公共的代码和属性统一起来极大的增加了代码的复用。
  • 为系统/自定义的协议提供的扩展。

Swift 的 55 标准库协议

Swift 的 55 标准库协议可以分为三类

类型 描述 标志
”Can do“协议 (表示能力)描述的事情是类型可以做或已经做过的。 -able 结尾
"Is a"协议 (表示身份)描述类型是什么样的,与”Can do”的协议相比,这些更基于身份,表示身份的类型。 -type 结尾
"Can be"协议 (表示转换)这个类型可以被转换到或者转换成别的东西。 -Convertible 结尾

如何更好的命名协议?

在自定义协议时应该尽可能遵守苹果的命名规则,便于开发人员之间的高效合作。

协议编程的优势

面向对象(继承)

有这样一个需求,在某个页面中,显示的 Logo 图片需要切圆角处理,让它更美观一些。

logoImageView.layer.cornerRadius = 5
logoImageView.layer.masksToBounds = true

如果要求,整个 APP 中所有的Logo都要切圆角处理。最容易的解决办法是定义一个名为LogoImageView的类,使用该类初始化Logo对象。

class LogoImageView: UIImageView {
init(radius: CGFloat = 5) {
super.init(frame: CGRect.zero)
layer.cornerRadius = radius
layer.masksToBounds = true
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

如果要求所有的 Logo 还要支持点击抖动效果。

class LogoImageView: UIImageView {

init(radius: CGFloat = 5) {
super.init(frame: CGRect.zero)
layer.cornerRadius = radius
layer.masksToBounds = true

isUserInteractionEnabled = true
let tap = UITapGestureRecognizer.init(target: self, action: #selector(shakeEvent))
addGestureRecognizer(tap)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

extension LogoImageView {
@objc func shakeEvent() {
let animation = CAKeyframeAnimation()
animation.keyPath = "transform"
animation.duration = 0.25

let origin = CATransform3DIdentity
let minimum = CATransform3DMakeScale(0.8, 0.8, 1)

let originValue = NSValue(caTransform3D: origin)
let minimumValue = NSValue(caTransform3D:minimum)

animation.values = [originValue, minimumValue, origin, minimumValue, origin]
layer.add(animation, forKey: "bounce")
layer.transform = origin
}
}

这个时候,如果其他的功能也要使用抖动动画,就不得不接受切圆角功能。即使把切圆角功能从初始化方法中剥离成一个可选方法,但是也不得不接受这份耦合代码。
有的项目里定义了继承 UIViewController 的父类,实现了很多功能,项目里页面都要继承它。而且往这个自定义UIViewController里塞代码实在太方便了,这个类很容易随着功能迭代逐渐膨胀,变的僵化,越来越难以维护。下面的子类代码全都依赖这个父类,想抽出来复用非常难。
项目里混合使用了原生功能和 H5 功能。定义的 H5 容器的父类 WebViewController,需要满足以下业务要求:

  • 有的 H5 页面比较简单,只需要正常展示网页即可。
  • 有的需要 JS 代码注入。
  • 有的需要提供保存图片到相册给 H5 使用。
  • 有的需要提供存跳转原始页面给 H5 使用。

这个父类中处理了WKWebView 实现、 JS 注入、桥的定义以及桥功能的实现等众多能力。导致WebViewController代码量多达几千行。很难维护扩展。

采用 继承 方式解决复用的问题,很容易带来代码的耦合。

假如 UILabel 也需要抖动的动画,采用继承无法实现。UIImageViewUILabel 已经是 UIView 的子类,除非改动 UIView,否则无法通过继承的方式实现。

面向对象(扩展)

通过扩展,可以不修改类的实现文件的情况下,给类增加新的方法。可以通过给 UIView 添加扩展来解决 UIImageView 和 UILabel 同时增加抖动功能的需求。缺点是给一个类加上这个东西就污染了该类所有的对象。(UIButton 说: 我不需要为什么塞给我?)

面向对象(工具类)

当然,我们可以直接写一个工具类来实现这个抖动的效果,然后把必要的参数(layer)传递过来。缺点就是,使用起来相对麻烦,众多的工具类难易管理。(你有因为不知道该使用哪个工具类头疼过么? 有因为要把方法放哪个工具类头疼过么?有因为工具类代码量过多头疼过么?)

面向协议

通过协议重新实现 切圆角功能和抖动动画功能。

/// 声明一个圆角的能力协议
protocol RoundCornerable {
func roundCorner(radius: CGFloat)
}

/// 通过扩展给这个协议方法添加默认实现,必须满足遵守这个协议的类是继承UIView的。
extension RoundCornerable where Self: UIView {
func roundCorner(radius: CGFloat) {
layer.cornerRadius = radius
layer.masksToBounds = true
}
}

/// 声明抖动动画的协议
protocol Shakeable {
func startShake()
}
/// 实现协议方法内容,并指定只有LogoImageView才可以使用。
extension Shakeable where Self: LogoImageView {
func startShake() {
let animation = CAKeyframeAnimation()
animation.keyPath = "transform"
animation.duration = 0.25

let origin = CATransform3DIdentity
let minimum = CATransform3DMakeScale(0.8, 0.8, 1)

let originValue = NSValue(caTransform3D: origin)
let minimumValue = NSValue(caTransform3D:minimum)

animation.values = [originValue, minimumValue, origin, minimumValue, origin]
layer.add(animation, forKey: "bounce")
layer.transform = origin
}
}

/// 遵守了RoundCornerable协议,才拥有切圆角的功能。遵守了Shakeable协议,才拥有抖动动画效果。
class LogoImageView: UIImageView, RoundCornerable, Shakeable {
init(radius: CGFloat = 5) {
super.init(frame: CGRect.zero)

roundCorner(radius: radius)

isUserInteractionEnabled = true
let tap = UITapGestureRecognizer.init(target: self, action: #selector(shakeEvent))
addGestureRecognizer(tap)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

@objc func shakeEvent() {
startShake()
}
}

泛型及泛型约束

有时候会有一些场景声明的协议只给部分对象使用。

/// 该协议只运行LogoImageView以及其子类使用
extension Shakeable where Self: LogoImageView { }

协议解决面向对象中棘手的超类问题

面向协议编程1

麻雀作为一种鸟类,应该继承,但是如果继承了,就相当于默认了麻雀是一种宠物,这显然是不和逻辑的。麻雀在图中的位置就显得比较尴尬。解决此问题的一般方法如下

面向协议编程2

乍一看好像解决了这样的问题,但是仔细想由于 Swift 只支持单继承,麻雀没有继承类就无法体现麻雀作为一种拥有的特性(比如飞翔)。如果此时出现一个新的飞机类,虽然飞机宠物之间没有任何联系,但是飞机是由很多共同特性的(比如飞翔),这样的特性该如何体现呢?答案还是新建一个类成为动物飞机的父类。
面向对象就是这样一层一层的向上新建父类最终得到一个“超级父类”NSObject。尽管问题得到了解决,但是麻雀飞机之间的共性并没有得到很好的体现。而协议的出现正是为了解决这类问题。

面向协议编程3

实际上图中包括动物飞机等类之间的关系就应该是如上图所示的继承关系。使用协议将“宠物”、“飞翔”等看作是一种特性,或者是从另一个维度描述这种类别,更重要的是使用协议并不会打破原有类别之间继承的父子关系。
和飞翔相关的代码统一放在Flyable中,需要“飞翔”这种能力就遵守该协议;和宠物相关的代码统一放在PetType中,需要成为宠物就遵守该协议。这些
协议灵活多变,结合原有的面向对象类之间固有的继承关系,完美的描述了这个世界。