離散請求

騎著jm的hi發表於2019-03-04

文章地址

網路層作為App架構中至關重要的中介軟體之一,承擔著業務封裝和核心層網路請求互動的職責。討論請求中介軟體實現方案的意義在於中介軟體要如何設計以便減少對業務對接的影響;明晰請求流程中的職責以便寫出更合理的程式碼等。因此在講如何去設計請求中介軟體時,主要考慮三個問題:

  • 業務以什麼方式發起請求
  • 請求資料如何交付業務層
  • 如何實現通用的請求介面

以什麼方式發起請求

根據暴露給業務層請求API的不同,可以分為集約式請求離散型請求兩類。集約式請求對外只提供一個類用於接收包括請求地址、請求引數在內的資料資訊,以及回撥處理(通常使用block)。而離散型請求對外提供通用的擴充套件介面完成請求

集約式請求

考慮到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;
複製程式碼

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

  1. 請求環境配置、引數構建、請求任務控制等業務無關程式碼
  2. 請求邏輯和回撥邏輯在同一處違背了單一原則
  3. block回撥潛在的引用問題

在業務封裝的層面上,應該只關心何時發起請求展示請求結果。設計上,請求中介軟體應當只暴露必要的引數property,隱藏請求過程和返回資料的處理

離散型請求

和集約式請求不同,對於每一個請求API都會有一個manager來管理。在使用manager的時候只需要建立例項,執行一個類似load的方法,manager會自動控制請求的發起和處理:

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

集約式請求和離散型請求最終的實現方案並不是互斥的,從底層請求的具體行為來看,最終都有統一執行的步驟:域名拼湊請求發起結果處理等。因此從設計上來說,使用基類來統一這些行為,再通過派生生成針對不同請求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
複製程式碼

離散型請求的一個特點是,將相同的請求邏輯抽離出來,統一行為介面。除了請求行為之外的行為,包括請求資料解析、重試控制、請求是否互斥等行為,每一個請求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應該是最合理的交付資料形態,原因有三:

  1. 如果交付的是JSON,業務封裝必須完成JSON -> Entity的轉換,多數時候請求發起的業務在C層中,而這些邏輯總是造成Fat Controller的原因
  2. 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可以同時交付TextEntity這兩種資料形態,使用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中的請求分為三類:GETPOSTUPLOAD,在不考慮進行封裝的情況下,核心層的請求介面至少需要三種不同的介面來對應這三種請求型別。此外還要考慮核心層的請求介面一旦發生變動(例如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時,建立對應的RequestEntityManager類來處理請求。對於業務上層來說,整個請求過程更像是一個非同步的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                 |
                   ---------------------------------------------
複製程式碼

關注我的公眾號獲取更新資訊

相關文章