Swift面向协议编程(附代码)

来源:互联网 发布:互联网 学什么知乎 编辑:程序博客网 时间:2024/06/04 18:50

什么是swift协议?

Protocol
Swift标准库中有50多个复杂不一的协议,几乎所有的实际类型都是妈祖若干协议的。protocol是Swift语言的底座,语言的其他部分正是在这个底座上组织和建立起来的。这和我们熟知的面向对象的构建方式很不一样。

一个最简单但是有实际用处的Swift协议定义如下:

protocol Greetable {    var name: String{get}    func greet()}

这几行代码定义了一个名为Greetable的协议,其中有一个name属性的定义,以及一个greet方法的定义。

所谓协议,就是一组属性和/或方法的定义,而如果某个具体类型想要遵守一个协议,那它需要实现这个协议所定义的所有这些内容。协议实际上做的事情不过是“关于实现的约定”。

面向对象

在深入Swift协议的概念之前,我想先重新让大家回国一下面向对象。相信我们不论在教科书或者是博客等各种地方对这个名词都十分熟悉了。那么又一个很有意思,但是其实并不是每个程序员都想过的问题,面向对象的核心思想究竟是什么?

class Animal {    var leg:Int {return 2}    func eat() {        print("eat food")    }    func run() {    print("run with \(leg)legs")        }}class Tiger:Animal {    override var leg: Int {return 4}    override func eat() {    print("eat meat")    }}let tiger = Tiger()tiger.eat()//eat meattiger.run()//run with 4 legs

父类Animal 定义了动物的leg,以及动物的eat和run方法,并未她们提供了实现。子类的Tiger根据自身情况充血了leg和eat,而对于run,父类的实现已经满足要求,因此不必重写。

我们看到Tiger和Animal共享了一部分代码,这部分代码还被封装到了父类中,而除了Tiger的其他的字类也能够使用Animal的这些代码。这其实就是OOP的核心思想-使用封装和集成,将一些列相关的内容放到一起。我们的前辈们为了能够对真实的世界的对象进行建模,发展出了面向对象编程的概念,但是这套理念有一些缺陷。虽然我们努力用这套抽象和集成的方法进行建模,但是实际的食物往往是一系列特质的组合,而不单单是以一脉相承并逐渐扩展的方式构建的。所以最近大家越来越发现面向对象很多时候其实不能很好的对食物进行抽象,我们可能需要寻找另一个更好的方式。

面向对象编程的困境

横切关注点

我们再来看一个例子。这次让我们远离动物界,回到Cocoa,假设我们又一个VIewController,它集成自UIViewController,我们向其中添加一个myMethod:

class ViewControllerUIViewControlller{    func myMethod() {    }}

如果这个时候我们又有一个继承自UITableviewController的AntherViewController,我们也想向其中添加同样的myMethod:

class AnotherViewControllerUITableViewController{    func myMethod(){    }}

这是我们迎来了OOP的第一大困境,那就是我们很难再不同集成关系的类里共用代码。这里的问题用“行话”来说叫做“横切关注点(cross-cutting concerns)”。我们的关注点myMethod位于两条继承链的横切面上。面向对象是一种不错的抽象方式,但是肯定不是最好的方式。它无法描述两个不同食物具有某个相同特性这一点。在这里,特性的组合要比集成更贴切事物的本质。

