分析實(shí)現(xiàn)-離散請求

原文地址

網(wǎng)絡(luò)層作為App架構(gòu)中至關(guān)重要的中間件之一,承擔(dān)著業(yè)務(wù)封裝和核心層網(wǎng)絡(luò)請求交互的職責(zé)耸成。討論請求中間件實(shí)現(xiàn)方案的意義在于中間件要如何設(shè)計(jì)以便減少對業(yè)務(wù)對接的影響;明晰請求流程中的職責(zé)以便寫出更合理的代碼等坛缕。因此在講如何去設(shè)計(jì)請求中間件時墓猎,主要考慮三個問題:

  • 業(yè)務(wù)以什么方式發(fā)起請求
  • 請求數(shù)據(jù)如何交付業(yè)務(wù)層
  • 如何實(shí)現(xiàn)通用的請求接口

以什么方式發(fā)起請求

根據(jù)暴露給業(yè)務(wù)層請求API的不同,可以分為集約式請求離散型請求兩類赚楚。集約式請求對外只提供一個類用于接收包括請求地址、請求參數(shù)在內(nèi)的數(shù)據(jù)信息骗卜,以及回調(diào)處理(通常使用block)宠页。而離散型請求對外提供通用的擴(kuò)展接口完成請求

集約式請求

考慮到AFNetworking基本成為了iOS的請求標(biāo)準(zhǔn),以傳統(tǒng)的集約式請求代碼為例:

/// 請求地址和參數(shù)組裝
NSString *domain = [SLNetworkEnvironment currentDomain];
NSString *url = [domain stringByAppendingPathComponent: @"getInterviewers"];
NSDictionary *params = @{
    @"page": @1,
    @"pageCount": @20,
    @"filterRule": @"work-years >= 3"
};

/// 構(gòu)建新的請求對象發(fā)起請求
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;

這樣的請求代碼存在這些問題:

  1. 請求環(huán)境配置寇仓、參數(shù)構(gòu)建举户、請求任務(wù)控制等業(yè)務(wù)無關(guān)代碼
  2. 請求邏輯和回調(diào)邏輯在同一處違背了單一原則
  3. block回調(diào)潛在的引用問題

在業(yè)務(wù)封裝的層面上,應(yīng)該只關(guān)心何時發(fā)起請求展示請求結(jié)果遍烦。設(shè)計(jì)上俭嘁,請求中間件應(yīng)當(dāng)只暴露必要的參數(shù)property,隱藏請求過程和返回數(shù)據(jù)的處理

離散型請求

和集約式請求不同服猪,對于每一個請求API都會有一個manager來管理供填。在使用manager的時候只需要創(chuàng)建實(shí)例,執(zhí)行一個類似load的方法罢猪,manager會自動控制請求的發(fā)起和處理:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.getInterviewerApiManager = [SLGetInterviewerApiManager new];
    [self.getInterviewerApiManager addDelegate: self];
    [self.getInterviewerApiManager refreshData];
}

集約式請求和離散型請求最終的實(shí)現(xiàn)方案并不是互斥的近她,從底層請求的具體行為來看,最終都有統(tǒng)一執(zhí)行的步驟:域名拼湊膳帕、請求發(fā)起粘捎、結(jié)果處理等。因此從設(shè)計(jì)上來說,使用基類來統(tǒng)一這些行為攒磨,再通過派生生成針對不同請求API的子類泳桦,以便獲得具體請求的靈活性:

@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

離散型請求的一個特點(diǎn)是,將相同的請求邏輯抽離出來娩缰,統(tǒng)一行為接口灸撰。除了請求行為之外的行為,包括請求數(shù)據(jù)解析漆羔、重試控制梧奢、請求是否互斥等行為,每一個請求API都有單獨(dú)的manager進(jìn)行定制演痒,靈活性更強(qiáng)亲轨。另外通過delegate統(tǒng)一回調(diào)行為,減少debug難度鸟顺,避免了block方式潛在的引用問題等

請求數(shù)據(jù)如何交付

