写点什么

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

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 类型,用来解决完全相同的问题。因为dropFirst用Collection类型定义,所有的 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
}
}

上面的代码很短,运行正常。直到我们要测试loadUser和loadEpisode之前,没有什么问题。现在我们要么用 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)
}
}

现在,我们可以不必通过模拟任何东西来测试user和episode了:它们是简单的结构值。我们仍然需要测试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的方式。

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

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

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

我可以继续展示例子,但我希望希望你已经明确我的观点:多数情况下都有更简单的选择。更抽象地说,协议只是实现多态代码的一种方式。还有许多其他方法:子类、泛型、值、函数等。使用值(例如,一个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