想要解决这个问题,我们有几个方案:

  • Copy&Paste
    这是一个比较糟糕的解决方案,但是演现场还是有不少朋友选择了这个方案,特别是在工期很紧,无暇优化的情况下。这诚然可以理解,但是这也是坏代码的开头。我们尽量避免这种做法。
  • 引入BaseViewControlller
    在一个集成自UIViewController的BaseViewControlller上添加需要共享的代码,或者干脆在UIViewController上添加extension。看起来这是一个稍微靠谱的做法,但是如果不断这么做,会让所谓的Base很快变成垃圾堆。指责不明确,任何东西都扔进Base,你完全不知道那些类走了Base,而这个超级类对代码的影响也会不可估量。
  • 依赖注入
    通过外界传入一个带有myMethod的对象,用新的类型来提供这个功能。这是一个稍好的方式,但是引入额外的依赖关系,坑能也是我门不太愿意看到的。
  • 多继承
    当然,Swift是不支持多继承的,不过如果有多集成的话,我们确实可以从多个父类进行集成,并将myMethod添加到合适的地方。有一些语言选择了支持多集成,但是它会带来OOP中另一个著名的问题:菱形缺陷

菱形缺陷

上面的例子中,如果我们有多继承,纳闷ViewController和AnotherViewController的关系可能会是这样的:
这里写图片描述
在上面这种拓扑结构中,我们只需要在ViewContorller中实现myMethod,在AnotherVIewController中也就可以继承并使用它了。看起来很完美,我们避免了重复。但是多集成有一个无法回避的问题,就是两个父类都实现了同样的方法时,自乐该怎么办,我们很难确定因该继承哪一个父类的方法。因为多集成的拓扑结构是一个菱形,所以这个问题又被叫做菱形缺陷。像是C++这样的语言选择粗暴的将菱形缺陷的问题交给程序员处理,这无疑非常复杂,并且增加了人为错误的可能性。二绝大多数现代语言对多集成这个特性选择避而远之。

动态派发安全性

OC恰如其名,是一门典型的OOP语言,同时它继承了SmallTalk的消息发送机制。这套机制十分灵活,是OC的基础思想,但是有时候相当危险。

ViewController *v1 = ...[v1 myMethod];AnotherViewController *v2 = ...[v2 myMethod]; NSArray *array = @[v1,v2]; for (id obj in array){ [obj myMethod];}

我们如果在ViewController和AnotherViewController中都实现了myMethod的话,这段代码是没有问题的。myMethod将会呗动态发送个array中的v1和v2。但是,要是我们有一个没实现myMethod的类型,会如何?

NSObject *v3 = [NSObject new];NSArray *array = @[v1,v2,v3];for (id obj in array){    [obj myMethod];};

编译依然可以通过,但是显然,程序将在运行时崩溃。OC是不安全的,编译器默认你知道某个方法确实有实现,这是消息发送的灵活性所必需付出的代价。而在app开发看来,用可能的崩溃来换取灵活性,显然这个代价太大了。肃然这不是OOP范式的问题,但是它确实在OC时代给我们带来了切肤之痛。

三大困境

我们可以总结一下OOP面临的这几个问题。

  • 动态派发安全性
  • 横切关注点
  • 菱形缺陷

    首先,在OC中动态派发让我们承担了在运行时才发现错误的风险,这很很有可能是发生在上线产品中的错误。其次,横切关注点让我们难以对对象进行完美的缄默,代码的重用也会更加糟糕

协议扩展和面向协议编程

使用协议解决OOP困境

协议并不是什么新东西,也不是Swift的发明。在Java和C#里,它叫做Interface。二Swift中的Protocol将这个概念继承了下来,并发扬光大。让我们回到一开始定义的那个简单协议,并尝试实现这个协议:

protocol    Greetable{     var name : String{get}     func greet()}
struct Person:Greetable{    let name :String    func greet(){    print("你好\(name)")    }}Person(name:"zq").greet()

实现很简单,Person结构体通过实现name和greet来满足Greetable。在调用时,我们就可以使用Greetable中定义的方法了。

动态派发安全性

除了Person,其他类型也可以实现Greetable,比如Cat:

struct Cat: Greetable {    let name: String    func greet() {    pring("meow~\(name)")    }}

现在,我们就可以将协议作为标准类型,来对方法调用进行动态派发了:

let array: [Greetable] = [    Person(name:"zq");    Cat(name:"zz")]for obj in array{    obj.greet()}

