iOS-面向协议编程(POP)

作者: 直男程序员 | 来源:发表于2020-06-30 14:35 被阅读0次

    1. 前言

    1.1 传统面向对象编程(OOP)的弊端

    从我们刚开始学编程开始,面向对象就被津津乐道,所谓万物皆对象,我们从开始认识到这个东西的核心:继承,封装,多态,到用成万行甚至上十万行代码去理解它,最后终于开始理解它。

    但是随着时间的推移,我们慢慢的发现它的各种弊端,如依赖性,耦合性,可维护性等,特别是继承的层级多了以后,可读性大大降低,还存在一个问题就是面向对象多使用继承,复用代码多放在继承的基类中,但不同继承链直接如何复用代码是个很大问题。

    1.2 什么是面向协议编程(POP)?

    这个问题,我感觉比较明确的定义就是2015年Apple WWDC中说的一句话很直了的解释了:

    Don't start with a class.
    Start with a protocol.
    

    即在程序设计中,不要以一个类开始设计,应该从一个协议开始,应抛弃之前OOP的对象设计理念,设计协议,这样不同的继承链之间也可以使用同一个协议。可以将协议看做一个组件,哪里需要哪里继承协议即可,而且协议是可以多继承的,iOS中的类只能单继承,这也是面向协议相对面向对象的一大优势。

    1.3 Objective-C 和Swift的面向协议编程区别

    我理解的OC和swift面向协议编程一个最大区别是OC的 Protocol 没有默认的实现,需要依赖具体的实现类实现协议定义的方法,而Swift2.0开始提供了Protocol + Extension,协议可以再 Extension中提供默认的实现,这样上层调用可以直接调用协议的默认实现。

    严谨来说,OC不是一门面向协议编程的语言,因为 Protocol 只提供定义,而不提供实现,所以叫他 面向接口编程 更合适一些。

    2. 在Objective-C中实现面对协议编程

    2.1 简述

    下面以一个简单的例子来看下在OC中面向协议编程的使用。

    在这个例子中,我简单模拟了一个网络请求的封装,包括请求参数、url以及请求方法,因为只是简单的模拟,所以就只提供简单的参数,重点在看下面向协议编程的方式。

    面向协议编程重点在于协议的设计,就如移动端和后端的API接口一样,设计好以后就可以并行开发了,但是如果设计不当改起来就麻烦了,所以使用面向协议编程,首先思考好功能的协议如何设计。

    这次得DEMO我设计思路如下:
    [图片上传失败...(image-27c2fb-1593498910139)]

    2.2 请求参数协议

    /** 请求参数协议 */
    #import <Foundation/Foundation.h>
    
    typedef NS_ENUM(NSUInteger, EHIRequestType) {
        Get = 1,
        Post,
    };
    
    NS_ASSUME_NONNULL_BEGIN
    
    @protocol EHIRequestParamProtocol <NSObject>
    
    @required
    
    // 请求方式
    @property (nonatomic, assign) EHIRequestType requestType;
    
    @property (nonatomic, strong) NSString *url;
    
    @optional
    
    @property (nonatomic, strong) NSDictionary *param;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    在这里,我们可以定义请求的方式,以及请求的url和参数,因为避免请求的协议过于庞大,后期不好维护,所以协议按照功能分开创建,这里只涉及请求参数和方式。

    2.3 请求方法协议

    在请求的时候,请求方法需要依赖请求参数,所以在定义请求方法接口的时候,参数可以设置为遵循请求参数的协议,这样便于解耦,比如不同的模块域名这些可能是不同的,这样请求url这些肯定是不相同的,在请求方法中,只要遵循了请求协议即可传入,这样就不用管请求参数的底层实现了,达到了解耦的作用。

    /** 请求方法协议 */
    #import <Foundation/Foundation.h>
    #import "EHIRequestParamProtocol.h"
    
    NS_ASSUME_NONNULL_BEGIN
    
    /** 请求接口 */
    @protocol EHIInterfaceRequestProtocol <NSObject>
    
    - (void)requestData:(__kindof NSObject<EHIRequestParamProtocol> *)param complete:(void (^)(NSDictionary * response))complete failed:(void (^)(NSDictionary * error))failed;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    2.4 请求参数实现类

    这里我们可以模拟一个具体的请求参数,包括请求方式、url和入参。实现类遵守请求参数的协议,需要实现协议中要求实现的属性。

    .h文件如下:

    /** 接口底层实现类 */
    #import <Foundation/Foundation.h>
    #import "EHIRequestParamProtocol.h"
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface EHIRequestParam : NSObject<EHIRequestParamProtocol>
    
    /** 获取请求参数 */
    + (instancetype)getRequestParam;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    .m文件如下:

    #import "EHIRequestParam.h"
    
    @implementation EHIRequestParam
    
    + (instancetype)getRequestParam {
        EHIRequestParam * param = [[EHIRequestParam alloc]init];
        return param;
    }
    
    - (EHIRequestType)requestType {
        return Get;
    }
    
    - (NSDictionary *)param {
        return @{@"id":@"111"};
    }
    
    - (NSString *)url {
        return @"https://api.ehi.com";
    }
    
    @synthesize url;
    
    @synthesize param;
    
    @synthesize requestType;
    
    @end
    

    在.m文件中可以下设置具体的请求参数,这里我通过类方法获取所有的参数,考虑的是对外尽可能不暴露过多信息,做到高内聚,低耦合。

    2.5 请求方法实现类

    请求方法搞定后,就可以实现请求方法,实现类遵守协议,实现协议的方法。因为是请求方法,会在多个地方多次使用,所以这里我设计的是可以通过单例获取,然后进行请求,这样就很方便了。

    .h文件如下:

    #import <Foundation/Foundation.h>
    #import "EHIInterfaceRequestProtocol.h"
    
    NS_ASSUME_NONNULL_BEGIN
    
    /** 请求方法实现类 */
    @interface EHIRequestManager : NSObject<EHIInterfaceRequestProtocol>
    
    + (instancetype)shareManager;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    .m文件如下:

    #import "EHIRequestManager.h"
    
    static EHIRequestManager *_instance = nil;
    
    @implementation EHIRequestManager
    
    + (instancetype)allocWithZone:(struct _NSZone *)zone {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            if (_instance == nil) {
                _instance = [super allocWithZone:zone];
            }
        });
        return _instance;
    }
    
    + (instancetype)shareManager {
        return [[self alloc] init];
    }
    
    /** 抽象参数,可指向InterfaceProtocol的任意实现类 */
    - (void)requestData:(__kindof NSObject<EHIRequestParamProtocol> *)param complete:(void (^)(NSDictionary * _Nonnull))complete failed:(void (^)(NSDictionary * _Nonnull))failed {
        
        // 在这里可以进行AF等网络请求
        // 这里因为是简单demo,就模拟下请求数据并返回
        if ([param.url isEqual: @"https://api.ehi.com"]) {
            complete(@{@"statusCode": @"200",
                       @"statusMsg": @"请求成功",
            });
        } else {
            failed(@{@"statusCode": @"400",
                     @"errorMsg": @"请求超时"});
        }
    }
    
    @end
    

    看.m文件,可以看到在这里我模拟了一下请求,在这里其实可以进行各种第三方请求框架的使用,上层调用不依赖与三方,所以后续如果需要切换三方库在这里也可以很方便的切换。

    2.6 上层调用

    我模拟下的是在ViewController进行网络请求,可以看下请求代码:

    /** 上层调用 */
        [[EHIRequestManager shareManager] requestData:[EHIRequestParam getRequestParam] complete:^(NSDictionary * _Nonnull response) {
            NSLog(@"%@", response[@"statusMsg"]);
        } failed:^(NSDictionary * _Nonnull error) {
            NSLog(@"%@", error[@"errorMsg"]);
        }];
    

    可以发现,代码比较简洁,优雅的实现了网络请求,并实现了调用层和网络层的解耦。

    3. 在Swift中实现面对协议编程

    3.1 Swift和OC中协议对比

    Swift中 Protocol 相比OC,强大之处就在于协议可以提供默认实现,这对于一些协议中共用的方法,有了默认的实现是非常方便的,这样就不需要每个遵守协议的类或结构体都再实现一遍相同的功能,这样如果不需要自定义实现方法的话,就直接使用默认实现即可,这样相对于OC来说,就少了实现类这一层,整体层级更加清晰。

    3.2 Swift实现DEMO结构图

    下面和上面OC的例子一样,也以网络请求为例看下在Swift中面向协议是如何实现的,设计的结构图如下:

    [图片上传失败...(image-df80e4-1593498910139)]

    可以看到如果去掉数据解析部分,其实和OC的实现相差不大,每个协议我在实现中都还有有一个实现的结构体,这个其实是因为为了代码的通用性和扩展性,结构体实现的是一个模块特有的功能,这些如果写到协议的默认实现中的话,就和协议耦合起来了,不便于以后的扩展。当然,如果在使用Swift协议时,如果是通用的方法这些,在协议的 Extension 中实现是最好的。

    下面直接上代码:

    3.3 请求参数协议

    import Foundation
    
    enum EHIRequestType: String {
        case Get
        case Post
    }
    
    // 请求参数协议 (具体每个模块的具体参数自己实现)
    protocol EHIRequestProtocol {
        
        // 请求方式
        var requestType: EHIRequestType {get}
        // url
        var url: String {get}
        // 参数
        var param: [String: Any] {get}
        
        // 关联类型(可以对回调参数进行抽象)
        associatedtype Response: EHIRequestDecodableProtocol
    }
    

    这里只定义协议,具体的实现交给实现的类或结构体。

    这里需要注意一下里面有一个关联类型,关联类型的具体类型在实现的类或结构体自己指定,这里使用关联类型,方便扩展,后续的请求方法中使用的是这个关联类型。关联类型遵守的EHIRequestDecodableProtocol协议,这个下面展开介绍。

    3.4 数据解析协议

    // 解析数据协议
    protocol EHIRequestDecodableProtocol {
        
        static func parse(data: Data) -> Self?
    }
    

    这里协议功能是为了请求下来数据后,对数据进行解析,具体的实现有具体的数据来实现。

    3.5 数据类型

    在这里,我们自定义一个 EHIUser 的结构体,一个成员变量为name,提供一个构造器方法对Data数据进行解析。同时EHIUser作为具体类型,在这里遵守解析协议并实现协议方法是最好的,在这里实现方法,解析数据为自己。

    import Foundation
    
    struct EHIUser {
        
        let name: 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
            }
            
            self.name = name
        }
    }
    
    // 遵守解析数据协议,解析数据为user类型
    extension EHIUser: EHIRequestDecodableProtocol {
        
        static func parse(data: Data) -> EHIUser? {
            return EHIUser(data: data)
        }
    }
    

    3.6 请求参数实现结构体

    上面可以看到请求参数协议EHIRequestProtocol,这里来实现下协议。

    // 实现EHIRequest协议
    struct EHIUserRequest: EHIRequestProtocol {
        
        // 实现协议内容
        let requestType: EHIRequestType = .Get
        
        var url: String = "https://api.ehi.com"
        
        var param: [String : Any] = ["id" : "111"]
    
        typealias Response = EHIUser
    }
    

    在这里,关联类型指定为 EHIUser。

    3.7 请求方法协议

    和OC中实现一样,为了扩展性,所以请求参数和请求方法分离,请求参数搞定后,接下来看下请求方法的协议:

    import UIKit
    
    // 请求方法协议
    protocol EHIRequestMethodProtocol {
        
        func requestData<T: EHIRequestProtocol>(_ request: T, handler: @escaping(T.Response?) -> Void)
    }
    
    extension EHIRequestMethodProtocol {
        
        // 这里不默认实现,因为默认实现就和使用的请求框架耦合,不便于替换请求框架,每个请求方法自己实现协议即可
    }
    

    因为请求参数的实现类霍结构体会有多种,所以这里定义为泛型,请求参数是遵守EHIRequestProtocol协议的任意类或结构体都可以。

    这里定义了一个逃逸闭包,定义返回值,返回在请求参数中定义的关联类型,有的话就返回,没有的话返回nil。

    3.8 请求方法协议实现

    // 实现请求方法协议,这里使用UrlSession实现,别的方法实现再创建别的结构体实现协议,这样解耦
    struct EHIUrlSessionRequestMethod: EHIRequestMethodProtocol{
        
        func requestData<T: EHIRequestProtocol>(_ requesProtocol: T, handler: @escaping (T.Response?) -> Void) {
            //请求实现
            let urlRequest = URL(string: requesProtocol.url)!
            let request = URLRequest(url: urlRequest)
            
            let task = URLSession.shared.dataTask(with: request) {
                data, _, error in
                if let data = data, let response = T.Response.parse(data: data) {
                    DispatchQueue.main.async {
                        handler(response)
                    }
                } else {
                    DispatchQueue.main.async {
                        handler(nil)
                    }
                }
            }
            task.resume()
        }
    }
    

    定义了一个 EHIUrlSessionRequestMethod 的结构体来实现协议,使用UrlSession进行请求,请求下来的数据进行解析并回调。

    这里可以看到解析数据时候 使用的方法:

    T.Response.parse(data: data)
    

    这里就体现出来强大的扩展性,解析实现在具体的类型中,这样泛型T的关联类型Response自己解析数据即可,且在这里不产生耦合,实现了高内聚低耦合。

    3.9 应用层调用

            // 应用层调用
            EHIUrlSessionRequestMethod().requestData(EHIUserRequest()) { user in
                print(user?.name ?? "")
            }
    

    使用 EHIUrlSessionRequestMethod 这个结构体请求即可,请求参数这里传入的是自己实现的EHIUserRequest这个结构体,然后打印了下请求下来的数据。

    自己在实现的时候,可以根据请求的框架和请求的数据自己实现对应地请求参数、方法即可。

    4. 总结

    通过面向协议的编程,我们可以从传统的继承上解放出来,用一种更灵活的方式,搭积木一样对程序进行组装,特别是Swift有了协议扩展,我们可以减少类和继承带来的共享状态的风险以及继承链的冗长,让代码更加清晰。

    最好做到每个协议专注于自己的功能,这样才有更好的扩展性和解耦,高度的协议化有助于解耦以及扩展,而结合泛型来使用协议,更可以让我们免于动态调用和类型转换的苦恼,保证了代码的安全性。

    最后就是编程世界没有银弹,每一种代码理念都有其存在的价值,所以不能为了用POP而使用POP,大家要做代码的主人。

    Demo连接:
    OCDemo
    SwiftDemo

    相关文章

      网友评论

        本文标题:iOS-面向协议编程(POP)

        本文链接:https://www.haomeiwen.com/subject/koiofktx.html