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 是引用类型,在代码中某处改变某个实例变量的时候,另一处在调用此变量时就会受此修改影响。示例代码如下:
这很容易就造成异常。尤其是在多线程时,我们经常遇到的资源竞速(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 来实现多态。同时测试时也可以利用 protocol 来 mock 真实的实例,减少对于对象及其实现的依赖。比如下面这个实例:
如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群101 295 1431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。
**消除动态分发的风险。**对于服从了 protocol 的类或结构体来说,它必须实现 protocol 声明的所有方法。否则编译时就会报错,这根本上杜绝了 runtime 时程序的风险,下面就是 POP 和 OOP 在动态派发时的对比:
**协议可以用于值类型。**相比于 OOP 只能用于 class,POP 可以用于 struct 和 enum 这样的值类型上。比如下面这个例子:
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 #关联类型
首先理清这道题目的基本逻辑,有 2 个协议,分别是 Food 和 Animal,然后两个结构体 fish 和 bone 分别服从 food 协议,cat 和 dog 分别服从 animal 协议。
如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群101 295 1431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。
其中又有两个方法为 eat 和 greet,我们发现分别在 cat 和 dog 中,eat 方法有对应类型的参数,同时 greet 也对应类型的参数。所以假如 cat 和 dog 中能在服从 Animal 协议的同时,又写出对应自己类型的函数,那就可以省掉 if else 这类判断了。比如下面这样:
很遗憾直接写成这样,程序是通不过编译的,Xcode 会提示,Cat 和 Dog 没有服从 Animal 协议,因为协议中要求的 food 必须是 Food,不能是 Bone 或者 Fish ,同理 greet 也是同样要求。但是我们可以用 Self 和关联类型去改进 Animal 协议,这样 Cat 和 Dog 这样写就没问题了。代码如下:
Self 相当于是 protocol 的占位符,它表示任意只要满足 Animal 的类型皆可。associatedType 就是关联类型,它实际上是一个类型的占位符,这样我们可以让 Dog 和 Cat 来指定 FoodType 到底是什么类型。而根据 greet 方法中对 FoodType 的使用,Swift 可以自动推断,FoodType 在 Cat 中是 Fish,在 Dog 中是 Bone。
6.试用 Swift 实现二分搜索算法
关键词:#Self #泛型
首先要审题,二分搜索算法,那么输入的对象是什么?是整型数组还是浮点型数组?如果输入不是排序过的数组该如何抛出异常?这些都是要在写答案之前与面试官探讨的问题。
我们先来热个身,假如面试官要求写出对于整型排序数组的二分搜索算法,则代码如下:
如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群101 295 1431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。
上面的方法完成了面试官的要求,但是有如下几个问题。首先,这个方法只适用于整型数组;其次,虽然变量名为 sortedElements,但是我们无法保证输入的数组就一定是按序排列的。我们来看看用面向协议的编程来实现二分法:
上面解法首先在 Array 的扩展中加入了新变量 isSorted 用于判断输入的数组是否按序排列。之后在 binarySearch 的方法中运用了泛型,保证其中每一个元素都遵循 Comparable 协议,并且所有元素都是一个类型。有了上面的写法,我们可以将二分搜索法运用到各种类型的数组中,灵活性大大提高,例如:
当然,上面方法还可以进一步优化。例如 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)
评论