在一次完整的fetch數(shù)據(jù)過程中惦蚊,數(shù)據(jù)可以分為四種形態(tài):

  • 服務(wù)端直接返回的二進(jìn)制形態(tài),稱為Data
  • AFN等工具拉取的數(shù)據(jù)讯嫂,一般是JSON
  • 被持久化或非短暫持有的形態(tài)蹦锋,一般從JSON轉(zhuǎn)換而來,稱作Entity
  • 展示在屏幕上的文本形態(tài)欧芽,大概率需要再加工莉掂,稱作Text

這四種數(shù)據(jù)形態(tài)的流動結(jié)構(gòu)如下:

    Server                  AFN                   controller                view
-------------           -------------           -------------           -------------
|           |           |           |           |           |  convert  |           |
|   Data    |   --->    |   JSON    |   --->    |   Entity  |   --->    |    Text   |    
|           |           |           |           |           |           |           |
-------------           -------------           -------------           -------------

普通情況下,第三方請求庫會以JSON的形態(tài)交付數(shù)據(jù)給業(yè)務(wù)方千扔≡髅睿考慮到客戶端與服務(wù)端的命名規(guī)范、以及可能存在的變更曲楚,多數(shù)情況下客戶端會對JSON數(shù)據(jù)加工成具體的Entity數(shù)據(jù)實(shí)體厘唾,然后使用容器類保存。從上圖的四種數(shù)據(jù)形態(tài)來說龙誊,如果中間件必須選擇其中一種形態(tài)交付給業(yè)務(wù)層抚垃,Entity應(yīng)該是最合理的交付數(shù)據(jù)形態(tài),原因有三:

  1. 如果交付的是JSON趟大,業(yè)務(wù)封裝必須完成JSON -> Entity的轉(zhuǎn)換鹤树,多數(shù)時候請求發(fā)起的業(yè)務(wù)在C層中,而這些邏輯總是造成Fat Controller的原因
  2. Entity -> Text涉及到了具體的上層業(yè)務(wù)护昧,請求中間件不應(yīng)該向上干涉魂迄。在JSON -> Entity的轉(zhuǎn)換過程中,Entity已經(jīng)組裝了業(yè)務(wù)封裝最需要的數(shù)據(jù)內(nèi)容

另一個有趣的問題是Entity描述的是數(shù)據(jù)流動的階段狀態(tài)惋耙,而非具體數(shù)據(jù)類型捣炬。打個比方熊昌,Entity不一定非得是類對象實(shí)例,只要Entity遵守業(yè)務(wù)封裝的讀取規(guī)范湿酸,可以是instance也可以是collection婿屹,比如一個面試者Entity只要能提供姓名工作年限這兩個關(guān)鍵數(shù)據(jù)即可:

/// 抽象模型
@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數(shù)據(jù),JSON -> Entity的形態(tài)轉(zhuǎn)換可能會導(dǎo)致請求中間件涉及到具體的業(yè)務(wù)邏輯中推溃,因此在實(shí)現(xiàn)上需要提供一個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承擔(dān)了統(tǒng)一的請求行為,派生的manager完全可以直接將轉(zhuǎn)換的邏輯直接封裝起來铁坎,無需額外的Parser蜂奸,唯一需要考慮的是Entity的具體實(shí)體對象是否需要抽象模型來表達(dá):

@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

甚至再進(jìn)一步,manager可以同時交付TextEntity這兩種數(shù)據(jù)形態(tài)硬萍,使用parser可以對C層完成隱藏數(shù)據(jù)的轉(zhuǎn)換過程:

@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

通用的請求接口

是否需要統(tǒng)一接口的請求封裝層

App中的請求分為三類:GET扩所、POSTUPLOAD,在不考慮進(jìn)行封裝的情況下朴乖,核心層的請求接口至少需要三種不同的接口來對應(yīng)這三種請求類型祖屏。此外還要考慮核心層的請求接口一旦發(fā)生變動(例如AFN在更新至3.0的時候修改了請求接口),因此對業(yè)務(wù)請求發(fā)起方來說买羞,存在一個封裝的請求中間層可以有效的抵御請求接口改動的風(fēng)險袁勺,以及有效的減少代碼量。上文可以看到對業(yè)務(wù)層暴露的中間件manager的作用是對請求的行為進(jìn)行統(tǒng)一畜普,但并不干預(yù)請求的細(xì)節(jié)期丰,因此manager也能被當(dāng)做是一個請求發(fā)起方,那么在其下層需要有暴露統(tǒng)一接口的請求封裝層:

            -------------
