面向協議程式設計

Kaitiren發表於2016-12-02

介面是一系列可呼叫方法的集合。何為介面程式設計?介面程式設計是指當寫一個函式或一個方法時,我們應該更加關注具體的介面,而不是實現類。具體理解可以參考這篇文章

在OC中,介面又可以理解為Protocol,面向介面程式設計又可以理解為面向Protocol程式設計,或者面向協議程式設計。在Swift中,蘋果大幅強化了 Protocol 在這門語言中的地位,整個 Swift 標準庫也是基於 Protocol 來設計的,有興趣的童鞋可以看看這篇文章。面向介面程式設計正逐步成為程式開發的主流思想

在實際開發中,大多數朋友都比較熟悉物件程式設計,比如,使用ASIHttpRequest執行網路請求

1
2
3
4
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDidFinishSelector:@selector(requestDone:)];
[request setDidFailSelector:@selector(requestWrong:)];
[request startAsynchronous];

request是請求物件,當發起請求時,呼叫者需要知道給物件賦哪些屬性或者呼叫物件哪些方法。然而,使用AFNetworking請求方式卻不盡相同

1
2
3
4
5
6
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:@"www.olinone.com" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
    NSLog(@"好網站,贊一個!");
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    //to do
}];

同是請求物件,使用AFNetworking發起請求時,呼叫者可以不需要關心它有哪些屬性,只有介面無法滿足需求時才需要了解相關屬性的定義。兩種設計思路完全不同,當然,此處並不是想表明孰優孰劣,只是想通過兩種截然不同的請求方式引出本文的思想——面向介面程式設計(或者面向協議程式設計)

介面比屬性直觀

在物件程式設計中,定義一個物件時,往往需要為其定義各種屬性。比如,ReactiveCocoa中RACSubscriber物件定義如下

1
2
3
4
5
6
7
@interface RACSubscriber ()
  
@property (nonatomic, copy) void (^next)(id value);
@property (nonatomic, copy) void (^error)(NSError *error);
@property (nonatomic, copy) void (^completed)(void);
  
@end

參考AFNetworking的思想,以介面的形式提供訪問入口

1
2
3
4
5
6
7
@interface RACSubscriber
  
+ (instancetype)subscriberWithNext:(void (^)(id x))next
                             error:(void (^)(NSError *error))error
                         completed:(void (^)(void))completed;
  
@end

通過介面的定義,呼叫者可以忽略物件的屬性,聚焦於其提供的介面和功能上。程式猿在首次接觸陌生的某個物件時,介面往往比屬性更加直觀明瞭,抽象介面往往比定義屬性更能描述想做的事情

介面依賴

設計一個APIService物件

1
2
3
4
5
6
7
8
@interface ApiService : NSObject
  
@property (nonatomic, strong) NSURL        *url;
@property (nonatomic, strong) NSDictionary *param;
  
- (void)execNetRequest;
  
@end

正常發起Service請求時,呼叫者需要直接依賴該物件,起不到解耦的目的。當業務變動需要重構該物件時,所有引用該物件的地方都需要改動。如何做到既能滿足業務又能相容變化?抽象介面也許是一個不錯的選擇,以介面依賴的方式取代物件依賴,改造程式碼如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@protocol ApiServiceProtocol  
- (void)requestNetWithUrl:(NSURL *)url Param:(NSDictionary *)param;
  
@end
  
@interface NSObject (ApiServiceProtocol)  
@end
  
@implementation NSObject (ApiServiceProtocol)
  
- (void)requestNetWithUrl:(NSURL *)url Param:(NSDictionary *)param {
    ApiService *apiSrevice = [ApiService new];
    apiSrevice.url = url;
    apiSrevice.param = param;
    [apiSrevice execNetRequest];
}
  
@end

通過介面的定義,呼叫者可以不再關心ApiService物件,也無需瞭解其有哪些屬性。即使需要重構替換新的物件,呼叫邏輯也不受任何影響。呼叫介面往往比訪問物件屬性更加穩定可靠

抽象物件

