Swift - 面向协议编程(POP)

一、OOP与POP

面向对象程序设计 (Object Oriented Programming) 其本质是以建立模型体现出来的抽象思维过程和面向对象的方法。模型是用来反映现实世界中事物特征的。任何一个模型都不可能反映客观事物的一切具体特征,只能对事物特征和变化规律的一种抽象,且在它所涉及的范围内更普遍、更集中、更深刻地描述客体的特征。通过建立模型而达到的抽象是人们对客体认识的深化。来自百度百科

正是因为化零为整的功效,方便储存数据然后传输! 面向对象的设计,在众多语言里面被采用!OC - Swift 主流方式也是 OOP. 这个相比大家已经非常熟悉了,这里也不再啰嗦。今天的主角是 POP (Protocol Oriented Programming) 一 面向协议编程

面向对象的困境

王巍面向协议编程 这一篇文章里面就有详细表达!

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

二、POP 面向协议编程

面向协议编程的思维,在 Swift 开发中非常非常重要!可以说如果你用好了 POP 那么你的项目绝对逼格不是一个 level 下面我们通过解决 OOP 问题的思路展开分析

POP 解决横切关注点

横切关注点 (Cross-Cutting Concerns) 那就是我们很难在不同继承关系的类里共用代码! 现在我们通过面向协议的方式,任何遵循协议的,都可以默认添加 name属性以及sayHello()方法!

protocol LGProtocl {
    /// 协议属性
    var name: String {get}
    /// 协议方法
    func sayHello()
}
  • 但是这里还有一个问题:缺乏实现!如果这样提供声明,我还需要在每一个类里面实现,有很多时候其实这些方法都是共有,不需要太多特定实现
  • 幸好在 WWDC 2015Swift 2 发布时,Apple 为协议引入了一个新特性一 协议扩展 它为 Swift 语言带来了一次革命性的变化。
struct LGTeacher: LGProtocl{
    var name: String
    func sayHello() {
        print("你好")
    }
}
  • 通过协议定义,提供实现的入口,遵循协议的类型需要对其进行实现
  • 协议扩展,为入口提供默认实现。根据入口提供额外实现

这样的操作有什么作用了? 一 万物皆 lg

// 声明协议
extension LGCompatible {
    /// Reactive extensions.
    public static var lg: LGRxSwift.Reactive<Self>.Type
    /// Reactive extensions.
    public var lg: LGRxSwift.Reactive<Self>
}
// NSObject 实现
extension NSObject : LGCompatible { }
  • 这样完美实现了万物皆lg的特性
  • 然后通过 LGCompatible 引申到 Reactive 响应式类或者结构体
  • 最后通过不断拓展 Reactive 的能力,就能完美切合

POP 解决动态派发安全性

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

// Compiler Error: 
// 'LGTeacher' does not conform to protocol 'LGProtocl'
// protocol requires function 'sayHello()'

POP 解决菱形缺陷

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

这里很遗憾POP在 解决菱形缺陷 这一点上也存在同样的BUG , 因为多个协议存在相同的协议属性、协议方法,遵循者也是无法确定的!⚠️ 我们在平时开发中一定要尽量规避同名协议遵循问题,我们在模块划分上面一定要做做到彻底,尽管 Swift 还不能很好地处理多个协议的冲突 但是我们可以在协议层功能抽层严格机智处理上浮与下沉功能。

举个例子:🌰

  • 我们刚刚是不是做了 LGCompatible, 它是我们功能的入口,万物皆lg,进来,那么这层协议只需要提供入口就完毕

  • 同时提供接口过度能力,把 LGReactiveCompatiblelg 过度到 Reactive

