文章地址html
網絡層做爲App
架構中相當重要的中間件之一,承擔着業務封裝和核心層網絡請求交互的職責。討論請求中間件實現方案的意義在於中間件要如何設計以便減小對業務對接的影響;明晰請求流程中的職責以便寫出更合理的代碼等。所以在講如何去設計請求中間件時,主要考慮三個問題:面試
根據暴露給業務層請求API
的不一樣,能夠分爲集約式請求
和離散型請求
兩類。集約式請求
對外只提供一個類用於接收包括請求地址、請求參數在內的數據信息,以及回調處理(一般使用block
)。而離散型請求
對外提供通用的擴展接口完成請求json
考慮到AFNetworking
基本成爲了iOS
的請求標準,以傳統的集約式請求代碼爲例:數組
/// 請求地址和參數組裝
NSString *domain = [SLNetworkEnvironment currentDomain];
NSString *url = [domain stringByAppendingPathComponent: @"getInterviewers"];
NSDictionary *params = @{
@"page": @1,
@"pageCount": @20,
@"filterRule": @"work-years >= 3"
};
/// 構建新的請求對象發起請求
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager POST: url parameters: params success: ^(NSURLSessionDataTask *task, id responseObject) {
/// 請求成功處理
if ([responseObject isKindOfClass: [NSArray class]]) {
NSArray *result = [responseObject bk_map: ^id(id obj) {
return [[SLResponse alloc] initWithJSON: obj];
}];
[self reloadDataWithResponses: result];
} else {
SLLog(@"Invalid response object: %@", responseObject);
}
} failure: ^(NSURLSessionDataTask *task, NSError *error) {
/// 請求失敗處理
SLLog(@"Error: %@ in requesting %@", error, task.currentRequest.URL);
}];
/// 取消存在的請求
[self.currentRequestManager invalidateSessionCancelingTasks: YES];
self.currentRequestManager = manager;
複製代碼
這樣的請求代碼存在這些問題:markdown
block
回調潛在的引用問題在業務封裝的層面上,應該只關心什麼時候發起請求
和展現請求結果
。設計上,請求中間件應當只暴露必要的參數property
,隱藏請求過程和返回數據的處理網絡
和集約式請求不一樣,對於每個請求API
都會有一個manager
來管理。在使用manager
的時候只須要建立實例,執行一個相似load
的方法,manager
會自動控制請求的發起和處理:架構
- (void)viewDidLoad {
[super viewDidLoad];
self.getInterviewerApiManager = [SLGetInterviewerApiManager new];
[self.getInterviewerApiManager addDelegate: self];
[self.getInterviewerApiManager refreshData];
}
複製代碼
集約式請求和離散型請求最終的實現方案並非互斥的,從底層請求的具體行爲來看,最終都有統一執行的步驟:域名拼湊
、請求發起
、結果處理
等。所以從設計上來講,使用基類來統一這些行爲,再經過派生生成針對不一樣請求API
的子類,以便得到具體請求的靈活性:dom
@protocol SLBaseApiManagerDelegate
- (void)managerWillLoadData: (SLBaseApiManager *)manager;
- (void)managerDidLoadData: (SLBaseApiManager *)manager;
@end
@interface SLBaseApiManager : NSObject
@property (nonatomic, readonly) NSArray<id<SLBaseApiManagerDelegate>) *delegates;
- (void)loadWithParams: (NSDictionary *)params;
- (void)addDelegate: (id<SLBaseApiManagerDelegate>)delegate;
- (void)removeDelegate: (id<SLBaseApiManagerDelegate>)delegate;
@end
@interface SLBaseListApiManager : SLBaseApiManager
@property (nonatomic, readonly, assign) BOOL hasMore;
@property (nonatomic, readonly, copy) NSArray *dataList;
- (void)refreshData;
- (void)loadMoreData;
@end
複製代碼
離散型請求的一個特色是,將相同的請求邏輯抽離出來,統一行爲接口。除了請求行爲以外的行爲,包括請求數據解析、重試控制、請求是否互斥等行爲,每個請求API
都有單獨的manager
進行定製,靈活性更強。另外經過delegate
統一回調行爲,減小debug
難度,避免了block
方式潛在的引用問題等異步
在一次完整的fetch
數據過程當中,數據能夠分爲四種形態:工具
Data
AFN
等工具拉取的數據,通常是JSON
JSON
轉換而來,稱做Entity
Text
這四種數據形態的流動結構以下:
Server AFN controller view
------------- ------------- ------------- -------------
| | | | | | convert | |
| Data | ---> | JSON | ---> | Entity | ---> | Text |
| | | | | | | |
------------- ------------- ------------- -------------
複製代碼
普通狀況下,第三方請求庫會以JSON
的形態交付數據給業務方。考慮到客戶端與服務端的命名規範、以及可能存在的變動,多數狀況下客戶端會對JSON
數據加工成具體的Entity
數據實體,而後使用容器類保存。從上圖的四種數據形態來講,若是中間件必須選擇其中一種形態交付給業務層,Entity
應該是最合理的交付數據形態,緣由有三:
JSON
,業務封裝必須完成JSON -> Entity
的轉換,多數時候請求發起的業務在C
層中,而這些邏輯老是形成Fat Controller
的緣由Entity -> Text
涉及到了具體的上層業務,請求中間件不該該向上干涉。在JSON -> Entity
的轉換過程當中,Entity
已經組裝了業務封裝最須要的數據內容另外一個有趣的問題是Entity
描述的是數據流動的階段狀態,而非具體數據類型。打個比方,Entity
不必定非得是類對象實例,只要Entity
遵照業務封裝的讀取規範,能夠是instance
也能夠是collection
,好比一個面試者Entity
只要能提供姓名
和工做年限
這兩個關鍵數據便可:
/// 抽象模型
@interface SLInterviewer : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) CGFloat workYears;
@end
SLInterviewer *interviewer = entity;
NSLog(@"The interviewer name: %@ and work-years: %g", interviewer.name, interviewer.workYears);
/// 鍵值約定
extern NSString *SLInterviewerNameKey;
extern NSString *SLInterviewerWorkYearsKey;
NSDictionary *interviewer = entity;
NSLog(@"The interviewer name: %@ and work-years: %@", interviewer[SLInterviewerNameKey], interviewer[SLInterviewerWorkYearsKey]);
複製代碼
若是讓集約式請求的中間件交付Entity
數據,JSON -> Entity
的形態轉換可能會致使請求中間件涉及到具體的業務邏輯中,所以在實現上須要提供一個parser
來完成這一過程:
@protocol EntityParser
- (id)parseJSON: (id)JSON;
@end
@interface SLIntensiveRequest : NSObject
@property (nonatomic, strong) id<EntityParser> parser;
- (void)GET: (NSString *)url params: (id)params success: (SLSuccess)success failure: (SLFailure)failure;
@end
複製代碼
而相較之下,離散型請求中BaseManager
承擔了統一的請求行爲,派生的manager
徹底能夠直接將轉換的邏輯直接封裝起來,無需額外的Parser
,惟一須要考慮的是Entity
的具體實體對象是否須要抽象模型來表達:
@implementation SLGetInterviewerApiManager
/// 抽象模型
- (id)entityFromJSON: (id)json {
if ([json isKindOfClass: [NSDictionary class]]) {
return [SLInterviewer interviewerWithJSON: json];
} else {
return nil;
}
}
- (void)didLoadData {
self.dataList = self.response.safeMap(^id(id item) {
return [self entityFromJSON: item];
}).safeMap(^id(id interviewer) {
return [SLInterviewerInfo infoWithInterviewer: interviewer];
});
if ([_delegate respondsToSelector: @selector(managerDidLoadData:)]) {
[_delegate managerDidLoadData: self];
}
}
/// 鍵值約定
- (id)entityFromJSON: (id)json keyMap: (NSDictionary *)keyMap {
if ([json isKindOfClass: [NSDictionary class]]) {
NSDictionary *dict = json;
NSMutableDictionary *entity = @{}.mutableCopy;
for (NSString *key in keyMap) {
NSString *entityKey = keyMap[key];
entity[entityKey] = dict[key];
}
return entity.copy;
} else {
return nil;
}
}
@end
複製代碼
甚至再進一步,manager
能夠同時交付Text
和Entity
這兩種數據形態,使用parser
能夠對C
層完成隱藏數據的轉換過程:
@protocol TextParser
- (id)parseEntity: (id)entity;
@end
@interface SLInterviewerTextContent : NSObject
@property (nonatomic, readonly) NSString *name;
@property (nonatomic, readonly) NSString *workYear;
@property (nonatomic, readonly) SLInterviewer *interviewer;
- (instancetype)initWithInterviewer: (SLInterviewer *)interviewer;
@end
@implementation SLInterviewerTextParser
- (id)parseEntity: (SLInterviewer *)entity {
return [[SLInterviewerTextContent alloc] initWithInterviewer: entity];
}
@end
複製代碼
是否須要統一接口的請求封裝層
在App
中的請求分爲三類:GET
、POST
和UPLOAD
,在不考慮進行封裝的狀況下,核心層的請求接口至少須要三種不一樣的接口來對應這三種請求類型。此外還要考慮核心層的請求接口一旦發生變更(例如AFN
在更新至3.0
的時候修改了請求接口),所以對業務請求發起方來講,存在一個封裝的請求中間層能夠有效的抵禦請求接口改動的風險,以及有效的減小代碼量。上文能夠看到對業務層暴露的中間件manager
的做用是對請求的行爲進行統一,但並不干預請求的細節,所以manager
也能被當作是一個請求發起方,那麼在其下層須要有暴露統一接口的請求封裝層:
-------------
中間件 | Manager |
-------------
↓
↓
-------------
請求層 | Request |
-------------
↓
↓
-------------
核心請求 | CoreNet |
-------------
複製代碼
封裝請求層的問題在於如何只暴露一個接口來適應多種狀況類型,一個方法是將請求內容抽象成一系列的接口協議,Request
層根據接口返回參數調度具體的請求接口:
/// 協議接口層
enum {
SLRequestMethodGet,
SLRequestMethodPost,
SLRequestMethodUpload
};
@protocol RequestEntity
- (int)requestMethod; /// 請求類型
- (NSString *)urlPath; /// 提供域名中的path段,以便組裝:xxxxx/urlPath
- (NSDictionary *)parameters; /// 參數
@end
extern NSString *SLRequestParamPageKey;
extern NSString *SLRequestParamPageCountKey;
@interface RequestListEntity : NSObject<RequestEntity>
@property (nonatomic, assign) NSUInteger page;
@property (nonatomic, assign) NSUInteger pageCount;
@end
/// 請求層
typedef void(^SLRequestComplete)(id response, NSError *error);
@interface SLRequestEngine
+ (instancetype)engine;
- (void)sendRequest: (id<RequestEntity>)request complete: (SLRequestComplete)complete;
@end
@implementation SLRequestEngine
- (void)sendRequest: (id<RequestEntity>)request complete: (SLRequestComplete)complete {
if (!request || !complete) {
return;
}
if (request.requestMethod == SLRequestMethodGet) {
[self get: request complete: complete];
} else if (request.requestMethod == SLRequestMethodPost) {
[self post: request complete: complete];
} else if (request.requestMethod == SLRequestMethodUpload) {
[self upload: request complete: complete];
}
}
@end
複製代碼
這樣一來,當有新的請求API
時,建立對應的RequestEntity
和Manager
類來處理請求。對於業務上層來講,整個請求過程更像是一個異步的fetch
流程,一個單獨的manager
負責加載數據並在加載完成時回調。Manager
也不用瞭解具體是什麼請求,只須要簡單的配置參數便可,Manager
的設計以下:
@interface WSBaseApiManager : NSObject
@property (nonatomic, readonly, strong) id data;
@property (nonatomic, readonly, strong) NSError *error; /// 請求失敗時不爲空
@property (nonatomic, weak) id<WSBaseApiManagerDelegate> delegate;
@end
@interface WSBaseListApiManager : NSObject
@property (nonatomic, assign) BOOL hasMore;
@property (nonatomic, readonly, copy) NSArray *dataList;
@end
@interface SLGetInterviewerRequest: RequestListEntity
@end
@interface SLGetInterviewerManager : WSBaseListApiManager
@end
@implementation SLGetInterviewerManager
- (void)loadWithParams: (NSDictionary *)params {
SLGetInterviewerRequest *request = [SLGetInterviewerRequest new];
request.page = [params[SLRequestParamPageKey] unsignedIntegerValue];
request.pageCount = [params[SLRequestParamPageCountKey] unsignedIntegerValue];
[[SLRequestEngine engine] sendRequest: request complete: ^(id response, NSError *error){
/// do something when request complete
}];
}
@end
複製代碼
最終請求結構:
-------------
業務層 | Client |
-------------
↓
↓
-------------
中間件 | Manager |
-------------
↓
↓
-------------
| Request |
-------------
↓
↓
請求層 -----------------------------------
↓ ↓ ↓
↓ ↓ ↓
------------- ------------- -------------
| GET | | POST | | Upload |
------------- ------------- -------------
↓ ↓ ↓
↓ ↓ ↓
---------------------------------------------
核心請求 | CoreNet |
---------------------------------------------
複製代碼