25 May 2017

POP 一定比 OOP 好吗?

大多数介绍 Swift 面向协议编程概念的文章,都会拿面向对象中的类继承的设计与面向协议中遵守协议的设计进行对比,但都忽略了现有的面向对象语言中不仅仅只有类,还有接口,如 C# 中的 interface,类似于 Swiftprotocol;以及 abstract class 能够只声明方法或者同时定义方法实现,指定子类是否需要实现或重写部分方法。鉴于此,所谓的 protocol 带来的绝对的设计优势也就不复存在了。面向协议编程中所提倡的以协议入手解决问题的论证并不是绝对的,而要视具体问题而定。

Chris Lattner 也曾这么评价面向协议编程:

What irritates me is when people say classes are bad. Or subclassing is bad. Thatʼs totally false. Classes are super important. Reference semantics are super important. If anything, the thing thatʼs wrong is to say, one thing is bad and the other thing is good. These are all different tools in our toolbox, and theyʼre used to solve different kinds of problems.

那么面向协议编程真的毫无优势吗?并非如此。

面向协议编程 = 协议 + 值类型

使用协议组合来替换类继承(组合优于继承),消除多余的继承链,以及单继承带来的问题,比如在 iOS 程序中,通常我们会自定义继承自 UIViewControllerBaseViewController 用于子类型代码复用,但此时子类型无法再继承自 UITableViewController,这就导致了我们需要把复用的部分重新提起并实现一个新的继承自 UITableViewControllerBaseTableViewController,而这种问题在使用协议时是不存在的,因为子类型允许实现多个协议;使用协议扩展的方式来为协议提供默认实现(注意:只在扩展中定义的方法是静态分发的,想要支持动态分发,需要同时将方法定义在协议中);使用值类型可以避免部分因并发引起的资源竞争问题,同时值类型提供 Copy-On-Write 的机制以最大化地提高程序性能。

Swift 标准库中大量使用 struct 代替 class 的类型定义;使用 associatedtype 更好地表达设计中的类型关系(如类同时继承两个包含不同的类型参数的泛型接口);使用枚举关联值同时表达状态和描述。Swift 中的 struct 相比其它语言的实现上,功能被大大地增强了,完全可以等同于 class 使用。

泛型类型设计

在面向对象为主的编程语言中,如 C#,我们通常会这样设计泛型结构:

public class Animal<TFood>
{
    public virtual void Eat(TFood food) { }
}

public class Grass { }
public class Meat { }

public class Cow : Animal<Grass> { }
public class Tiger : Animal<Meat> { }

通过在定义基类时定义泛型参数 TFood,在具体的子类定义时指定泛型参数的具体类型 Grass,来实现参数的多态性。实现参数多态性的条件就是我们只能在 class 上定义泛型参数类型,而定义成类的形式就会导致继承结构间的强耦合,如果我们尝试将 class 定义改为 interface,则必须在每个实现该接口的类中定义 Eat 方法的实现,因为在 C# 中,我们无法扩展接口添加默认实现。Swift 为我们提供了协议扩展,可以解决上面的问题:

protocol Animal {
    associatedtype Food
    func eat(food: Food)
}

extension Animal {
    func eat(food: Food) { }
}

struct Grass { }
struct Meat { }

struct Cow: Animal {
    typealias Food = Grass
}
struct Tiger: Animal {
    typealias Food = Meat
}

SwiftProtocol 无法向 C# 中那样定义 Type Parameters,而是通过 associatedtype 替代了定义类型参数的方式,并为协议添加了默认实现,使得在子类型中可以选择使用默认的实现。

为什么要使用关联类型而不是类型参数?

类型参数在使用时会遇到一个问题,比如在 C# 中定义一个接口 IAnimal<Food>,如果我们实现了参数不同的该接口多次,会出现如下代码:

public interface IAnimal<TFood>
{
    void Eat(TFood food);
}
public class Dog : IAnimal<Meat>, IAnimal<Grass>
{
    public void Eat(Meat food){ }
    public void Eat(Grass food){ }
}

由于 C# 中方法重载机制,上面代码似乎并没什么明显的问题。如果我们将类型参数定义为返回值呢?因为重载机制是忽略返回值的。

public interface IAnimal<TFood>
{
    TFood Eat();
}
public class Dog : IAnimal<Meat>, IAnimal<Grass>
{
    public Meat Eat(){ ... }
    Grass IAnimal<Grass>.Eat(){ ... }
}

C# 将额外的接口实现定义为显示接口,只保留一个默认的接口实现。由于 Swift 中没有采用类型参数的设计方式,所以不会存在这种问题。

关联类型使用时的问题

关联类型定义的方式,同时引入了另外一个问题,就是我们再也无法直接使用 Animal 类型定义了,但可以用在类或者方法泛型约束上。

比如,在 C# 中我们直接可以这样使用 Animal

Animal<Grass> animal = new Cow();

但在 Swift 中我们没办法直接这么写:

let animal: Animal = Cow()

上面的代码会输出这样的错误:

protocol ‘Animal’ can only be used as a generic constraint because it has Self or associated type requirements

因为 Swift 是一门类型安全的语言,而当我们使用 Animal 时却无法指定它的 associatedtype,这就导致编译器无法在编译时刻得到具体的类型信息,拒绝编译通过。

想要解决上面这个问题,我们可以使用一点小技巧 type erasure,向下面这样:

struct AnyAnimal<Food>: Animal {
    
    private let _eat: ((Food) -> Void)
    
    init<T: Animal where T.Food == Food>(_ animal: T) {
        self._eat = animal.eat
    }
    
    func eat(food: Food){
        _eat(food)
    }
}

let animal: AnyAnimal = AnyAnimal<Grass>(Cow())

Real World Protocol / Protocol Oriented Programming is Not a Silver Bullet / Generic Protocols And Their Shortcomings / Why Associated Type / protocol & class hierarchies

Protocol-Oriented Programming in Swift
Building Better Apps with Value Types in Swift
Protocol and Value Oriented Programming in UIKit Apps