1. 前言
1.1 传统面向对象编程(OOP)的弊端
说起面对协议编程,就不得不先说下使用继承的痛点,因为面向接口编程很大程度上解决的就是继承带来的痛苦。
可以看下casa的一篇博客:跳出面向对象思想(一) 继承
我们可以看到滥用继承的话,后面如果要抽离功能的话就牵一发而动全身,抽离一个功能涉及到继承链上多个类的功能,难道抽离一个业务要把整个继承链涉及的功能都抽出来?显示太麻烦了,这时候如果在一开始使用协议就可以很好解决,每个单独功能一个协议,复杂功能将协议组合起来即可,后续抽离时抽单独协议和实现类即可(swift中可以将实现写在扩展中,可能不涉及实现类),这样就简单很多了。
继承还有一个问题就是多继承链复用代码问题,比如有A、B两个继承链,B继承链中想使用A继承链中的某个共用方法,这时候怎么办呢?OC和swift和C++不同,都不支持多继承,所以这时候就不能复用了。但是如果将公共的这个方法定义为协议,是不是就解决了?定义为协议,就像积木一样,哪里需要哪里直接用就好了。
所以,本篇博客就简单介绍下面向协议编程。
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
typedef NS_ENUM(NSUInteger, EHIRequestType) {
Get = 1,
Post,
};
NS_ASSUME_NONNULL_BEGIN
@protocol EHIRequestParamProtocol
@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
#import "EHIRequestParamProtocol.h"
NS_ASSUME_NONNULL_BEGIN
/** 请求接口 */
@protocol EHIInterfaceRequestProtocol
- (void)requestData:(__kindof NSObject *)param complete:(void (^)(NSDictionary * response))complete failed:(void (^)(NSDictionary * error))failed;
@end
NS_ASSUME_NONNULL_END
2.4 请求参数实现类
这里我们可以模拟一个具体的请求参数,包括请求方式、url和入参。实现类遵守请求参数的协议,需要实现协议中要求实现的属性。
.h文件如下:
/** 接口底层实现类 */
#import
#import "EHIRequestParamProtocol.h"
NS_ASSUME_NONNULL_BEGIN
@interface EHIRequestParam : NSObject
/** 获取请求参数 */
+ (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
#import "EHIInterfaceRequestProtocol.h"
NS_ASSUME_NONNULL_BEGIN
/** 请求方法实现类 */
@interface EHIRequestManager : NSObject
+ (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 *)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(_ request: T, handler: @escaping(T.Response?) -> Void)
}
extension EHIRequestMethodProtocol {
// 这里不默认实现,因为默认实现就和使用的请求框架耦合,不便于替换请求框架,每个请求方法自己实现协议即可
}
因为请求参数的实现类霍结构体会有多种,所以这里定义为泛型,请求参数是遵守EHIRequestProtocol协议的任意类或结构体都可以。
这里定义了一个逃逸闭包,定义返回值,返回在请求参数中定义的关联类型,有的话就返回,没有的话返回nil。
3.8 请求方法协议实现
// 实现请求方法协议,这里使用UrlSession实现,别的方法实现再创建别的结构体实现协议,这样解耦
struct EHIUrlSessionRequestMethod: EHIRequestMethodProtocol{
func requestData(_ 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