面向协议编程并非银弹

前言

本文翻译自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
    }
}

上述代码简单并工作地很好。它没有问题,直到我们希望测试loadUserloadEpisode的时候。现在我们要不存根加载,或者使用依赖注入的方式传一个模拟的请求进去。我们可以定义一个URLSession遵循的请求并在一个测试实例中传递进去。但是,在这个例子中,解决办法可以更简单:我们可以将需要改变的部分从Webservice抽离到一个结构体中(在Swift Talk Episode 1Advanced 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)
    }
}

现在,我们可以不通过模拟任何东西进行userepisode的测试:他们是简单的结构体类型。我们仍然不得不测试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作为协议并创建遵循该协议的UserResourceEpisodeResource结构体,代替将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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容