前言
本文翻译自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
}
}
上述代码简单并工作地很好。它没有问题,直到我们希望测试loadUser
和loadEpisode
的时候。现在我们要不存根加载,或者使用依赖注入的方式传一个模拟的请求进去。我们可以定义一个URLSession
遵循的请求并在一个测试实例中传递进去。但是,在这个例子中,解决办法可以更简单:我们可以将需要改变的部分从Webservice
抽离到一个结构体中(在Swift Talk Episode 1
及Advanced Swift
也介绍过):
struct Resource<A> {
let url: URL
let parse: ([AnyHashable:Any]) -> A
}
class Webservice {
let user = Resource<User>(url: URL(string: "/users/current")!, parse: User.init)
let episode = Resource<Episode>(url: URL(string: "/episodes/latest")!, parse: Episode.init)
private func load<A>(resource: Resource<A>) -> 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)
}
}
现在,我们可以不通过模拟任何东西进行user
和episode
的测试:他们是简单的结构体类型。我们仍然不得不测试load
方法,但那仅仅是一个方法(针对对每个资源的)。现在让我们来添加一些协议。
我们可以为类型定义一个能从JSON数据初始化的协议,而不是一个parse
函数。
protocol FromJSON {
init(json: [AnyHashable:Any])
}
struct Resource<A: FromJSON> {
let url: URL
}
class Webservice {
let user = Resource<User>(url: URL(string: "/users/current")!)
let episode = Resource<Episode>(url: URL(string: "/episodes/latest")!)
private func load<A>(resource: Resource<A>) -> 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
作为协议并创建遵循该协议的UserResource
和EpisodeResource
结构体,代替将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<R: Resource>(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