iOS开发笔记iOS 开发每天分享优质文章iOS程序猿

iOS-面向协议编程(POP)

2020-06-30  本文已影响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

上一篇下一篇

猜你喜欢

热点阅读