面向协议编程并非银弹

银弹(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从网络加载实体。(实际上并不加载东西,领会意思即可):


  1. class Webservice { 
  2.   func loadUser() -> User? { 
  3.     let json = self.load(URL(string: "/users/current")!) 
  4.     return User(json: json) 
  5.   } 
  6.   func loadEpisode() -> Episode? { 
  7.     let json = self.load(URL(string: "/episodes/latest")!) 
  8.     return Episode(json: json) 
  9.   } 
  10.   private func load(_ url: URL) -> [AnyHashable:Any] { 
  11.     URLSession.shared.dataTask(with: url) 
  12.     // etc. 
  13.     return [:] // should come from the server 
  14.   } 

上面的代码很短,运行正常。直到我们要测试loadUser和loadEpisode之前,没有什么问题。现在我们要么用stub方法来模拟load,要么通过依赖注入来传入一个模拟的URLSession。我们还可以定义一个符合URLSession的协议,然后传递一个测试实例。不过在这个案例中,我们采用更简单的解决方案,将Webservice更改的部分取出并转换为结构体:


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

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

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


  1. protocol FromJSON { 
  2.   init(json: [AnyHashable:Any]) 
  3. struct Resource<A: FromJSON> { 
  4.   let url: URL} 
  5. class Webservice { 
  6.   let user = Resource<User>(url: URL(string: "/users/current")!) 
  7.   let episode = Resource<Episode>(url: URL(string: "/episodes/latest")!) 
  8.   private func load<A>(resource: Resource<A>) -> A { 
  9.     URLSession.shared.dataTask(with: resource.url) 
  10.     // load asynchronously, parse the JSON, etc. For the sake of the example, we directly return an empty result. 
  11.     let json: [AnyHashable:Any] = [:] // should come from the server 
  12.     return A(json: json) 
  13.   } 

上面的代码可能看起来更简单,但灵活性也大大降低。例如,你如何定义一个具有User值的数组资源?(上述面向协议的例子中,是不可能实现的,我们必须等待Swift 4或5,直至可用。)协议使代码得以简化,但我认为它不为自身买单,因为它大大减少了我们可以创建一个Resource的方式。

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


  1. protocol Resource { 
  2.   associatedtype Result 
  3.   var url: URL { get } 
  4.   func parse(json: [AnyHashable:Any]) -> Result} 
  5. struct UserResource: Resource { 
  6.   let url = URL(string: "/users/current")! 
  7.   func parse(json: [AnyHashable : Any]) -> User { 
  8.     return User(json: json) 
  9.   } 
  10. struct EpisodeResource: Resource { 
  11.   let url = URL(string: "/episodes/latest")! 
  12.   func parse(json: [AnyHashable : Any]) -> Episode { 
  13.     return Episode(json: json) 
  14.   } 
  15. class Webservice { 
  16.   private func load<R: Resource>(resource: R) -> R.Result { 
  17.     URLSession.shared.dataTask(with: resource.url) 
  18.     // load asynchronously, parse the JSON, etc. For the sake of the example, we directly return an empty result. 
  19.     let json: [AnyHashable:Any] = [:] 
  20.     return resource.parse(json: json) 
  21.   } 

但如果我们仔细看看,我们真正得到了什么?代码变得更冗长、复杂、不直观。并且由于关联类型,结果最后我们可能定义一个AnyResource。EpisodeResource结构和episodeResource值有什么区别呢?它们都是全局定义的。对于结构体,名称以大写字母开头;而对于值,则使用小写字母。除此之外,结构真的没有任何优势。你可以将它们加入命名空间(自动补全)。所以在这种情况下,有一个值肯定会更简短。

我在网上看到的很多代码例子。例如,我看到这样的协议:


  1. protocol URLStringConvertible { 
  2.   var urlString: String { get } 
  3. // Somewhere laterfunc sendRequest(urlString: URLStringConvertible, method: ...) { 
  4.   let string = urlString.urlString 

是什么打动了你?为什么不简单地删除协议并直接传递urlString呢?这样就简单多了。或者,一个单一方法的协议:


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

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

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

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

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

作者:刘志勇

来源:51CTO

时间: 2017-08-03

面向协议编程并非银弹的相关文章

在 Swift 3 上对视图控件实践面向协议编程

本文讲的是在 Swift 3 上对视图控件实践面向协议编程, 学习如何对 button, label, imageView 创建动画而不制造一串乱七八糟的类 你可能听人说过,学到了知识却缺失了行动就好比人长了牙却还老盯着奶喝一样.那好,我们要怎样开始在我的应用中实践面向协议编程? 为了能更加高效的理解下面的内容,我希望读者能够明白 Complection Handlers,并且能创建协议的基本实现.如果你还不熟悉他们,可以先查看下面的文章和视频再回来接着看: 前景提要: Intro to Pro

Swift 面向协议编程入门

本文讲的是Swift 面向协议编程入门, 面向对象编程的思想没毛病,但老铁你可以更 666 的 上图这个人不是我,但这就是使用面向协议编程替换掉面向对象编程之后的感觉. 介绍 这个教程也是为了那些不知道类和结构体根本区别的人写的.我们都知道在结构体里是没有继承的,但是为什么没有呢? 如果你不知道上面问题的答案,那么花几秒钟看下下面的代码.请再次原谅我的排版,我已经让它尽可能的简单明了了. 注:译者已经改过排版了 class HumanClass { var name: String init(n

面向协议编程,灵丹妙药或是饮鸩止渴?

本文讲的是面向协议编程,灵丹妙药或是饮鸩止渴?, 在 Swift 中,面向协议编程正值流行.许多 Swift 框架都自称是面向协议编程的,一些开源库甚至将其标榜为特点.而我认为,很多时候眼下的问题本可以用一种更简单的方法解决,但是在 Swift 中我们过度使用各种协议了.简言之:不要教条地使用(或避免)协议. WWDC 2015(苹果电脑全球研发者大会,译者注)中最有影响力的一个分会场就是 Swift 中的面向协议编程.会议表明(当然还有其他内容)你能够用一个面向协议的解决方案替换掉类的层次结构

Swift 中的面向协议编程是如何点亮我的人生的

本文讲的是Swift 中的面向协议编程是如何点亮我的人生的, 面向对象编程至今已经使用了数十年了,并且成为了构建大型软件约定俗成的标准.作为iOS编程的中心思想,遵循面向对象规范来编写一个 iOS 的应用几乎不可能实现.虽然面向对象有很多优点比如封装性,访问控制和抽象性,但是它也自带有固有的缺点. 大多数类的情况下,当一个单继承的类需要更多不同类中的函数功能时,你会倾向于使用多继承来实现. 但是大部分的编程语言不支持这一特性,而且会导致类的继承关系变得复杂. 在多线程环境下,如果所有对象在函数中

依赖注入(DI)有助于应用对象之间的解耦,而面向切面编程(AOP)有助于横切关注点与所影响的对象之间的解耦(转good)

依赖注入(DI)有助于应用对象之间的解耦,而面向切面编程(AOP)有助于横切关注点与所影响的对象之间的解耦.所谓横切关注点,即影响应用多处的功能,这些功能各个应用模块都需要,但又不是其主要关注点,常见的横切关注点有日志.事务和安全等. 将横切关注点抽离形成独立的类,即形成了切面.切面主要由切点和通知构成,通知定义了切面是什么,以及何时执行何种操作:切点定义了在何处执行通知定义的操作. http://ju.outofmemory.cn/entry/216839 引子: AOP(面向方面编程:Asp

面向服务编程

    从最初的面向过程编程,到后来觉得难以理解的面向对象编程,从软件工程的发展历程中来看,这已经成为我们编程路上熟知的两种编程方式.     接触了ITOO项目这么长时间,才发现,不知不觉,我们已经进入了软件工程发展历程之面向服务编程的开发.本篇博客的主题便是面向服务编程.     [一.面向服务编程从何而来?]     想要了解面向服务编程的发展方向以及它在软件行业中所占的地位,我们首先要了解的便是它的起源和发展.没有任何一种方法是一蹴而就的,应该是经历了数十年渐进的演化历程.下面,我们就来

我们来谈谈面向指针编程的那些事

面向对象编程,面向设计模式编 程(亦即设计模式),面向接口编程,面向模板编程(亦即泛型编程),面向函数编程(亦即函数式编程),面向多核时代的并行编程,面向大数据的机器学习编 程--这么多年,大家要面向的东西已经够多了,然而我看到的现象是,很多编程语言让大家面向 xxx 的同时在竭力回避指针.我可不想面向这么多东西,所以我只好加入指针的黑暗势力.我要不自量力的来写一篇<面向指针编程>作为投名状,借以表示我与软件世 界的光明势力的彻底决裂. 这个世界上,提供指针的编程语言很少,这样的语言有汇编语言

Web Service——面向服务编程的方式之一

    在上篇博客中,我们认识了面向服务编程.本篇博客,将学习Web Service,它属于实现面向服务编程的方式之一.     [一.什么是Web Service?]     Web Service也叫XML WebService,是一种可以接收从Internet或者Intranet上的其它系统中传递过来的请求,轻量级的独立的通讯技术.     从生活中举一个简单的例子,我们平时在浏览不同的网站的时候,都会看见很多相同网站的身影,换句话说,我们在各种网站都可以看见百度.淘宝等,这便是他们将自己

C#面向服务编程技术WCF从入门到实战演练

  一.WCF课程介绍 1.1.Web Service会被WCF取代吗? 对于这个问题阿笨的回答是:两者在功能特性上却是有新旧之分,但是对于特定的系统,适合自己的就是最好的.不能哪一个技术框架和行业标准作比较,任何对于二者的比较都是错误的,因为两者根不不在同一个范畴里.就好比不能拿个汽车和交通法规比较一样,这是个误区. 阿笨的宗旨就是学完此<C#面向服务编程技术WCF从入门到实战演练>课程,让您从零基础上手后直接将学习的成果运用到实际项目中去.阿笨本次分享的WCF技术是完全来源于切身实际项目中