中間件       |  Manager  |
            -------------
                  ↓
                  ↓
            -------------
請求層       |  Request  |
            -------------
                  ↓
                  ↓
            -------------
核心請求     |  CoreNet  |
            -------------

封裝請求層的問題在于如何只暴露一個接口來適應(yīng)多種情況類型吃挑,一個方法是將請求內(nèi)容抽象成一系列的接口協(xié)議咐汞,Request層根據(jù)接口返回參數(shù)調(diào)度具體的請求接口:

/// 協(xié)議接口層
enum {
    SLRequestMethodGet,
    SLRequestMethodPost,
    SLRequestMethodUpload  
};

@protocol RequestEntity

- (int)requestMethod;           /// 請求類型
- (NSString *)urlPath;          /// 提供域名中的path段,以便組裝:xxxxx/urlPath
- (NSDictionary *)parameters;   /// 參數(shù)

@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

這樣一來儒鹿,當(dāng)有新的請求API時,創(chuàng)建對應(yīng)的RequestEntityManager類來處理請求几晤。對于業(yè)務(wù)上層來說约炎,整個請求過程更像是一個異步的fetch流程,一個單獨(dú)的manager負(fù)責(zé)加載數(shù)據(jù)并在加載完成時回調(diào)蟹瘾。Manager也不用了解具體是什么請求圾浅,只需要簡單的配置參數(shù)即可,Manager的設(shè)計(jì)如下:

@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

最終請求結(jié)構(gòu):

                                    -------------
業(yè)務(wù)層                               |  Client  |
                                    -------------
                                          ↓
                                          ↓
                                    -------------
中間件                               |  Manager  |
                                    -------------
                                          ↓
                                          ↓
                                    -------------
                                    |  Request  |
                                    -------------
                                          ↓
                                          ↓
請求層                   -----------------------------------
                        ↓                 ↓               ↓
                        ↓                 ↓               ↓
                   -------------    -------------   -------------                
                   |    GET    |    |   POST    |   |   Upload  |                
                   -------------    -------------   -------------    
                        ↓                 ↓               ↓
                        ↓                 ↓               ↓
                   ---------------------------------------------
核心請求            |                   CoreNet                 |
                   ---------------------------------------------
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末憾朴,一起剝皮案震驚了整個濱河市狸捕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌众雷,老刑警劉巖灸拍,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件做祝,死亡現(xiàn)場離奇詭異,居然都是意外死亡鸡岗,警方通過查閱死者的電腦和手機(jī)混槐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來轩性,“玉大人声登,你說我怎么就攤上這事〈眨” “怎么了悯嗓?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長卸察。 經(jīng)常有香客問我脯厨,道長,這世上最難降的妖魔是什么蛾派? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任俄认,我火速辦了婚禮,結(jié)果婚禮上洪乍,老公的妹妹穿的比我還像新娘眯杏。我一直安慰自己,他們只是感情好壳澳,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布岂贩。 她就那樣靜靜地躺著,像睡著了一般巷波。 火紅的嫁衣襯著肌膚如雪萎津。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天抹镊,我揣著相機(jī)與錄音锉屈,去河邊找鬼。 笑死垮耳,一個胖子當(dāng)著我的面吹牛颈渊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播终佛,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼俊嗽,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了铃彰?” 一聲冷哼從身側(cè)響起绍豁,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎牙捉,沒想到半個月后竹揍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體敬飒,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年鬼佣,在試婚紗的時候發(fā)現(xiàn)自己被綠了驶拱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡晶衷,死狀恐怖蓝纲,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情晌纫,我是刑警寧澤税迷,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站锹漱,受9級特大地震影響箭养,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜哥牍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一毕泌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嗅辣,春花似錦撼泛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蛙奖,卻和暖如春潘酗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背雁仲。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工仔夺, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人攒砖。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓囚灼,卻偏偏與公主長得像,于是被迫代替她去往敵國和親祭衩。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容