面向协议编程并非银弹

前言

本文翻译自Protocol Oriented Programming is Not a Silver Bullet
翻译的不对的地方还请多多包涵指正,谢谢~

面向协议编程并非银弹

为什么我们应该建设性地使用协议 (be critical of using protocols)

在Swift中,面向协议编程非常流行。有许多代码都是面向协议的,一些开源的库甚至声明它是库的一个特性。我认为协议在Swift中被过度使用了,有些问题可以简单的方式解决。简言之:不要教条式的使用(或避免)使用协议。

在2015 WWDC上最具影响力的章节之一是Protocol-Oriented Programming in Swift。它阐述了你可以用面向协议方案(就是说,遵循协议的协议或者类型)替代类继承(就是说,父类或者子类)。面向协议的方案更加简单,更加灵活。例如,类只能有一个父类,但类型可以遵循多个协议。

让我们来看看他们在WWDC演讲上说的问题。一些列的绘制命令需要作为图形被绘制,且需要输出到控制台。通过在协议内定义绘制命令,描述绘制的任何代码都可以被协议的方法进行解析。协议扩展能够允许你定义新的绘制功能,作为协议的基础功能,那么任意遵循该协议的类型都能免费的获得这个功能。

在以上例子中,协议解决了在多个类型间共享代码的问题。在Swift标准库中,协议重度地使用在集合中,他们解决也是相同的问题。因为Collection类型定义dropFirst方法,所有集合类型都免费的获得了这个方法~ 同时,有许多集合相关的类型和协议,找起来很困难。这就是协议其中一个缺点,但在标准库这个例子中协议的优势还是大于它的这个劣势。

现在,让我们通过一个例子来说明。这里,我们有一个Webservice的类。它使用URLSession从网络上下载实体。(实际上它并没有下载东西,仅用于说明)

class Webservice {
    func loadUser() -> User? {
        let json = self.load(URL(string: "/users/current")!)
        return User(json: json)
    }
    
    func loadEpisode() -> Episode? {
        let json = self.load(URL(string: "/episodes/latest")!)
        return Episode(json: json)
    }
    
    private func load(_ url: URL) -> [AnyHashable:Any] {
        URLSession.shared.dataTask(with: url)
        // etc.
        return [:] // should come from the server
    }
}

上述代码简单并工作地很好。它没有问题,直到我们希望测试loadUserloadEpisode的时候。现在我们要不存根加载,或者使用依赖注入的方式传一个模拟的请求进去。我们可以定义一个URLSession遵循的请求并在一个测试实例中传递进去。但是,在这个例子中,解决办法可以更简单:我们可以将需要改变的部分从Webservice抽离到一个结构体中(在Swift Talk Episode 1Advanced Swift也介绍过):

struct Resource {
    let url: URL
    let parse: ([AnyHashable:Any]) -> A
}

class Webservice {
    let user = Resource(url: URL(string: "/users/current")!, parse: User.init)
    let episode = Resource(url: URL(string: "/episodes/latest")!, parse: Episode.init)
    
    private func load(resource: Resource) -> A {
        URLSession.shared.dataTask(with: resource.url)
        // load asynchronously, parse the JSON, etc. For the sake of the example, we directly return an empty result.
        let json: [AnyHashable:Any] = [:] // should come from the server
        return resource.parse(json)
    }
}

现在,我们可以不通过模拟任何东西进行userepisode的测试:他们是简单的结构体类型。我们仍然不得不测试load方法,但那仅仅是一个方法(针对对每个资源的)。现在让我们来添加一些协议。

我们可以为类型定义一个能从JSON数据初始化的协议,而不是一个parse函数。

protocol FromJSON {
    init(json: [AnyHashable:Any])
}

struct Resource {
    let url: URL
}

class Webservice {
    let user = Resource(url: URL(string: "/users/current")!)
    let episode = Resource(url: URL(string: "/episodes/latest")!)
    
    private func load(resource: Resource) -> A {
        URLSession.shared.dataTask(with: resource.url)
        // load asynchronously, parse the JSON, etc. For the sake of the example, we directly return an empty result.
        let json: [AnyHashable:Any] = [:] // should come from the server
        return A(json: json)
    }
}

