写点什么

面向协议编程并不是一颗银弹

2016 年 12 月 28 日

为什么我们对使用协议感到不满

本文来自于《Advanced Swift》作者 Chris Edihof 博客,经作者授权同意 InfoQ 中文站翻译并发布。

InfoQ 注:标题中银弹一词(Silver Bullet)出自 IBM 大型机之父 Frederick P. Brooks, Jr. 在 1986 年发表的一篇关于软件工程的经典论文,便以《没有银弹:软件工程的本质性与附属性工作》(No Silver Bullet — Essence and Accidents of Software Engineering)为标题。其中的“银弹”是指一项可使软件工程的生产力在十年内提高十倍的技术或方法。该论文强调由于软件的复杂性本质,而使这样“真正的银弹”并不存在。

银弹在软件工程中的含义是指妄图创造某种便捷的开发技术,从而使某个项目的实施提高效率。又或者指摆脱该项目的本质或核心,而达到超乎想象的成功。但这么做的结果却是徒劳的。

在本文中 Chris 介绍了 Swift 中的面向协议编程的滥用情况,认为很多时候有更简单的解决办法,面向协议编程并非银弹。

在 Swift 语言中,面向协议编程很流行。在“面向协议”那儿有很多 Swift 代码,一些开源库甚至将其声明为功能。我觉得协议在 Swift 中被过度滥用了,其实问题常常可以用更简单的方式来解决。简而言之,就是不要生搬硬套协议的条条框框,而不知变通。

在 WWDC2015 上苹果推出了一个 Session 叫 Swift 的面向协议编程,它成了这届大会上最有影响力的 Session 之一。
它表明了除某些情况外,用户可以使用面向协议的解决方案(即协议和一些符合协议的类型)来替换类层次结构(即超类和一些子类)。面向协议的解决方案更简单、更灵活。例如,一个类只能有一个超类,但一个类型可以符合许多协议。

让我们来看看他们在 WWDC 演讲中解决的这个问题:一系列绘图命令需要渲染成图像,并将指令记录到控制台。通过将绘图命令放在协议中,描述绘图的任何代码可以根据协议的方法来表述。协议扩展允许你根据协议的基本功能定义新的绘图功能,并且每个符合的类型都可以自动获得新的功能。

在上述例子中,协议解决了多种类型之间共享代码的问题。在 Swift 的标准库中,协议主要用于 Collection 类型,用来解决完全相同的问题。因为dropFirstCollection类型定义,所有的 Collection 类型都能自动得到它。与此同时,标准库中定义了太多的 Collection 相关的协议和类型,当我们想找东西时会面临困难。这是协议的一个缺点,然而,在标准库的情况下还是利大于弊。

现在,让我们通过一个例子来开始。这里有一个 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之前,没有什么问题。现在我们要么用 stub 方法来模拟load,要么通过依赖注入传递来模拟URLSession。我们还可以定义一个符合URLSession的协议,然后传递一个测试实例。不过在这个案例中,我们采用更简单的解决方案,将 Webservice 更改的部分取出并转换为结构体:

复制代码
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)
}
}

现在,我们可以不必通过模拟任何东西来测试userepisode了:它们是简单的结构值。我们仍然需要测试load,但只有这一个方法需要写测试(而不是为每个资源)。现在让我们来添加一些协议。

取代parse函数,我们可以为能够从 JSON 初始化的类型创建一个协议。

复制代码
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值的数组资源?(上述面向协议的例子中,是不可能实现的,我们必须等待 Swift 4 或 5,直至可用。)协议使代码得以简化,但我认为它不为自身买单,因为它大大减少了我们可以创建一个Resource的方式。

代替将userepisode作为Resource值,我们还可以使Resource成为协议并具有UserResourceEpisodeResource结构。这似乎是一个很流行的做法,因为拥有类型比只是一个值来说,“就是感觉要对一些”:

复制代码
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)
}
}

但如果我们仔细看看,我们真正得到了什么?代码变得更冗长、复杂、不直观。并且由于关联类型,结果最后我们可能定义一个AnyResourceEpisodeResource结构和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
}

有些争议的是:为什么不简单地删除协议,并在某处传递函数?这样岂不是更简单。(除非你的协议是一个类的协议,你想要一个弱引用)。

我可以继续展示例子,但我希望希望你已经明确我的观点:多数情况下都有更简单的选择。更抽象地说,协议只是实现多态代码的一种方式。还有许多其他方法:子类、泛型、值、函数等。使用值(例如,一个String,而不是一个URLStringConvertible)是最简单的方法。函数(例如adapt而不是RequestAdapter)比值复杂一点,但仍然很简单。泛型(无任何限制)比协议简单。为了完成代码,协议通常比类层次结构更简单。

