写点什么

iOS 面试策略之经验之谈 - 面向协议的编程

用户头像
iOSer
关注
发布于: 2021 年 05 月 26 日
iOS 面试策略之经验之谈-面向协议的编程

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 开发中我们可以将共同的方法和变量定义在父类中,在子类继承时再各自实现对应功能,做到代码复用的高效运作。同时针对不同情况可以调用不同子类,大大增加代码的灵活性。


如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群101 295 1431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。

2.请谈谈 OOP 在 iOS 开发中的缺点

关键词:#内存 #继承


一般面试官这样问,我们不仅要回答出缺点,还要说出一个比较成熟的解决方案。一个专业的程序员不仅要知道问题出在哪里,更要知道该怎么修正问题。


OOP 有以下几个缺点:


  • **隐式共享。**class 是引用类型,在代码中某处改变某个实例变量的时候,另一处在调用此变量时就会受此修改影响。示例代码如下:


class People { var name = “”}// 创建张三,设置其名字为张三let zhangSan = People()zhangSan.name = “张三”
// 创建李四,设置其名字为李四let liSi = zhangSanLisi.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 真实的实例,减少对于对象及其实现的依赖。比如下面这个实例:


如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群101 295 1431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。


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 没有实现handleSomethingNSArray *array = @[vc, tvc, ob];for (id obj in array) { [obj handleSomething]; // 能通过编译,但运行到ob时程序会崩溃}
// Swift中使用了POPlet 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 面试实战

如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群101 295 1431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。

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 #关联类型


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 协议。


如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群101 295 1431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。


其中又有两个方法为 eat 和 greet,我们发现分别在 cat 和 dog 中,eat 方法有对应类型的参数,同时 greet 也对应类型的参数。所以假如 cat 和 dog 中能在服从 Animal 协议的同时,又写出对应自己类型的函数,那就可以省掉 if else 这类判断了。比如下面这样:


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 这样写就没问题了。代码如下:


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 #泛型


首先要审题,二分搜索算法,那么输入的对象是什么?是整型数组还是浮点型数组?如果输入不是排序过的数组该如何抛出异常?这些都是要在写答案之前与面试官探讨的问题。


我们先来热个身,假如面试官要求写出对于整型排序数组的二分搜索算法,则代码如下:


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}
复制代码


如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群101 295 1431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。


上面的方法完成了面试官的要求,但是有如下几个问题。首先,这个方法只适用于整型数组;其次,虽然变量名为 sortedElements,但是我们无法保证输入的数组就一定是按序排列的。我们来看看用面向协议的编程来实现二分法:


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 协议,并且所有元素都是一个类型。有了上面的写法,我们可以将二分搜索法运用到各种类型的数组中,灵活性大大提高,例如:


binarySearch(sortedElements: [1,4,7], for: 4)            // truebinarySearch(sortedElements: [1.0,3.2,9.23], for: 3.2)   // truebinarySearch(sortedElements: ["1","2","3"], for: "4")    // falsebinarySearch(sortedElements: ["4","2","3"], for: "4")    // assert failure
复制代码


当然,上面方法还可以进一步优化。例如 Array 的扩展可以放到 Collection 之中;isSorted 可以接受数学符号进行正反向排序查询;binarySearch 方法可以直接写到 Collection 的扩展中进行调用。总之,运用 POP 的思路,可以写出严谨、灵活的代码,其实用性和可读性也非常之好。

## 文章到这里就结束了,感谢你的观看,只是有些话想对读者们说说:

iOS 开发人群越来越少,说实在的,每次在后台看到一些读者的回应都觉得很欣慰,至少你们依然坚守 iOS 技术岗…为了感谢读者们,我想把我收藏的一些编程干货贡献给大家,回馈每一个读者,希望能帮到你们。

干货主要有:

① iOS 中高级开发必看的热门书籍(经典必看)

② iOS 开发技术进阶教学视频

③ BAT 等各个大厂 iOS 面试真题+答案.PDF 文档

④ iOS 开发中高级面试"简历制作"指导视频

**如果你用得到的话可以直接拿走;如何获取,具体内容请转看-我的**[**GitHub**](https://github.com/iOS-Mayday/iOSAdvanced-Roadmap)

**我的:**[**GitHub 地址**](https://github.com/iOS-Mayday/iOSAdvanced-Roadmap)

用户头像

iOSer

关注

微信搜索添加微信 mayday1739 进微信群哦 2020.09.12 加入

更多大厂面试资料进企鹅群931542608

评论

发布
暂无评论
iOS 面试策略之经验之谈-面向协议的编程