關于網絡層的設計(一)——和業務層的對接

前言

關于網絡層的設計,最主要的是和業務層的對接問題。
網絡層設計得好,可以讓業務層開發事半功倍;反之,若網絡層設計地很糟糕,則會讓業務層開發事倍功半,心里法克連連。


p1.jpeg

關于網絡層和業務層的對接,我們一般從下面幾個方面進行考量:

  • 選擇哪種方式請求網絡數據,系統自帶的還是AFNetworking?
  • 以什么模式給業務層交付數據?delegate 還是block?
  • 交付給業務層什么形式的數據?直接返回dict就行了,還是把dict在網絡層轉換為ResultModel再交給業務層?
  • 封裝API應該選擇集約型還是離散型?

第一個問題,“選擇哪種方式請求網絡數據?”。第三方庫AFNetworking很強大而且使用起來比較簡單,所以一般我們選擇AFNetworking。蘋果自帶的NSURLSession等以后再研究。

第二個問題,“以什么模式給業務層交付數據?”。一般選擇Delegate和block。關于它們,應該說各有利弊吧,具體使用場景具體選擇使用。block使用起來較方便,但也有調試時不好追蹤,容易出現循環引用等坑的缺點。而且若在業務層block返回數據后,要做比較復雜的邏輯處理的話,那在block里會寫有大段代碼,這樣閱讀起來也不好,使代碼整體結構顯得很不清晰。
但是,在此,我們仍先以block為例來理解網絡層的設計。

第三個問題,“交付給業務層什么形式的數據?”。我們設計網絡層,就要想著能盡量減輕業務層的開發量,最好把網絡層從后臺拿到的一大串數據,剝離、加工、整理成業務層需要的數據格式然后再交付給它。

第四個問題,“封裝API應該選擇集約型還是離散型?”。所謂集約型,就是只能業務層提供一個方法,所有業務層的網絡請求都要通過該方法完成。因此,該方法至少要能傳入接口路徑(path)、請求方式(get/post)、請求參數(param)等。集約型的好處是對于網絡層的編寫來說方便快捷,但對業務層來說要傳入這么多參數并不太好。我們設計的目的就是盡量使業務層使用起來簡單輕巧,所以我們常常采用離散型方式。(說得不太恰當。集約型為所有的業務請求提供一個接口,省去了編寫業務模塊xxxManager的工作量。但對集約型而言,提供的這個唯一的網絡請求方法得有接口地址,請求方式,接口參數等多個參數。這是其繁瑣之處,而離散型則為了避免給業務層帶來這樣的繁瑣,而在xxxManager提供的接口方法里自己配置了接口地址interface和請求方式,并以方法名加以體現。那對業務層開發來說就簡潔明了了許多。一個比如用戶模塊UserManager里的對登錄請求的封裝,只需業務層傳入accountpassword兩個參數,而接口地址和請求方式已封裝在其方法里了login:password:success:failure,而且方法名也體現出了請求接口login。但離散型的問題是無疑為增加代碼量,為編寫xxxManager層將花費大量時間。)
不言而喻,和集約型相對的,離散型就是根據功能模塊分為不同的模塊,分別提供不同的方法給業務層調用。比如,把和用戶有關的所有網絡請求,放在一個叫UserManager的類中,登錄、注冊、修改密碼等分別提供不同的方法,這樣的好處在于,一、不同功能模塊放在不同的文件中,使項目結構更清晰,維護升級更容易;二、對于業務開發人員來說,不同的功能叫不同的方法名這樣更友好易懂。三、更重要的是,你可以在xxxManager這一層做一些針對該模塊的個性化處理。沒錯,你可以在這一層完成上個問題中所說的數據加工后再交付給業務層。我們把和用戶相關的網絡請求API都定義在UserManager類中,并在其中轉換為UserObject然后交付給業務層。
除此外,“離散”不僅體現在提供的API方法上,還體現在網絡請求連接上。我們定義一個HttpClient類,在該類中專門完成對服務器的網絡請求。并且給xxxManager這一層提供不同請求方式對應的方法。

好了,基本結構就是這樣,下面上代碼。我們“從內至外”的看代碼。
首先就是HttpClient這個類了,該類是完成網絡請求連接的核心。并給xxxManager提供網絡連接的接口方法。

HttpClient.h

#import <Foundation/Foundation.h>
#import "AFNetworking.h"

#define BaseURL @"http://192.168.1.125/v1/" // 服務器地址

typedef NS_ENUM(NSInteger, RequestMethod)
{
    POST = 0,
    GET,
    PUT,
    DELETE,
};


@interface HttpClient : NSObject

// get請求
- (void)getOfPath:(NSString *)path
          prama:(id)prama
        success:(void(^)(id result))success
        failure:(void(^)(NSError *error))failure;


