2015 年 WWDC,苹果第一次提出了 Swift 的面向协议编程(Protocol Oriented Programming,以下简称 POP ),这是计算机历史上一个全新的编程范式。在此之前,相对应的面向对象的编程(Object Oriented Programming,以下简称 OOP )已经大行其道 50 年,它几乎完美的解决函数式编程(Functional Programming)的缺点,并且出现在从大型系统到小型应用、从服务器端到前端的各个方面。它的优点被无数程序员称颂,它解决了诸多开发中的大小问题。那么问题来了,既然 OOP 如此万能,为什么 Swift 要弄出全新的 POP ?
笔者认为,原因有三。其一,OOP 有自身的缺点。在继承、代码复用等方面,其灵活度不高。而 POP 恰好可以优雅得解决这些问题;其二,POP 可以保证 Swift 作为静态语言的安全性,而彼时 Objective-C 时代的 OOP,其动态特性经常会导致异常;其三,OOP 无法应用于值类型,而 POP 却可以将其优势拓展到结构体(struct)和枚举(enum)类型上。
本节将通过问题串联的形式,说明 POP 相比于 OOP 的优势,同时展示 POP 在实际开发中的运用。
POP vs OOP
1.什么是 OOP ?它在 iOS 开发中有哪些优点?
关键词:#面向对象编程
OOP 全称是 Object Oriented Programming,即面向对象的编程,是目前最主流的编程范式。在 iOS 开发中,绝大多数的部分运用的都是 OOP。
在 iOS 开发中,它有如下优点:
- 封装和权限控制。相关的属性和方法被放入一个类中,Objective-C 中 ".h" 文件负责声明公共变量和方法,".m" 文件负责声明私有变量,并实现所有方法。Swift 中也有 public/internal/fileprivate/private 等权限控制。
- 命名空间。在 Swift 中,不同的 class 即使命名相同,在不同的 bundle 中由于命名空间不同,它们依然可以和谐共存毫无冲突。这在 App 很大、bundle 很多的时候特别有用。Objective-C 没有命名空间,所以很多类在命名时都加入了驼峰式的前缀。
- 扩展性。在 Swift 中,class 可以通过 extension 来进行增加新方法,通过动态特性亦可以增加新变量。这样我们可以保证在不破坏原来代码封装的情况下实现新的功能。Objective-C 中,我们可以用 category 来实现类似功能。另外,Swift 和 Objective-C 中还可以通过 protocol 和代理模式来实现更加灵活的扩展。
- 继承和多态。同其他语言一样,iOS 开发中我们可以将共同的方法和变量定义在父类中,在子类继承时再各自实现对应功能,做到代码复用的高效运作。同时针对不同情况可以调用不同子类,大大增加代码的灵活性。
2.请谈谈 OOP 在 iOS 开发中的缺点
关键词:#内存 #继承
一般面试官这样问,我们不仅要回答出缺点,还要说出一个比较成熟的解决方案。一个专业的程序员不仅要知道问题出在哪里,更要知道该怎么修正问题。
OOP 有以下几个缺点:
- 隐式共享。class 是引用类型,在代码中某处改变某个实例变量的时候,另一处在调用此变量时就会受此修改影响。示例代码如下:
class People { var name = “”}
// 创建张三,设置其名字为张三
let zhangSan = People()
zhangSan.name = “张三”
// 创建李四,设置其名字为李四
let liSi = zhangSan
Lisi.name = “李四”
print(zhangSan.name) // 李四
print(Lisi.name) // 李四
这很容易就造成异常。尤其是在多线程时,我们经常遇到的资源竞速(Race Condition)就是这个情况。解决方案是在多线程时枷锁,当然这个方案会引入死锁和代码复杂度剧增的问题。最好的解决这个问题是尽可能用诸如 struct 这样的值类型取代 class。
- 冗杂的父类。试想这样一种场景,一个 UIViewController 的子类和一个 UITableViewController 中都需要加入 handleSomething() 这种方法。OOP 的解决方案是直接在 UIViewController 的 extension 中加入 handleSomething()。但是随着新方法越加越多,以后 UIVIewController 会越变越冗杂。当然我们也可以引入一个专门的父类或工具类,但是依然有职权不明确、依赖、冗杂等多种问题。
另一方面,父类中的 handleSomething() 方法必须由具体实现,它不能根据子类做出灵活调整。子类如果要做特定操作,必须要重写方法来实现。既然子类要重写,那么在父类中的实现在这种时候就显得多此一举。解决方案使用 protocol,这样它的方法就不需要用具体实现了,交给服从它的类或结构体即可。
- 多继承。 Swift 和 Objective-C 是不支持多继承的,因为这会造成菱形问题,即多个父类实现了同一个方法,子类无法判断继承哪个父类的情况。在 Java 中,有 interface 的解决方案,Swift 中有类似 protocol 的解决方案。
2.说说 POP 相比于 OOP 的优势
关键词:#灵活 #安全
这道题是一个开放性的问题。在面试中一个很好的回答方式是理论 举例。POP 相比 OOP 具有如下优势。
- 更加灵活。比如上题中我们提到的冗杂的父类的例子。我们可以用协议和其扩展来让所有服从此协议的 class 都可以用到默认的 handleSomething() 方法,同时服从了该协议的同时也增加了代码的可读性。具体代码如下:
protocol SomethingHandleable {
func handleSomething()
}
extension SomethingHandleable {
func handleSomething() {
// 实现
}
}
class ViewController: UIViewController, SomethingHandleable { }
class TableViewController: UITableViewController, SomethingHandleable { }
- 减少依赖。相对于传入具体的实例变量,我们可以传入 protocol 来实现多态。同时测试时也可以利用 protocol 来 mock 真实的实例,减少对于对象及其实现的依赖。比如下面这个实例:
protocol Request {
func send(request: Info)
}
protocol Info {}
class UserRequest: Request {
// 注意这里我们传入了Info这个protocol,它无需是具体的UserInfo,这方便了我们之后测试和扩展
func send(info: Info) {
// 实际实现,一般是把info发给server
}
}
class UserInfo: Info {}
class MockUserRequest: Request {
func send(info: Info) { // 这里我们就可以为测试方便来自定义实现 }
}
func testUserRequest() {
let userRequest = MockUserRequest()
userRequest.send(info: UserInfo())
}
- 消除动态分发的风险。对于服从了 protocol 的类或结构体来说,它必须实现 protocol 声明的所有方法。否则编译时就会报错,这根本上杜绝了 runtime 时程序的风险,下面就是 POP 和 OOP 在动态派发时的对比:
// Objective-C下动态派发runtime报错实例
ViewController *vc = ...
[vc handleSomething];
TableViewController *tvc = ...
[tvc handleSomething];
NSObject *ob = ... // ob 没有实现handleSomething
NSArray *array = @[vc, tvc, ob];
for (id obj in array) {
[obj handleSomething]; // 能通过编译,但运行到ob时程序会崩溃
}
// Swift中使用了POP
let vc = ...
let tvc = ...
let ob = ...
let array: [SomethingHandleable] = [vc, tvc, ob] // 这里直接会报错,因为ob没有实现SomethingHandleable协议
- 协议可以用于值类型。相比于 OOP 只能用于 class,POP 可以用于 struct 和 enum 这样的值类型上。比如下面这个例子:
protocol Flyable { }
protocol Bird {
var name: String { get }
var canFly: Bool { get }
}
extension Bird {
var canFly: Bool { return self is Flyable }
}
struct ButterFly: Flyable {}
struct Penguin: Bird {
var name = "Penguin"
}
struct Eagle: Bird, Flyable {
var name = “Eagle”
}
enum FlyablePokemon: Flyable {
case Pidgey
case Duduo
}
POP 面试实战
4.要给一个 UIButton 增加一个点击后抖动的效果,该怎样实现?
关键词:#扩展 #协议
解决方案有三种。个人推荐用 protocol 来解决。
- 实现一个自定义的 UIButton 类,在其中添加点击抖动效果的方法(shake 方法);
- 写一个 UIButton 或者 UIView 的拓展(extension),然后在其中增加 shake 方法;
- 定义一个 protocol,然后在协议扩展(protocol extension)中添加 shake 方法;
分析这三种方法:
- 在自定义的类中添加 shake 方法扩展性不好。如果 shake 方法被用在其他地方,又要在其他类中再添加一遍 shake 方法,这样代码复用性差。
- 在 extension 中实现虽然解决了代码复用性问题,但是可读性比较差。团队开发中并不是所有人都知道这个 extension 中存在 shake 方法,同时随着功能的扩展,extension 中新增的方法会层出不穷,它们很难归类管理。
- 用协议定义解决了复用性、可读性、维护性三个难题。协议的命名(例如 Shakeable)直接可以确定其实现的 UIButton 拥有相应 shake 功能;通过协议扩展,可以针对不同类实现特定的方法,可维护性也大大提高;因为协议扩展通用于所有实现对象,所以代码复用性也很高。
5.优化以下代码
关键词:#Self #关联类型
代码语言:txt复制protocol Food {}
struct Fish: Food {}
struct Bone: Food {}
protocol Animal {
func eat(food: Food)
func greet(other: Animal)
}
struct Cat: Animal {
func eat(food: Food) {
guard let _ = food as? Fish else {
print("猫只吃鱼!")
return
}
}
func greet(other: Animal) {
if let _ = other as? Cat {
print("喵~")
} else {
print("猫很傲娇,不会对其他动物打招呼!")
}
}
}
struct Dog: Animal {
func eat(food: Food) {
guard let _ = food as? Bone else {
print("狗只啃骨头!")
return
}
}
func greet(other: Animal) {
if let _ = other as? Cat {
print("汪~")
} else {
print("狗很骄傲,不会像其他动物打招呼!")
}
}
}
首先理清这道题目的基本逻辑,有 2 个协议,分别是 Food 和 Animal,然后两个结构体 fish 和 bone 分别服从 food 协议,cat 和 dog 分别服从 animal 协议。
其中又有两个方法为 eat 和 greet,我们发现分别在 cat 和 dog 中,eat 方法有对应类型的参数,同时 greet 也对应类型的参数。所以假如 cat 和 dog 中能在服从 Animal 协议的同时,又写出对应自己类型的函数,那就可以省掉 if else 这类判断了。比如下面这样:
代码语言:txt复制struct Dog: Animal {
func eat(food: Bone) {}
func greet(other: Dog) { print("汪~") }
}
struct Cat: Animal {
func eat(food: Fish) {}
func greet(other: Cat) { print("喵~") }
}
很遗憾直接写成这样,程序是通不过编译的,Xcode 会提示,Cat 和 Dog 没有服从 Animal 协议,因为协议中要求的 food 必须是 Food,不能是 Bone 或者 Fish ,同理 greet 也是同样要求。但是我们可以用 Self 和关联类型去改进 Animal 协议,这样 Cat 和 Dog 这样写就没问题了。代码如下:
代码语言:txt复制protocol Animal {
associatedtype FoodType: Food
func greet(other: Self)
func eat(food: FoodType)
}
Self 相当于是 protocol 的占位符,它表示任意只要满足 Animal 的类型皆可。associatedType 就是关联类型,它实际上是一个类型的占位符,这样我们可以让 Dog 和 Cat 来指定 FoodType 到底是什么类型。而根据 greet 方法中对 FoodType 的使用,Swift 可以自动推断,FoodType 在 Cat 中是 Fish,在 Dog 中是 Bone。
6.试用 Swift 实现二分搜索算法
关键词:#Self #泛型
首先要审题,二分搜索算法,那么输入的对象是什么?是整型数组还是浮点型数组?如果输入不是排序过的数组该如何抛出异常?这些都是要在写答案之前与面试官探讨的问题。
我们先来热个身,假如面试官要求写出对于整型排序数组的二分搜索算法,则代码如下:
代码语言:txt复制func binarySearch(sortedElements: [Int], for element: Int) -> Bool {
var lo = 0, hi = sortedElements.count - 1
while lo <= hi {
let mid = lo (hi - lo) / 2
if sortedElements[mid] == element {
return true
} else if sortedElements[mid] < element {
lo = mid 1
} else {
hi = mid - 1
}
}
return false
}
上面的方法完成了面试官的要求,但是有如下几个问题。首先,这个方法只适用于整型数组;其次,虽然变量名为 sortedElements,但是我们无法保证输入的数组就一定是按序排列的。我们来看看用面向协议的编程来实现二分法:
代码语言:txt复制extension Array where Element: Comparable {
public var isSorted: Bool {
var previousIndex = startIndex
var currentIndex = startIndex 1
while currentIndex != endIndex {
if self[previousIndex] > self[currentIndex] {
return false
}
previousIndex = currentIndex
currentIndex = currentIndex 1
}
return true
}
}
func binarySearch<T: Comparable>(sortedElements: [T], for element: T) -> Bool {
// 确保输入数组是按序排列的
assert(sortedElements.isSorted)
var lo = 0, hi = sortedElements.count - 1
while lo <= hi {
let mid = lo (hi - lo) / 2
if sortedElements[mid] == element {
return true
} else if sortedElements[mid] < element {
lo = mid 1
} else {
hi = mid - 1
}
}
return false
}
上面解法首先在 Array 的扩展中加入了新变量 isSorted 用于判断输入的数组是否按序排列。之后在 binarySearch 的方法中运用了泛型,保证其中每一个元素都遵循 Comparable 协议,并且所有元素都是一个类型。有了上面的写法,我们可以将二分搜索法运用到各种类型的数组中,灵活性大大提高,例如:
代码语言:txt复制binarySearch(sortedElements: [1,4,7], for: 4) // true
binarySearch(sortedElements: [1.0,3.2,9.23], for: 3.2) // true
binarySearch(sortedElements: ["1","2","3"], for: "4") // false
binarySearch(sortedElements: ["4","2","3"], for: "4") // assert failure
当然,上面方法还可以进一步优化。例如 Array 的扩展可以放到 Collection 之中;isSorted 可以接受数学符号进行正反向排序查询;binarySearch 方法可以直接写到 Collection 的扩展中进行调用。总之,运用 POP 的思路,可以写出严谨、灵活的代码,其实用性和可读性也非常之好。
文章到这里就结束了,感谢你的观看,只是有些话想对读者们说说:
iOS开发人群越来越少,说实在的,每次在后台看到一些读者的回应都觉得很欣慰,至少你们依然坚守iOS技术岗…为了感谢读者们,我想把我收藏的一些编程干货贡献给大家,回馈每一个读者,希望能帮到你们。
干货主要有:
① iOS中高级开发必看的热门书籍(经典必看)
② iOS开发技术进阶教学视频
③ BAT等各个大厂iOS面试真题 答案.PDF文档
④ iOS开发中高级面试"简历制作"指导视频
如果你用得到的话可以直接拿走;如何获取,具体内容请转看-我的GitHub
我的:GitHub地址