对于没有实现Greetbale的类型,编译器将返回错误,因此不存在消息错误发送的情况

横切关注点

使用协议和协议扩展,我们可以很好的共享代码。回到上一节的myMethod方法,我们来看卡如何使用协议来搞定它。首先,我们可以定义一个含有myMethod的协议:

protocol P {    func myMethod()}   

注意这个协议没有提供任何实现。我们依然需要在实际类型遵守这个协议的时候为它提供具体的实现:

extension ViewController: P {    func myMethod() {    doWork()    }}extension AnotherViewController: P {    func myMethod() {    doWork()    }}

这和Copy&Paste也没什么不同啊?没有!听说过协议扩展吗 继续看

extension P {    func myMethod() {        doWork()    }}

给协议做扩展,有了这个扩展后我们只需要简单的声明ViewController和AnotherViewController遵守P,就可以直接使用myMehtod的实现了:

extension ViewController: P {}extension AnotherViewController: P {}

不仅如此,除了已经定义过的方法,我们甚至可以在扩展中添加协议里没有定义过的方法。在这些额外的方法中,我门可以依赖协议定义过的方法进行操作。

  • 协议定义
    ·提供实现的入口
    ·遵循协议的类型需要对其进行实现
  • 协议扩展
    ·为入口提供默认实现
    ·根据入口提供额外实现
    这样一来,横切关注的问题也简单安全的得到了解决。

菱形缺陷

最后我们看看多继承。多集成中存在的一个重要问题是菱形缺陷,也就是字类无法确定使用哪个负累的方法。在协议的对应方面,这个问题虽然依然存在,但却是可以为宜安全的确定的。

protocol Nameable {    var name: String{get}}protocol Identifiable {    var name: String{get}     var id: Int {get}}如果有一个类型,需要同时实现两个协议的话,他必须提供一个name属性,来同时满足两个协议的要求

struct Person: Nameable, Identifiable {
let name :String
let id: Int
}

这样以来菱形缺陷的问题就基本得以解决#在日常开发中使用协议##基于Protocol的网络请求网络请求层是实践POP的一个理想场所。我们在接下的例子中将从零开始,用最简单的面向协议的方式向偶见一个不那么完美的网络请求和模型层,他肯呢个包含一些不合理的设计和耦合,但是却是起步最容易得到的结果。然后我们逐步捋顺各部分的所属,并用分离职责的方式来进行重构。最后我们会为这个网络请求层进行测试。通过这个例子,我希望能够设计出包括类型安全,解耦合,易于测试和良好的扩展性等诸多优秀特性在内的POP代码。 1. 初步实现首先我们想要做的事情是从一个API请求一个JSON,然后将它转换成为Swift中可用的实例。作为例子的API非常简单,你可以直接访问https://api.onevcat.com/users/onevcat 来查看返回

{“name”:”onevcat”,”message”:”Welcome to MDCC 16!”}

我们新建一个项目并使用struct建立一个模型,为什么要使用struct呢?

// User.swift
import Foundation

struct User {
let name: String
let message: String

init?(data: Data) {    guard let obj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {        return nil    }    guard let name = obj?["name"] as? String else {        return nil    }    guard let message = obj?["message"] as? String else {        return nil    }    self.name = name    self.message = message}

}

User.init(data:)将输入的data数据解析为JSON对象然后取出name和message,并构建代表API返回的User实例,非常简单。现在让我们来看看有趣的部分,也就是如何使用POP的方式从URL请求数据,并声称对应的User。首先,我们可以创建一个protocol来代表请求。对于一个请求,我们需要知道它的请求路径,HTTP方法,所需要的参数等信息。一开始这个协议可能是这样的:

enum HTTPMethod: String {
case GET
case POST
}

protocol Request {
var host: String { get }
var path: String { get }

var method: HTTPMethod { get }var parameter: [String: Any] { get }

}

现在新建一个UserRequest来实现Request协议:

struct UserRequest: Request {
let name: String

let host = "https://api.onevcat.com"var path: String {    return "/users/\(name)"}let method: HTTPMethod = .GETlet parameter: [String: Any] = [:]

}

UserRequest中有一个未定义初始值的name属性,其他的属性都是为了满足协议所定义的。因为请求的参数用户名name会通过URL进行传递,所以parameter是一个空字典(因为要用get请求)。有了协议定义和一个满足定义的具体请求,现在我们需要发送请求。为了任意请求都可以通过同样的方法发送,我们将发送方法定义在Request协议扩展上:

extension Request {
func send(handler: @escaping (User?) -> Void) {
// … send 的实现
}
}

在send(handler:)的参数重,我们定义了可逃逸的(User?)-> Void,在请求完成后,我们调用这个handler方法来通知调用者是否完成,如果一切正常,则将一个User实例返回,否则传回nil。我们想要这个send方法对于搜游的Request都通用所以显然回调的参数不能是User。通过在Request协议中添加一个管理类型,我们可以将回调参数进行抽象。在Request最后添加:

protocl Request {
···
associatedtype Response
}

然后在UserRequest中,我们也相应的添加类型定义,以满足协议:

struct UserRequest: Request {

typealias Response = User
}

现在,我们来重新实现send方法,我们可以用Response代替具体的User,让send一般化。我们这里使用URLSession来发送请求