extension LGReactiveCompatible {
    /// Reactive extensions.
    public static var lg: Reactive<Self>.Type {
        get { return Reactive<Self>.self }
        set {// this enables using Reactive to "mutate" base type }
    }
    /// Reactive extensions.
    public var lg: Reactive<Self> {
        get { return Reactive(self) }
        set { // this enables using Reactive to "mutate" base object }
    }
}
  • Reactive层, 根据业务划分开来,达到逻辑代码下沉效果!
public struct Reactive<Base> {
    public let base: Base
    public init(_ base: Base) {
        self.base = base
    }
}
  • 根据 Reactive 里面关联的 Base 类型来确定不同的响应式功能
  • 比如:extension Reactive where Base: UISwitch 其中 UISwitch 可以换成 UITableViewUITextFieldUIView ... 不断业务下沉!

相信到这里你已经感受到了面向协议编程的方便之处,但是还有一个非常重要的特性没有展现出来就是 一 耦合度大大降低,代码分层,逻辑更清晰

三、POP 网络

我们在实际开发中,网络请求是一个非常重要的模块

Alamofire.request("http://127.0.0.1:5000/pythonJson/")
    .validate(statusCode: 200..<300)
    .validate(contentType: ["application/json"])
    .responseData { response in
        switch response.result {
        case .success:
            print(response)
            let _ = LGLoginClient.lgJson(data: response.data)
        case .failure(let error):
            print(error)
        }
}
  • 上面这段代码,没有错!但是如果你是一个资深iOS开发,肯定会发现问题!
  • 如果你直接在 ViewController (代表应用层) 直接这么网络请求,耦合度是非常大的(应用层与网络层耦合在一起)
  • 还有到处嵌套,可复用性特别低
  • 应用层其实根本不应该关心网络请求的method、接口、参数 说白了我也不想关心
  • 如果系统模块化处理,那我就非常 happy 😄😄😄

1️⃣:网络信息能力

enum LGHTTPMethod: String {
    case GET
    case POST
}

protocol LGRequest {
    var host: String { get }
    var path: String { get }
    var method: LGHTTPMethod { get }
    var parameter: [String: Any] { get }
    
    associatedtype Response
    func parse(data: Data) -> Response?
}
  • LGHTTPMethod 提供本模块 LGRequest 需要的请求方法枚举
  • LGRequest 是登录注册模块的请求能力赋予者,通过面向协议的方式给我们的模块提供能力
  • Response 这个关联类型,方便后面 json 转模型,设置这个泛型类型是能够通用化

2️⃣:模块信息层

struct LGLoginRequest: LGRequest {
    typealias Response = LGPerson
    let name: String
    
    let host = "http://127.0.0.1:5000"
    var path: String {
        return "/pythonJson/getTeacherInfo/?username=\(name)"
    }
    let method: LGHTTPMethod = .GET
    let parameter: [String: Any] = [:]
    
    func parse(data: Data) -> LGPerson? {
        return LGPerson(data: data)
    }
}
  • LGLoginRequest 遵循 LGRequest 获得host、path、method、parameter、parse 处理能力,在这里可以直接处理,就不需要到应用层再去传值!

3️⃣:网络请求能力

extension LGRequest {
    func send(handler: @escaping (Response?) -> Void) {
        let url = URL(string: host.appending(path))!
        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue
        let task = URLSession.shared.dataTask(with: request) {
            data, response, error in
            if let data = data, let res = self.parse(data: data) {
                DispatchQueue.main.async { handler(res) }
            } else {
                DispatchQueue.main.async { handler(nil) }
            }
        }
        task.resume()
    }
}
  • 因为面向协议,我们利用协议提供公用网络请求能力
  • 其中 Response 就是相应模型的具体类型

4️⃣:应用层调用

override func viewDidLoad() {
    super.viewDidLoad()
    let request = LGPersonRequest(name: "Cooci")
    request.send { (person) in
        self.updataUI(person: person!)
    }
}
  • 应用层与网络层完全分隔开来
  • 应用层只提供必要的参数信息,具体调用哪个接口,怎么处理交给下层
  • LGPersonRequest 模块信息处理提供层
  • LGPersonRequest 同时还具备调用网络能力

5️⃣:POP网络优化重构

很显然我们的 LGRequest 这个家伙的能力太强了!能提供信息,还能发起请求,连序列化的处理能力也是由LGRequest提供!优化重构迫在眉睫。。。。

1:信息提供能力者

protocol LGRequest {
    var path: String { get }
    var method: LGHTTPMethod { get }
    var parameter: [String: Any] { get }
    
    associatedtype Response: LGDecodable
}

struct LGPersonRequest: LGRequest {
    typealias Response = LGPerson
    
    let name: String
    var path: String {
        return "/pythonJson/getTeacherInfo/?username=\(name)"
    }
    let method: LGHTTPMethod = .GET
    let parameter: [String: Any] = [:]
}
  • 把公共提供的 host 提取出去
  • 不再提供 LGRequest 网络请求能力
  • 序列化交付给具体的模型,提供一个序列化能力 LGDecodable
protocol ClientProtocol {
    var host: String { get }
    func send<T: LGRequest>(_ r: T, handler: @escaping (T.Response?) -> Void)
}


class LGClient: ClientProtocol{
    static let manager = LGClient()
    let host: String = "http://127.0.0.1:5000"
    
    func send<T>(_ r: T, handler: @escaping (T.Response?) -> Void) where T : LGRequest {
        
        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, response, 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()
    }
}
  • 提供一个网络管理类 LGClient
  • 管理网络请求能力和公共提供参数

2:网络能力提供者

class LGLoginClient: LGClient {
    
    override func send<T>(_ r: T, handler: @escaping (T.Response?) -> Void) where T : LGRequest {
        
        switch r.path {
        case let string where string.contains("/pythonJson/getTeacherInfo"):
            print("123456")
            handler(nil)
        default:
            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, response, 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()
        }

    }
}
  • 模块网路能力层 LGLoginClient 重写,根据不同的接口分块处理
  • 其中序列化层交给泛型模型 (T.Response) 处理
  • 当然大家这里还可以将网络能力继续下沉,就给具体网络综合请求者去处理

3:序列化能力提供层

extension LGPerson: LGDecodable {
    static func parse(data: Data) -> LGPerson? {
        return LGPerson(data: data)
    }
}
  • 这里序列化就简单表达了,大家可以调用一些优化的三方框架

6️⃣:小结

  • POP网络,让应用层与网络层完全脱离!
  • 面向协议的编程方式来提供能力,大大拓展了复用性,同时耦合度也得以处理!
  • 同时这个模型(应用层 -> 信息提供层 -> 网络层) 编程思维也是比较容易理解,操作更容易上手!
  • 还有这样的编程习惯也符合开发流程(一般都是由后台开发人员做出相应接口,我们才会去做网络调试)
  • 后期维护简单,后期更改,我们只需要对信息提供层处理响应配置,根本不需要去动应用层或者网络层
  • 当然这也是主流开发思维,作为一名中高级iOS开发人员不动 POP网络编程 那么我估计你需要好好学习咯!💪💪💪

由于篇幅问题,这一篇面向协议编程,我们先介绍到这里!下一篇介绍 当Moya遇上RxSwift,当北京遇上西雅图,当 Moya 遇上 RxSwift,简直不要太爽!

就问此时此刻还有谁?45度仰望天空,该死!我这无处安放的魅力!

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

推荐阅读更多精彩内容