上述代码可能看起来更简洁,但它也缺少一些灵活性。例如,你怎样定义一个拥有User值数组的资源(Resource)呢?(在上述面向协议的例子中,还不可能,我们不得不等到Swift4或者5时才能看到)。协议可以使事情更加简单,但我认为这不能为其缺点买账,因为它极大地创建Resource的方式。

我们可以将Resource作为协议并创建遵循该协议的UserResourceEpisodeResource结构体,代替将user, episode作为Resource的值类型。这看起来是非常普遍的做法,因为拥有一个类型而不是一个值“感觉很合适”。

protocol Resource {
    associatedtype Result
    var url: URL { get }
    func parse(json: [AnyHashable:Any]) -> Result
}

struct UserResource: Resource {
    let url = URL(string: "/users/current")!
    func parse(json: [AnyHashable : Any]) -> User {
        return User(json: json)
    }
}

struct EpisodeResource: Resource {
    let url = URL(string: "/episodes/latest")!
    func parse(json: [AnyHashable : Any]) -> Episode {
        return Episode(json: json)
    }
}

class Webservice {
    private func load(resource: R) -> R.Result {
        URLSession.shared.dataTask(with: resource.url)
        // load asynchronously, parse the JSON, etc. For the sake of the example, we directly return an empty result.
        let json: [AnyHashable:Any] = [:]
        return resource.parse(json: json)
    }
}

但如果我们严格地审视,我们真正有获得了什么?代码变长,更加复杂且不直接。并且因为关联对象,最终我们很可能一个AnyResource。使用EpisodeResource结构体而不是episodeResource值真的有好处吗?他们都是全局定义的。对于结构体,名字是以大写字母开头,对于值类型,是小写字母。除此之外,几乎没有任何优点。他们都可以有命名空间(对于自动补全来说)。因此对于这个例子,使用值绝对是更简单,短小。

在围绕网络方面的代码中,有许多其他的例子。例如,我看到这样一个协议:

protocol URLStringConvertible {
    var urlString: String { get }
}

// Somewhere later

func sendRequest(urlString: URLStringConvertible, method: ...) {
    let string = urlString.urlString
}

这对你来说有什么好处呢?为什么不去掉协议直接传进urlString来呢?更简单的,看这样有单个方法的协议:

protocol RequestAdapter {
    func adapt(_ urlRequest: URLRequest) throws -> URLRequest
}

更为有争议的是:为什么不简单地去掉协议,在某处传递一个方法?更简单吧。(除非你的协议是一个仅适用于类的协议,且你希望若引用它)。

我可以继续举例子,但我希望观点已经非常清晰了。大多数来说,有更加简单的选择。更加抽象地说,协议仅仅是一种实现多态代码的方式。有许多其他的方法:子类,泛型,值,函数等等。值(例如,一个字符串而不是一个URLStringConvertible协议)是最简单的方式。函数(直接采用而不是RequestAdapter的协议)比值更加复杂一些,但仍然简单。泛型(没有任何限制)比协议更加简单。为完成某件事,协议相对类的层次来说通常更更加简单。

一个更具启发式方法可能是思考你的协议是对数据还是行为建模。对于数据,结构体可能更加简单。对于行为动作(比如:有很多方法的代理),协议通常更加简单。(标准库中的结合协议有点特殊:他们实际不是描述数据,而不是数据操作。)

也就是说,协议可以非常有用。但不要仅仅因为需要面向协议编程而先开始写协议。应该先审视你的问题,尽可能地用最简单的方式来解决它。让问题来驱动解决方案,而不是其他因素。面向协议编程本性并不是好或者坏。就像其他技术一样(函数式编程,面向对象,依赖注入,子类化)是用来解决问题,我们应当选择合适的工具进行工作。有时它是协议编程,但通常,有更简单的方案。

想了解更多:

Beyond Crusty: Real-World Protocols
Haskell Game Object Design - Or How Functions Can Get You Apples

你可能感兴趣的:(面向协议编程并非银弹)