extension Request {
func send(handler: @escaping (Response?) -> Void) {
let url = URL(string: host.appending(path))!
var request = URLRequest(url: url)
request.httpMethod = method.rawValue

    // 在示例中我们不需要 `httpBody`,实践中可能需要将 parameter 转为 data    // request.httpBody = ...    let task = URLSession.shared.dataTask(with: request) {        data, res, error in        // 处理结果        print(data)    }    task.resume()}

}

通过拼接host和path,可以得到API的entry point。根据这个URL创建请求,进行配置,生成data task并将请求发送。剩下的工作就是将会掉中的data转换成为合适的对象类型。并调用handler同志外部调用者。对于User我们知道可以使用User.init(data:),但是对于一般的Response,我们还不知道如何将数据专为模型。我们可以在Request里定义一个pase(data:)方法,来要求满足该协议的具体类型提供合适的实现。这样一来,提供转换方法的任务就被下放到了UserRequest

protocol Request {

associatedtype Response
func parse(data: Data) -> Response?
}

struct UserRequest: Request {

typealias Response = User
func parse(data: Data) -> User? {
return User(data: data)
}
}

有了data转换为Response的方法后,我们就可以对请求的结果进行处理了:

extension Request {
func send(handler: @escaping (Response?) -> Void) {
let url = URL(string: host.appending(path))!
var request = URLRequest(url: url)
request.httpMethod = method.rawValue

    // 在示例中我们不需要 `httpBody`,实践中可能需要将 parameter 转为 data    // request.httpBody = ...    let task = URLSession.shared.dataTask(with: request) {        data, _, error in        if let data = data, let res = parse(data: data) {            DispatchQueue.main.async { handler(res) }        } else {            DispatchQueue.main.async { handler(nil) }        }    }    task.resume()}

}

现在我们做一下请求

let request = UserRequest(name: “onevcat”)
request.send { user in
if let user = user {
print(“(user.message) from (user.name)”)
}
}

2.重构关注点分离虽然能够实现需求,但是上面的实现可以说非常糟糕。让我们看看现在Request的定义和扩展

protocol Request {
var host: String { get }
var path: String { get }

var method: HTTPMethod { get }var parameter: [String: Any] { get }associatedtype Responsefunc parse(data: Data) -> Response?

}

extension Request {
func send(handler: @escaping (Response?) -> Void) {

}
}

这里最大的问题在于,Request管理了太多的东西。一个Request应该做的事情应该仅仅是定义请求入口和期望的相应类型,而现在Request补光定义了host的值,还对如何解析数据了如指掌。最后send方法被绑死在了URLSession的实现上,而且是作为Request的一部分村子。这是很不合理的,因为这意味着我们无法在不更改请求的情况下更新发送请求的方式,它们被耦合在了一起,这样的结构让测试变得异常艰难,我们可能需要通过stub和mock的方式对请求拦截,然后返回构造的数据,这会用到NSURLProtocol的内容,或者是引入一些第三方的测试框架大大的增加了项目的复杂程度。在OC这一时期这可能是一个可选项,但是在Swift新时代,我们有好的多的方法来处理这件事情。首先我们将send(handler:)从Request中分离出来。我们需要一个单独的类型负责发送请求。这里机遇POP的开发方式,我们定义一个可以发送请求的协议:

protocol Client {
func send(_ r: Request, handler: @escaping (Request.Response?) -> Void)
//编译错误原因是,Request是含有“关联类型”的协议,所以它并不能作为独立的类型来使用,我们只能够将它作为类型约束,来限制输入参数request。
//关联类型(associatedtype):我不知道具体类型是什么,一些服从我的类,结构体,枚举会帮我实现这个细节
}

正确的方式

protocol Client {
func send(_ r: T, handler: @escaping (T.Response?) -> Void)
var host: String { get }
}

除了使用<T:Request>这个范型方式意外,我们还将host从Request移动到了Client里,这是更适合他的地方。现在,我们可以把含有send的Request协议扩展删除,重新创建一个类型满足Client了。和之前一样,他将使用URLSession来发送请求:

struct URLSessionClient: Client {
let host = “https://api.onevcat.com”

func send<T: Request>(_ r: T, handler: @escaping (T.Response?) -> Void) {    let url = URL(string: host.appending(r.path))!    var request = URLRequest(url: url)    request.httpMethod = r.method.rawValue    let task = URLSession.shared.dataTask(with: request) {        data, _, error in        if let data = data, let res = r.parse(data: data) {            DispatchQueue.main.async { handler(res) }        } else {            DispatchQueue.main.async { handler(nil) }        }    }    task.resume()}

}

现在发送请求部分和请求本身分离开了,而且我们使用协议的方式定义了Client。除了URLSessionClient意外,我们还可以使用任意的类型来满足这个协议,并发送请求。这样网络层的具体实现和请求本身就不再想管了,我们之后在测试的时候会进一步看到这么做所带来的好处。现在还有一个问题,Request的pase方法。请求不应该知道如何解析得到的数据,这项工作应该叫个Response来做。而现在我们没有对Response进行任何限定。接下来我们将新增一个协议,满足这个协议的类型将知道如何将一个data转换为实际的类型:

protocol Decodable {
static func parse(data: Data) -> Self?
}

Decodable定义了一个静态的parse方法,现在我们需要在RequestResponse关联类型中为它加上这个限制,这样我们可以保证所有的Response都可以对数据进行解析,远了Request中的parse生命也就可以解除了。最终的Request协议

protocol Request {
var path: String { get }
var method: HTTPMethod { get }
var parameter: [String: Any] { get }

// associatedtype Response// func parse(data: Data) -> Response?associatedtype Response: Decodable

}

修改User满足Decodable,并且修改上面URLSessionClient的解析部分的代码,让它使用Response中的parse 方法

extension User: Decodable {
static func parse(data: Data) -> User? {
return User(data: data)
}
}

struct URLSessionClient: Client {
func send(_ r: T, handler: @escaping (T.Response?) -> Void) {

// if let data = data, let res = parse(data: data) {
if let data = data, let res = T.Response.parse(data: data) {

}
}
}

最后,将client中不再需要的host和parse等清理一下,一个类型安全,解耦合的面向协议的网络层就呈现在我们眼前了。想要调用UserRequest时,我们可以这样写

URLSessionClient().send(UserRequest(name: “onevcat”)) { user in
if let user = user {
print(“(user.message) from (user.name)”)
}
}

可以为URLSessionClient添加一个单例,或者为请求添加Promise的调用方式。在POP的组织下,这些改动都很自然,也不会牵扯到请求的其他部分。你可以用和UserRequest类型相似的方式,为网络层添加其他的API请求,只需要定义请求所必要的内容,而不用担心会触及网络方面的具体实现。3.网络层测试将Client生命为协议给我们带来了额外的好处,那就是我们不在局限于使用某种特定的技术(比如这里的URLSession)来实现网络请求。利用POP,你只是定义了一个发送请求的协议,你可以很容易的使用像是ADNetworking或者Alamofire这样的成熟的第三方框架来构建具体的数据并处理请求的低层实现。我们甚至可以提供一组“虚假”对请求的响应,用来进行测试。这和传统的stub&mock的方式在概念上时接近的,但是实现起来要简单得多,也明确的多。这个就是加载一个本地文件。4.可扩展性因为高度耦合,这种基于POP的实现为代码的扩展提供了相对宽松的可能性。我们刚才已经说过,你不必自行去实现一个完整的Client,而可以依赖于现有的网络请求框架,实现请求发送的方法即可。也就是说你也可以很容易的将某个正在使用的请求方式替换为另外的方式,而不会影响请求的定义和使用。类似的以使用任意的第三方JSON解析库,来帮助我们迅速构建模型类型,这仅仅只需要实现一个将Data转换为对应模型类型的方法即可。#使用协议帮助改善代码通过面向协议的编程,我们可以从传统的集成商解放出来,用一种更灵活的方式,搭积木一样对程序进行组装。每个协议专注于自己的功能,特别得益于协议扩展,我们可以减少类和集成带来的共享状态的风险,让代码更加清晰。高度的协议话有助于解耦合,测试以及扩展,而结合范型来使用协议,更可以让我们免于动态调用和类型转换的苦恼,保证了代码的安全性。这段代码用的playground

import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
protocol Client {
func send(_ r:T,handler:@escaping(T.Response?)-> Void)

}
protocol Decodable {
static func parse (data:Data) -> Self?
}
struct User:Decodable {
let name:String
let message:String
static func parse(data: Data) -> User? {
return User.init(data:data)
}
init?(data:Data) {
guard let obj = try? JSONSerialization.jsonObject(with: data, options:[])as?[String:Any] else{
return nil
}
guard let name = obj?[“name”] as? String else{
return nil
}
guard let message = obj?[“message”] as? String else{
return nil
}
self.name = name
self.message = message
}

}

enum HTTPMethod:String{
case GET
case Post
}

protocol Request{
var host:String {get}
var path:String {get}
var method:HTTPMethod{get}
var parameter:[String:Any]{get}
associatedtype Response:Decodable

}
struct URLSessionClient: Client {
static func shareClient() ->URLSessionClient{

return self.init()}func send<T: Request>(_ r: T, handler: @escaping (T.Response?) -> Void) {    let url = URL(string: r.host.appending(r.path))!    var request = URLRequest(url: url)    request.httpMethod = r.method.rawValue    let task = URLSession.shared.dataTask(with: request) {        data, _, error in        if let data = data, let res = T.Response.parse(data: data) {            DispatchQueue.main.async { handler(res) }        } else {            DispatchQueue.main.async { handler(nil) }        }    }    task.resume()}

}
struct UserRequest:Request {

let name: Stringlet host = "https://api.onevcat.com"var path: String {    return "/users/\(name)"}let method: HTTPMethod = .GETlet parameter: [String : Any] = [:]typealias Response = Userfunc phase(data: Data) -> User? {    return User.init(data: data)}

}
URLSessionClient.shareClient().send(UserRequest.init(name: “onevcat”)) { (user) in
if let user = user {
print(“(user.name)from(user.message)”)
}
}

“`

0 0
原创粉丝点击