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

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


在 Swift 中,面向协议编程正值流行。许多 Swift 框架都自称是面向协议编程的,一些开源库甚至将其标榜为特点。而我认为,很多时候眼下的问题本可以用一种更简单的方法解决,但是在 Swift 中我们过度使用各种协议了。简言之:不要教条地使用(或避免)协议。

WWDC 2015(苹果电脑全球研发者大会,译者注)中最有影响力的一个分会场就是 Swift 中的面向协议编程。会议表明(当然还有其他内容)你能够用一个面向协议的解决方案替换掉类的层次结构。面向协议的解决方案即一个协议定义和适用于该协议的类型,而类的层次结构即父类和子类的结构。面向协议的解决办法更简单灵活,比如,一个类只能有一个父类,但是一个类型却能适应多种协议。

我们来看看他们在 WWDC 会议上解决的那个问题。一系列的绘图命令都需要被渲染成图像,也要被记录到控制台。通过将绘图命令嵌入协议,任何描述图像的代码都可以用协议的方法来表达。协议扩展使得你能在基础功能上定义新的功能,每一个符合协议的类型都能够自由获取新的功能。

在上面的例子中,协议解决了在多个类型中间共享代码的难题。在 Swift 的标准库中,协议被大量用于集合,并解决了相同的问题。因为 dropFirst 是在 Collection 类型中定义的,所有的集合类型都能自由获取!同样的,太多集合相关的协议和类型,也会使得查找变得困难。这是协议的一个弊端,但在标准库这个例子中,还是优势更多一些。

现在,让我们从实践中得出真知。有一个网络服务的类,它通过 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)

            // 略

            return [:] // 来自服务器的内容
        }
    }

以上的代码简短有效,直到我们想要测试 loadUser 和 loadEpisode 的时候,出现了问题。现在我们或者去掉 load,或者用依赖注入的方式传入一个模拟的 URLSession。我们也可以定义一个 URLSession 适用的协议,然后传入一个测试实例。但就此而言,有更简单的解决方法:我们能将变化的部分从 Webservice 中抽离出来,并且写入一个结构类型(我们在 Swift Talk Episode 1 和 Advanced 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)

            // 异步加载,解析 JSON 等等,仅作为实例,我们直接返回一个空的结果

            let json: [AnyHashable:Any] = [:] // 来自服务器的内容
            return resource.parse(json)
        }
    }

现在,我们能够测试 user 和 episode 而免于虚拟任何内容:他们是简单的结构类型的值。我们还是要测试 load,但是只有一个方法(而不是与资源一一对应)。现在,我们来添加一些协议。

为了不用 parse 函数,我们可以创建一个使用 JSON 初始化类型的协议。

    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)

            // 异步加载,解析 JSON 等等,仅作为实例,我们直接返回一个空的结果

            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(resource: R) -> R.Result {
            URLSession.shared.dataTask(with: resource.url)

            // 异步加载,解析 JSON 等等,仅作为实例,我们直接返回一个空的结果

            let json: [AnyHashable:Any] = [:]
            return resource.parse(json: json)
        }
    }

但是我们批判性地来看,我们真正得到了什么?代码变得冗长、复杂、间接,而且由于关联类型我们很可能最终要定义一个AnyResource。那么得到一个 EpisodeResource 结构比得到一个 episodeResource 值有什么益处么?他们都是全局定义,结构类型中命名是以大写字母开头,而值类型的命名是以小写字母开头,除了这,两者无异,而且你可以给它们都定义命名空间 (为了支持自动完成)。因此,得到一个值显然更简单、代码也更短。

网上还有许多的例子,比如我曾看到一个这样的协议:

    protocol URLStringConvertible {
        var urlString: String { get }
    }

    // 其中一段代码

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

这有什么用吗?为什么不移除协议然后直接传入 urlString 呢?这明显简单多了。

又或者只有一个方法的协议:

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

这个观点略有争议:为什么不移除协议,然后再其他地方传入一个函数呢?这明显简单多了!(除非你的协议仅支持类,并且你想得到一个弱引用)。

我可以继续举例论证,但是我希望这个观点已经很明确了,在面向协议编程中,我们通常会有更简单的选择。抽象一点,协议仅仅是实现多态的代码的一种方式,其他很多方式也都可以实现:继承、泛型、值、函数等等。值(比如用 String 替代URLStringConvertible)是最简单的方法;函数(比如用 adapt 替代 RequestAdapter)比值的方式复杂一点,但是仍然很简单;泛型(无约束)也比协议简单。但完整地说,协议通常比类的继承结构要简单。

给你一点启发性的建议,你应该仔细考虑你的协议是塑造数据模型还是行为模型。对数据来说,结构类型可能更简单一点。对复杂的行为来说(比如有多个方法的委托),协议通常会更简单。(标准库中的集合协议有点特殊:它们并不真的描述数据,而是在描述数据处理)

也就是说,尽管协议非常有用,但是不要为了面向协议而使用协议。首先检视你的问题,并且尽可能地尝试用最简单的方法解决。通过问题顺藤摸瓜找到解决办法,不要背道而驰。面向协议编程并没有好坏之分,跟其他的技术(函数编程、面向对象、依赖注入、类的继承)一样,它能解决一些问题,但我们不应盲目,要择其善者而从之。有时候使用协议,但通常还有更简单的方法。






原文发布时间为:2016年12月08日


本文来自合作伙伴掘金,了解相关信息可以关注掘金网站。

时间: 2017-10-18

面向协议编程,灵丹妙药或是饮鸩止渴?的相关文章

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

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

Swift 面向协议编程入门

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

面向协议编程并非银弹

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

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

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

面向服务编程

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

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

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

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

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

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

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

Spring Framework中的面向方面编程

编程 作为这个介绍Spring框架中的面向方面编程(Aspect-Oriented Programming,AOP)的系列的第一部分,本文介绍了使您可以使用Spring中的面向方面特性进行快速开发的基础知识.使用跟踪和记录方面(面向方面领域的HelloWorld)作为例子,本文展示了如何使用Spring框架所独有的特性来声明切入点和通知以便应用方面.本系列的第二部分将更深入地介绍如何运用Spring中的所有通知类型和切入点来实现更实用的方面和面向方面设计模式.对于AOP的更一般性的介绍,请查看O