// post請求
- (void)postOfPath:(NSString *)path
          prama:(id)prama
        success:(void(^)(id result))success
        failure:(void(^)(NSError *error))failure;

@end

HttpClient.m
注意,我們提供給外部get和post請求對應的兩個方法調用,但其實在內部,我們是定義了一個“全能方法”來完成網絡連接的,這才是核心。

#import "HttpClient.h"

@implementation HttpClient

// get
- (void)getOfPath:(NSString *)path
            prama:(id)prama
          success:(void(^)(id result))success
          failure:(void(^)(NSError *error))failure
{
    [self sendRequestOfType:GET path:path prama:prama success:success failure:failure];
}

// post
- (void)postOfPath:(NSString *)path
             prama:(id)prama
           success:(void(^)(id result))success
           failure:(void(^)(NSError *error))failure
{
    [self sendRequestOfType:POST path:path prama:prama success:success failure:failure];
}



// 完成網絡連接的核心
- (void)sendRequestOfType:(RequestMethod)requestType
                     path:(NSString *)path
                    prama:(id)prama
                  success:(void(^)(id result))success
                  failure:(void(^)(NSError *error))failure
{
    // 拼接參數,得到完整的接口地址
    NSURL *baseUrl = [NSURL URLWithString:BaseURL];
    NSString *pathUrl = [NSString stringWithFormat:@"%@%@",BaseURL,path];
    
    /**
     通常在此,可以完成拼接公共參數、密碼加密、或者簽名認證等操作。
     */

    AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseUrl];
    
    switch (requestType)
    {
            
// ------------ GET -------------
        case GET:
        {
            [manager GET:pathUrl
              parameters:prama
                 success:^(AFHTTPRequestOperation *operation, id responseObject) {
                        success(responseObject);
            } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                        failure(error);
            }];
            break;
        }
            
// ------------ POST -------------
            
        case POST:
        {
            [manager POST:pathUrl
               parameters:prama success:^(AFHTTPRequestOperation *operation, id responseObject) {
                        success(responseObject);
            } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                        failure(error);
            }];
            break;
        }
            
        default:
            break;
    }
    
}

@end

好了,現在看看xxxManager層。
UserManager.h

#import <Foundation/Foundation.h>
#import "UserObject.h"

@interface UserManager : NSObject

// 提供獲取UserManager實例的類方法
+ (UserManager *)getInstance;

// 給業務層提供的“登錄”功能的網絡數據請求方法
- (void)login:(NSString *)account
     password:(NSString *)password
      success:(void(^)(UserObject *userObj))success
      failure:(void(^)(NSError *error))failure;

@end

UserManager.m
登錄、注冊、修改密碼、修改個人資料分別提供不同的方法。在相應方法里通過調用HttpClient提供的網絡連接方法完成網絡連接,然后把數據轉換加工成業務層需要的數據格式UserObject,再交付之。

#import "UserManager.h"
#import "HttpClient.h"
#include "MJExtension.h"


@implementation UserManager


//=================================== UserManager ==========================================//
// 和User有關的所有請求接口路徑
NSString *const kUserLogin              = @"user/login";
NSString *const kUserRegister           = @"user/register";


+ (UserManager *)getInstance
{
    return [UserManager new];
}

- (void)login:(NSString *)account
     password:(NSString *)password
      success:(void(^)(UserObject *userObj))success
      failure:(void(^)(NSError *error))failure
{
    NSDictionary *pramaDict = @{@"account":account, @"password":password};

    // 通過HttpClient提供的請求方法完成網絡請求
    HttpClient *httpClient = [HttpClient new];
    
    [httpClient postOfPath:kUserLogin prama:pramaDict success:^(id result) {
        // 把服務器返回的json數據result轉換為UserObject類型的userObj
        UserObject *userObj = [UserObject mj_objectWithKeyValues:result];
        success(userObj);
    } failure:^(NSError *error) {
        failure(error);
    }];
}

@end

好了。當業務層開發人員需要完成“登錄”功能時,只需調用UserManager中我們定義的login方法就得到了網絡數據,并且已轉為UserObject給我們。

#import "ViewController.h"
#import "UserManager.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    [[UserManager getInstance] login:@"wang66" password:@"123456" success:^(UserObject *userObj) {
        NSLog(@"登錄后后臺返回用戶信息----%@",userObj.description);
    } failure:^(NSError *error) {
        NSLog(@"登錄失敗----%@",error);
    }];
    
    
}

@end



補充和優化

上面我們實現了一個簡單的網絡層,但其實是比較簡陋的。真是情況要考慮很多地方的。

1. 在請求中添加簽名認證,保證請求來源于我們自己的APP。