一个有用的启发是,也许是考虑您的协议是依照数据还是行为来建模。对于数据,结构可能更容易。对于复杂的行为(例如,具有多个方法的委托),协议通常更容易。(标准库的 collection 协议有点特别:它们并不真正描述数据,而是描述数据操作。)

也就是说,协议可能非常有用。但不要为了面向协议编程而编程。首先要审视你的问题,并尝试以最简单的方式来解决它。让问题推动解决方案,而不是相反。面向协议编程本身无所谓好与坏。就像任何其他技术(函数式编程,OO,依赖注入,子类化)一样,它可以用来解决一个问题,我们应该尝试选择合适的工具。有时这是一个协议,但往往,有一个更简单的方法。

其他

如果您喜欢这篇文章,可以参阅我们的书《 Advanced Swift 》(已更新到 Swift 3)或者中文电子版:《 Swift 进阶》(王巍 译),或者观看视频系列《 Swift Talk 》。


感谢徐川对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2016 年 12 月 28 日 16:081209
用户头像

发布了 325 篇内容, 共 120.5 次阅读, 收获喜欢 802 次。

关注

评论

发布
暂无评论
发现更多内容

阿里聚划算5轮面试题:GC收集器、多线程锁、海量数据技术考核

Java架构之路

Java 程序员 架构 面试 编程语言

Java开发者必读的〈Java开发手册(嵩山版)〉灵魂15问,深究Java规约背后的原理。

Java成神之路

Java 程序员 架构 面试 编程语言

一只支持凡尔赛文学创作的摄影手机

脑极体

网络篇:朋友面试之TCP/IP,回去等通知吧

Crud的程序员

TCP 网络协议 IP

2020年我凭借这份pdf成功拿到了阿里,腾讯,京东等六家大厂offer

Crud的程序员

Java 阿里巴巴 程序员 java面试 offer

图解MyBatis

田维常

《架构即未来:现代企业可扩展的Web架构流程和组织》.pdf

田维常

架构

2020最新最全的Java架构面试复习指南,掌握10%阿里P7没问题

Java架构之路

Java 程序员 架构 面试 编程语言

2020年高频Java面试题集锦(含答案),让你的面试之路畅通无阻!

Java成神之路

Java 程序员 架构 面试 编程语言

LeetCode题解:22. 括号生成,BFS,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

架构师训练营第八周作业

丁乐洪

《前端算法系列》如何让前端代码速度提高60倍

徐小夕

Java 算法 前端 前端进阶

架构训练营-week12-作业1

于成龙

Java内存模型JMM详细解析

云流

程序员 并发编程 架构师 java面试

2020的另一面:5G的斯普特尼克之年

脑极体

命令行搜索神器fzf

Rayjun

Linux

刚参加完阿里P6面试归来(Offer已斩获),6点面试经验总结

Java架构之路

Java 程序员 架构 面试 编程语言

Github上标星30K+的SpringBoot实战电商项目,简直不要太牛!

Java成神之路

Java 程序员 架构 面试 编程语言

陪你手撕源码系列之 STL set 相关算法

herongwei

c++ 算法 set stl

OSI七层模型与TCP/IP五层模型

Linux服务器开发

TCP/IP 网络协议栈 底层应用开发 Linux服务器开发 OSI

架构师训练营第 12 周学习总结

netspecial

极客大学架构师训练营

架构训练营-week-12总结

于成龙

架构训练营

100+大厂应届offer,从7个维度全面分析

程序员小灰

编程 面试 面经 腾讯大厂

Gradle使用问题梳理

maijun

Gradle

面试官:简单说一下RocketMQ整合SpringBoot吧

比伯

Java 编程 程序员 架构 计算机

什么?还不知道该如何学习微服务?这份Github上星标55.9k的微服务神仙笔记真的太香了!

Java成神之路

Java 程序员 架构 面试 编程语言

真的爱了!这份阿里P8整理的《Java核心技术整理》新版手抄本,简直把所有Java知识操作都写出来了

Java成神之路

Java 程序员 架构 面试 编程语言

架构师训练营第 12 周作业

netspecial

极客大学架构师训练营

TCC Demo 代码实现

Java 分布式事务 Demo TCC

spring2.5.6+java6升级到spring4+java8了

阿水

Java spring 升级

怎么保护自己的音乐作品不被盗用,用FL制作防盗水印片段

懒得勤快

版权保护 音乐 音乐制作 编曲

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

面向协议编程并不是一颗银弹-InfoQ