前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >iOS 面试策略之经验之谈-面向协议的编程

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

原创
作者头像
会写bug的程序员
修改2021-05-25 17:48:47
1.2K0
修改2021-05-25 17:48:47
举报
文章被收录于专栏: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 开发中我们可以将共同的方法和变量定义在父类中,在子类继承时再各自实现对应功能,做到代码复用的高效运作。同时针对不同情况可以调用不同子类,大大增加代码的灵活性。

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

关键词:#内存 #继承

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

OOP 有以下几个缺点:

  • 隐式共享。class 是引用类型,在代码中某处改变某个实例变量的时候,另一处在调用此变量时就会受此修改影响。示例代码如下:
代码语言:txt
复制
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 会越变越冗杂。当然我们也可以引入一个专门的父类或工具类,但是依然有职权不明确、依赖、冗杂等多种问题。
代码语言:txt
复制
另一方面,父类中的 handleSomething() 方法必须由具体实现,它不能根据子类做出灵活调整。子类如果要做特定操作,必须要重写方法来实现。既然子类要重写,那么在父类中的实现在这种时候就显得多此一举。解决方案使用 protocol,这样它的方法就不需要用具体实现了,交给服从它的类或结构体即可。
  • 多继承。 Swift 和 Objective-C 是不支持多继承的,因为这会造成菱形问题,即多个父类实现了同一个方法,子类无法判断继承哪个父类的情况。在 Java 中,有 interface 的解决方案,Swift 中有类似 protocol 的解决方案。

2.说说 POP 相比于 OOP 的优势

关键词:#灵活 #安全

这道题是一个开放性的问题。在面试中一个很好的回答方式是理论+举例。POP 相比 OOP 具有如下优势。

  • 更加灵活。比如上题中我们提到的冗杂的父类的例子。我们可以用协议和其扩展来让所有服从此协议的 class 都可以用到默认的 handleSomething() 方法,同时服从了该协议的同时也增加了代码的可读性。具体代码如下:
代码语言:txt
复制
protocol SomethingHandleable {
  func handleSomething()
}

extension SomethingHandleable {
  func handleSomething() {
    // 实现
  }
} 

class ViewController: UIViewController, SomethingHandleable { }
class TableViewController: UITableViewController, SomethingHandleable { }
  • 减少依赖。相对于传入具体的实例变量,我们可以传入 protocol 来实现多态。同时测试时也可以利用 protocol 来 mock 真实的实例,减少对于对象及其实现的依赖。比如下面这个实例:
代码语言:txt
复制
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 在动态派发时的对比:
代码语言:txt
复制
// 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 这样的值类型上。比如下面这个例子:
代码语言:txt
复制
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地址

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • POP vs OOP
    • 1.什么是 OOP ?它在 iOS 开发中有哪些优点?
      • 2.请谈谈 OOP 在 iOS 开发中的缺点
        • 2.说说 POP 相比于 OOP 的优势
        • POP 面试实战
          • 4.要给一个 UIButton 增加一个点击后抖动的效果,该怎样实现?
            • 5.优化以下代码
              • 6.试用 Swift 实现二分搜索算法
              • 文章到这里就结束了,感谢你的观看,只是有些话想对读者们说说:
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档