2. 取消無用的請求。
比如,比如我們剛進入一個界面后,此刻便會發出一條該界面數據的請求,但是此時用戶卻點了“返回”,退回了上個界面。此時上個界面的請求已經飛出但還未完成。這時,我們應當取消上個界面的請求,釋放帶寬。這樣對于下來的網絡請求是有利的。

3. 錯誤信息的處理

// ------------ GET -------------
        case GET:
        {
            [manager GET:pathUrl
              parameters:prama
                 success:^(AFHTTPRequestOperation *operation, id responseObject) {
                        success(responseObject);
            } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                        failure(error);
            }];
            break;
        }

這段代碼,AFNetworking提供的GET請求,請求成功時回調block返回responseObject,失敗時返回error。但是請注意,這里的錯誤回調僅僅指網絡請求錯誤,要注意區分網絡錯誤和業務錯誤(也就是網絡請求是成功的,但是對于我們的業務來說,是有問題的)。這些信息同樣是會在responseObject返回。實際上一般網絡請求成功后,后臺返回的responseObject一般都有errorCode字段,只有當errorCode=0時,就說明一切OK,正常返回了我們需要的數據。所以,為了給業務層提供方便,我們還得在網絡層做些處理。使交付給業務層的數據里,成功回調的block里就純粹了業務邏輯意義上正確的數據,而失敗的回調里的數據則包括一切錯誤信息。所說的處理就是在該方法里對回調block做層包裝。

// 完成網絡連接的核心
- (void)sendRequestOfType:(RequestMethod)requestType
                     path:(NSString *)path
                    prama:(id)prama
                  success:(void(^)(id result))success
                  failure:(void(^)(NSError *error))failure
{
    // 拼接參數,得到完整的接口地址
    NSURL *baseUrl = [NSURL URLWithString:BaseURL];
    NSString *pathUrl = [NSString stringWithFormat:@"%@%@",BaseURL,path];
    
    /**
     通常在此,可以拼接一些公共參數,或者簽名認證參數。
     */

    AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseUrl];
    
    // ------------------ 包裝回調block ------------------
    //請求成功block
    void(^ok)(id responseObject) = ^(id responseObject){
        if([responseObject isKindOfClass:[NSDictionary class]])
        {
            Result *result = [Result mj_objectWithKeyValues:responseObject];
            if (result.errorCode == 0)
            {
                success(responseObject); //業務邏輯意義上的正確返回。
            }
            else
            {
                // 有錯誤。
                NSError *error = [NSError errorWithDomain:result.message code:result.errorCode userInfo:nil];
                failure(error);
            }
        }
        else
        {
            NSError *error = [NSError errorWithDomain:@"服務返回數據異常" code:-1 userInfo:nil];
            failure(error);
        }
    };
    
    //請求失敗block
    void(^fail)(NSError *error) = ^(NSError *error){
        failure(error);
    };
    
    // ------------------------------------


    
    switch (requestType)
    {
// ------------ GET -------------
        case GET:
        {
            [manager GET:pathUrl
              parameters:prama
                 success:^(AFHTTPRequestOperation *operation, id responseObject) {
//                        success(responseObject);
                     ok(responseObject);
            } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//                        failure(error);
                    fail(error);
                
            }];
            break;
        }
            
// ------------ POST -------------
            
        case POST:
        {
            [manager POST:pathUrl
               parameters:prama success:^(AFHTTPRequestOperation *operation, id responseObject) {
//                        success(responseObject);
                   ok(responseObject);
            } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//                        failure(error);
                    fail(error);
            }];
            break;
        }
            
        default:
            break;
    }
}

** 4.多服務器多環境切換:**
一般比較規范的項目都有開發環境、測試環境、預發布環境、正式環境(生產環境)四種環境,它們對應的服務器地址分別是不同的。在項目版本迭代過程以“開發——>測試——>預發布——>正式”這個順序進行的。開發環境就是新需求下來后的更改。測試環境就是給測試打了包后,改bug時的更改。預發布環境就是測試基本完成,交付給運營測試,改動基本比較小。正式環境不用解釋,不言而喻。
我們可以把多環境的配置寫在預編譯頭文件中:

/************環境配置開關**********
 * OPEN_TEST  0:為開發環境
 *            1:為測試環境
 *            2:為預發布外網環境
 *            其他:為生產環境
 ***************************/


#define OPEN_TEST 0

#if (OPEN_TEST == 0)/************開發環境************/
#define HTTPSURLEVER  @"http://www.runedu.test/api"


#elif (OPEN_TEST == 1)/************測試環境************/
#define HTTPSURLEVER  @"http://www.rjy.rd/api"


#elif (OPEN_TEST == 2)/************預發布環境************/
#define HTTPSURLEVER  @"http://www.prerjy.com/api"


#else/************生產環境************/
#define HTTPSURLEVER @"http://www.runjiaoyu.com.cn/api"

#endif
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容