定義ApiServiceProtocol可以隱藏ApiService物件,但是受限於ApiService物件的存在,業務需求發生變化時,仍然需要修改ApiService邏輯程式碼。如何實現在不修改已有ApiService業務程式碼的條件下滿足新的業務需求?

參考Swift抽象協議的設計理念,可以使用Protocol抽象物件,畢竟呼叫者也不關心具體實現類。Protocol可以定義方法,可是屬性的問題怎麼解決?此時,裝飾器模式也許正好可以解決該問題,讓我們試著繼續改造ApiService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@protocol ApiService  
// private functions
  
@end
  
@interface ApiServicePassthrough : NSObject
  
@property (nonatomic, strong) NSURL        *url;
@property (nonatomic, strong) NSDictionary *param;
  
- (instancetype)initWithApiService:(id)apiService;
- (void)execNetRequest;
  
@end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface ApiServicePassthrough ()
  
@property (nonatomic, strong) id apiService;
  
@end
  
@implementation ApiServicePassthrough
  
- (instancetype)initWithApiService:(id)apiService {
    if (self = [super init]) {
        self.apiService = apiService;
    }
    return self;
}
  
- (void)execNetRequest {
    [self.apiService requestNetWithUrl:self.url Param:self.param];
}
  
@end

經過Protocol的改造,ApiService物件化身為ApiService介面,其不再依賴於任何物件,做到了真正的介面依賴取代物件依賴,具有更強的業務相容性

定義一個Get請求物件

1
2
3
4
5
6
7
8
9
10
@interface GetApiService : NSObject   
@end
  
@implementation GetApiService
  
- (void)requestNetWithUrl:(NSURL *)url Param:(NSDictionary *)param {
    // to do
}
  
@end

請求程式碼也隨之改變

1
2
3
4
5
6
7
8
9
10
11
@implementation NSObject (ApiServiceProtocol)
  
- (void)requestNetWithUrl:(NSURL *)url Param:(NSDictionary *)param {
    id apiSrevice = [GetApiService new];
    ApiServicePassthrough *apiServicePassthrough = [[ApiServicePassthrough alloc] initWithApiService:apiSrevice];
    apiServicePassthrough.url = url;
    apiServicePassthrough.param = param;
    [apiServicePassthrough execNetRequest];
}
  
@end

物件可以繼承物件,Protocol也可以繼承Protocol,並且可以繼承多個Protocol,Protocol具有更強的靈活性。某一天,業務需求變更需要用到新的Post請求時,可以不用修改 GetApiService一行程式碼,定義一個新的 PostApiService實現Post請求即可,避免了物件裡面出現過多的if-else程式碼,也保證了程式碼的整潔性

依賴注入

文章寫到這裡,細心的童鞋可能已經發現問題——GetApiService依然是以物件依賴的形式存在。如何解決這個問題?沒錯,那就是依賴注入!

依賴注入是什麼?借用部落格裡面的一句話,其最大的特點就是:幫助我們開發出鬆散耦合、可維護、可測試的程式碼和程式。這條原則的做法是大家熟知的面向介面,或者說是面向抽象程式設計。 objc上這篇文章介紹的不錯, 有興趣的童鞋也可以看看,在此就不再累述

基於依賴注入objection開源庫的基礎上繼續改造ApiService

1
2
3
4
5
6
7
8
9
10
11
@implementation NSObject (ApiServiceProtocol)
  
- (void)requestNetWithUrl:(NSURL *)url Param:(NSDictionary *)param {
    id apiSrevice = [[JSObjection createInjector] getObject:[GetApiService class]];
    ApiServicePassthrough *apiServicePassthrough = [[ApiServicePassthrough alloc] initWithApiService:apiSrevice];
    apiServicePassthrough.url = url;
    apiServicePassthrough.param = param;
    [apiServicePassthrough execNetRequest];
}
  
@end

呼叫者關心請求介面,實現者關心需要實現的介面,各司其職,互不干涉

介面和實現分離的設計適用於團隊協作開發,實現了系統的鬆散耦合,便於以後升級擴充套件。當然,介面程式設計對開發人員的要求也比較高,需要提前定義好介面,介面一變,全部亂套,這就是所謂的設計比實現難,但是設計介面的人工資都高啊!!!

相關文章