iOS的MVC框架之模型層的構建

歐陽大哥2013發表於2018-02-24

這篇文章是論MVVM偽框架結構和MVC中M的實現機制的姊妹篇。在前面的文章中更多介紹的是一些理論性質的東西,一些小夥伴在評論中也說希望有一些具體設計實踐的例子,以及對一些問題進行了更加深入的交流討論,因此準備了這篇文章。這篇文章將更多的介紹如何來進行模型層構建。

框架中層次的劃分主要是基於角色和職責作為標準,某些具有相同性質的角色和職責聚合在一起而形成了一個層的概念。MVC框架也是如此,M層負責業務的構建和實現、V層負責展示和進行輸入輸出互動、C層則負責進行整個系統的協調和控制。說的通俗一點就是V層是我要什麼,M層是我有什麼,C層則是我怎麼去做?

在前一篇文章的評論區中還有一些同學提出了用JSON構建的資料模型稱為模型層,其實這是一個誤區,JSON構建的資料模型只是一種資料結構的描述,他其實並不是一種角色或者是一種職責,因此他並不是MVC中所說的M。嚴格的說他只是M所操作的資料物件,希望大家能夠體會到這一點。

廢話了那麼多,回到我們構建模型層的正題裡面來,如何來構建一個模型層呢?蘋果的開發框架中並沒有定義一個標準模式,原因是業務是複雜多樣且沒有標準可言,只有當某個業務場景是明確時才可能有標準。那麼在蘋果的SDK框架中除了提供V層和C的UIKit.framkework框架外,有沒有提供一些具體的業務框架呢?

有!

我們要舉例或者學習如何定義M層架構其實並不需要從其他地方去找,iOS本身的很多業務框架就提供了非常經典的設計思路。比如定位框架CoreLocation.framework和地圖MapKit.framework框架就實現了經典的MVC中M層的設計模式。我其實主要也是想介紹定位框架是如何來實現M層的。需要注意的是本文並不是要介紹定位庫如何使用的,而是介紹這個庫是如何實現M層的。

iOS的定位庫CoreLocation.framework對M層的封裝實現

◎第一步:業務建模

我們知道CoreLocation.framework是iOS用來進行定位的一個庫。定位就是一種具體的業務需求場景。一般的定位需求就是需要隨時獲取我的當前位置,並且在我的當前位置更新後還需要實時的通知觀察使用者;以及需要知道某個位置具體是在哪個國家哪個城市哪個街道等地標資訊。有了這些需求後就能進行業務模型的構建了:

  • 需要有一個位置類來對位置進行描述。位置裡面應該有經緯度值、位置海拔、以及位置方向等資訊。
  • 需要有一個地標類來描述某個位置是哪個國家、城市、街道等資訊。
  • 需要有一個位置管理器來獲取我當前的位置、以及需要實時的進行位置更新和位置變化的通知。
  • 需要有一個地標解析器來根據指定的位置獲取到對應的地標資料。

上面就是一個定位業務所應該具有的基本需求,因此我們可以根據這些需求來進行建模:

定位業務靜態模型

沒錯上面你所見到的類圖,其實就是蘋果定位庫的業務模型框架的定義。下面就是主體類的大概定義(節選自CoreLocation.framework的標頭檔案):


//位置類
@interface CLLocation : NSObject <NSCopying, NSSecureCoding>

- (instancetype)initWithLatitude:(CLLocationDegrees)latitude
	longitude:(CLLocationDegrees)longitude;

@property(readonly, nonatomic) CLLocationCoordinate2D coordinate;

@end

//地標類
@interface CLPlacemark : NSObject <NSCopying, NSSecureCoding>

@property (nonatomic, readonly, copy, nullable) CLLocation *location;
@property (nonatomic, readonly, copy, nullable) NSString *locality; 
@property (nonatomic, readonly, copy, nullable) NSString *country; 

@end

//位置管理器類
@interface CLLocationManager : NSObject

@property(assign, nonatomic, nullable) id<CLLocationManagerDelegate> delegate;

@property(readonly, nonatomic, copy, nullable) CLLocation *location;

- (void)startUpdatingLocation;
- (void)stopUpdatingLocation;
@end

//地標解析器類
@interface CLGeocoder : NSObject

- (void)reverseGeocodeLocation:(CLLocation *)location completionHandler:(CLGeocodeCompletionHandler)completionHandler;

@end


//位置更新介面
@protocol CLLocationManagerDelegate<NSObject>

- (void)locationManager:(CLLocationManager *)manager
	 didUpdateLocations:(NSArray<CLLocation *> *)locations;

@end
複製程式碼

◎第二步:屬性設計

當類結構和框架確定下來後,接下來我們就需要對類的屬性進行設計了。類的屬性描述了一個類所具有的特性,正是因為屬性值的不同而產生了物件之間的差異。從上面的類圖以及業務需求中我們可以知道一個位置類應該具有經度和緯度屬性,而一個地標類則應該具有位置、地標所屬的國家、城市和街道等資訊,而一個位置管理器類則應該具有一個當前位置屬性和委託屬性。我們知道一個類就是一些屬性和操作方法的集合,而在實踐中並非所有的類中都必須要有屬性和方法。怎麼來判別那些類需要方法那些類不需要方法呢?一個原則就是從業務分析的角度中找出操作與被操作者。一般被操作者只需要定義屬性,它所具有的功能只是對一個個現實事物的抽象;而操作者則通常同時具有屬性和操作其他屬性的方法,他所具有的功能是負責實現某種能力,以及維護和更新某些資料。 我們通常把只有屬性而沒有加工方法的類稱之為資料模型類,而同時具有屬性和加工方法的類稱之為業務類或者為服務類。上面的例子中我們可以看出位置類和地標類是屬於資料模型類,而位置管理器和地標解析器則是屬於業務類。

只讀屬性

仔細觀察上面大部分類的屬性的定義都被設定為了只讀屬性。比如CLLocationManager類中對於當前位置的屬性的定義:

   @property(readonly, nonatomic, copy, nullable) CLLocation *location;
複製程式碼

這裡裡面的location屬性就是用來表示位置管理器物件的當前位置。我們發現這個屬性被定義為了只讀,這裡為什麼要定義為只讀呢?原因就是因為我們的位置管理器類的職責就是負責管理當前的位置,同時內部會實時的更新這個當前的位置。而對於外部使用者來說只需要在適當的時候讀取這個屬性中的資料就可以了。使用者是不需要維護和更新這個位置值的。這種設計機制也給外部使用者明確的傳達了一個資訊就是外部使用者只要負責讀取資料就好了,具體的資料更新則是由提供者來完成。這種設計的思想很清晰的體現了層次分明的概念。而且從編碼的角度也能減少屬性值的誤更新和亂用。另外一個原因就是保護資料的安全性,一個類的屬性一旦暴露出去後你就無法控制使用者如何去使用這些屬性了,如果使用者不清楚業務邏輯而手動去改寫了某個資料模型或者業務模型的屬性值時就有可能造成災難性的後果,所以我們最好還是將資料的更新交給業務提供方而不是業務使用方。

在實踐中模型層的類設計最好也遵守這個原則:

  • 業務類中的屬性設計為只讀。使用者只能通過屬性來讀取資料。而由業務類中的方法內部來更新這些屬性的值。
  • 資料模型類中的屬性定義最好也設定為只讀,因為資料模型的建立是在業務類方法內部完成並通過通知或者非同步回撥的方式交給使用者。而不應該交由使用者來建立和更新。
  • 資料模型類一般提供一個帶有所有屬性的init初始化方法,而初始化後這些屬性原則上是不能被再次改變,所以應該設定為只讀屬性。

上面的設計原則都是基於消費者和生產者理論來構建的,生產者也就是M層負責資料的構建和更新,消費者也就是C層或者V層來負責資料的使用和消費。我們可以通過下面兩個例子來體驗這種差異化:

  1. 可讀寫屬性的資料模型
  //..........................................
  //模型層中使用者類的定義 User.h
  @interface User
       @property(nonatomic, copy)  NSString *name;
       @property(nonatomic, assign) BOOL isLogin;
   @end

  //..........................................
  //模型層中使用者類的實現User.m
  @implementation User
  @end

//..........................................
//模型層中使用者管理器類的定義 UserManager.h
@interface UserManager

   //單例物件
   +(instanceType)sharedInstance;

    //定義當前登入的使用者。
    @property(nonatomic, strong) User *currentUser;
  
   //登入方法
    -(void)loginWith:(User*)user;

@end

//..........................................
//模型層中使用者管理器類的實現UserManager.m
@implementation UserManager

-(void)loginWith:(User*)user
{
         user.isLogin = YES;
         self.currentUser = user;
}
   
@end

//..........................................
//VC中某個使用登入的場景

-(void)handleLogin:(id)sender
{
      User *user =[User new];
      user.name = @"jack";

      //用jack執行登入成功!!
     [[UserManager sharedInstance] loginWith:user];

    /*因為沒有約束,呼叫者可以任意的修改登入的名字以及登入狀態,以及將currentUser變為了nil表示沒有使用者登入了。
       因為沒有屬性保護導致使用過程中可能出現不當使用而產生未可知的問題。*/
    user.name = @"bob";
   user.isLogin = NO;
   [UserManager sharedInstance].currentUser = nil;
}


複製程式碼

2.只讀屬性的資料模型

//..........................................
//模型層中使用者類的對外定義.h
 @interface User
       @property(nonatomic, copy, readonly)  NSString *name;
       @property(nonatomic, assign, readonly) BOOL isLogin;
   @end

//..........................................
//模型層中使用者類的實現.m
//在內部的擴充套件中屬性重新定義為讀寫,以便內部修改。
@interface User()
     @property(nonatomic, copy) NSString *name;
     @property(nonatomic, assign) BOOL isLogin;
@end

  @implementation User
  @end  

//..........................................
//模型層中使用者管理器類的定義 UserManager.h
@interface UserManager

   //單例物件
   +(instanceType)sharedInstance;

    //定義當前登入的使用者。
    @property(nonatomic, strong, readonly) User *currentUser;
  
   //登入方法
    -(void)loginWith:(NSString *)name;

@end

//..........................................
//模型層中使用者管理器類的實現UserManager.m

//因為UserManager內部要讀寫User的屬性,因此這裡要將這些屬性再次申明一下。
@interface User(UsedByUserManager)
    @property(nonatomic, copy) NSString *name;
    @property(nonatomic, assign) BOOL isLogin;
@end

@implementation UserManager
{
    //你也可以這樣在內部來定義一個可讀寫屬性。
     User *_currentUser;
}

-(void)loginWith:(NSString*)name
{
      _currentUser = [User new];
      _currentUser.name = name;
     _currentUser.isLogin = YES;
}
@end


..........................................
//VC中某個使用登入的場景

-(void)handleLogin:(id)sender
{
      //用jack執行登入成功!!
     [[UserManager sharedInstance] loginWith:@"jack"];

     //使用者後續都無法對currentUser進行任何修改!只能讀取。從而保證了資料的安全性和可靠性。
   
}

複製程式碼

很明顯上面通過只讀屬性的封裝,我們的模型層的標頭檔案程式碼定義和使用將更加清晰,而且保證了資料和使用的安全性問題。同時上面也介紹了一種屬性內外定義差異化的技巧,對外面暴露的儘可能的少和簡單,而同一個層次內部則可以放開出很多隱藏的屬性和方法。再比如下面的程式碼:


//外部標頭檔案。
  @interface User
        @property(nonatomic, readonly) NSString *name;
        @property(nonatomic, readonly) NSArray  *accounts;
   @end  


 //內部實現檔案。
 @interface User()
     @property(nonatomic, copy) NSString *name;
     @property(nonatomic, strong) NSMutableArray  *accounts;

     -(id)initWithName:(NSString*)name;
 @end

@implementation User
   //....
@end

複製程式碼

◎第三步:方法設計

類的屬性設計完成後,接下來就需要考慮類的方法的設計了。一般場景下業務模型所要解決的事情,最終都要走網路向伺服器進行訪問,或者訪問本地資料庫。這兩種型別的處理都跟IO有關,進行IO的一個問題就是可能會阻塞,如果我們將IO放在主執行緒的話那麼就可能導致主執行緒被阻塞而不能響應使用者的請求了。因此一般情況下我們設計業務類的方法時就不能考慮同步返回以及同步阻塞了。而是要採用呼叫方法立即返回且資料更新後非同步通知的模式了。

上面有說到我們希望的一個功能是位置管理器能夠實時的更新當前的位置並通知給使用者,以及地標解析器能夠根據輸入的位置來解析出一個地標物件。這兩個需求都有可能產生阻塞,因此對應的類裡面提供的方法就應該採用非同步的方式來實現。這裡面iOS用到了兩種經典的非同步通知返回機制:Delegate和Block回撥方式。

Delegate非同步通知方式

來考察一下定位管理器類CLLocationManager的定義裡面的一個屬性:

    @property(assign, nonatomic, nullable) id<CLLocationManagerDelegate> delegate;
複製程式碼

這個屬性指定了一個委託者,也就是說如果某個使用者物件要想實時的接收到位置變化的通知,那麼他只需要實現CLLocationManagerDelegate這個介面協議並賦值給CLLocationManager物件的delegate即可。我們來看CLLocationManagerDelegate的部分定義:

@protocol CLLocationManagerDelegate<NSObject>

@optional
- (void)locationManager:(CLLocationManager *)manager
	 didUpdateLocations:(NSArray<CLLocation *> *)locations API_AVAILABLE(ios(6.0), macos(10.9));

@end
複製程式碼

可以看出當位置管理器物件更新了當前的位置後就會呼叫delegate屬性所指物件的didUpdateLocations方法來通知對應的使用觀察者,然後使用觀察者就會根據最新的位置進行某些特定的處理。 但這裡還需要解決幾個問題?

  1. 誰來建立M層的位置管理物件? 答案是: 控制器C。因為控制器是負責協調和使用M層物件的物件,所以C層具有負責建立並持有M層物件的責任,C層也是一個使用觀察者。

  2. M層如何來實現實時的更新和停止更新? 答案是: 在位置管理器類裡面提供了2個方法:

/*
 *  startUpdatingLocation
 *  
 *  Discussion:
 *      Start updating locations.
 */
- (void)startUpdatingLocation API_AVAILABLE(watchos(3.0)) __TVOS_PROHIBITED;

/*
 *  stopUpdatingLocation
 *  
 *  Discussion:
 *      Stop updating locations.
 */
- (void)stopUpdatingLocation;

複製程式碼

位置管理器物件通過這兩個方法來實現位置的實時更新啟動和停止。也就是說位置的實時更新和停止都是由M層來實現,至於他如何做到的則是一個黑盒,呼叫者不需要關心任何實現的細節。

  1. 誰來負責呼叫M層提供的那些方法? 答案是: 控制器C層。因為控制器既然負責M層物件的構建,那他當然也是負責M層方法的呼叫了。

  2. 誰來觀察M層的資料變化通知並進行相應的處理? 答案是: 控制器C層。因為C層既然負責呼叫M層所提供的方法,那麼他理所當然的也要負責對方法的返回以及更新進行處理。在這裡我們的C層控制器需要實現CLLocationManagerDelegate介面,並賦值給位置管理器物件的delegate屬性。

定位管理器的Delegate通知機制你是否有似曾相似的感覺? 沒有錯UITableView也是採用這種機制來實現控制器C和檢視V之間的互動的和資料更新的。UITableView中也指定一個dataSource和delegate物件來進行介面的更新通知處理,同樣也提供了一個reloadData的方法來進行介面的更新。

我們知道MVC結構中,C層是負責協調和排程M和V層的一個非常關鍵的角色。而C和M以及V之間的互動協調方式用的最多的也是通過Delegate這種模式,Delegate這種模式並不侷限在M和C之間,同樣也可以應用在V和C之間。Delegate的本質其實是一種雙方之間通訊的介面,而通過介面來進行通訊則可以最大限度的減少物件之間互動的耦合性。 下面就是Delegate介面通訊的經典框架圖:

Delegate介面通訊經典框架圖

Block非同步通知方式

除了用Delegate外,我們還可以用Block回撥這種方式來實現方法呼叫的非同步通知處理。標準格式如下:

    typedef void (^BlockHandler)(id obj, NSError * error);

    返回值 方法名:(引數型別)引數1 ...  其他引數...  回撥:(BlockHandler)回撥
複製程式碼

這種方式可以表示為呼叫了某個方法並指定一個block回撥來處理方法的非同步返回。採用block方式定義非同步方法時一般要符合如下幾個規則:

  1. BlockHandler的引數確保就是固定的2個:一個是非同步方法返回的物件,這個物件可以根據不同的方法而返回不同的物件。一個是NSError物件表示非同步訪問發生了錯誤的返回。

  2. 將block回撥處理作為方法的最後一個引數。

  3. 不建議在一個方法中出現2個block回撥:一個正確的和一個失敗的。比如如下方式:

typedef void (^ SuccessfulBlockHandler)(id obj);
typedef void (^ FailedBlockHandler)(NSError *error)

返回值 方法名:(引數型別)引數1 ...  其他引數...  成功回撥:(SuccessfulBlockHandler)成功回撥  失敗回撥:(FailedBlockHandler)失敗回撥
複製程式碼

如果實現2個block來分別對成功和失敗處理有可能會使得程式碼增多和不必要的冗餘程式碼出現。比如:


-(void)ClickHandle:(UIButton*)sender
{
      sender.userInteractionEnabled = NO;
       __weak XXXVC  *weakSelf = self;

      [user login:@"jack"  
       successful:^(id obj){
         if (weakSelf == nil)
            return;

        sender.userInteractionEnabled = YES;

        //處理成功邏輯
      }
          failed:^(NSError *error){
     
            //這裡無可避免要新增重複程式碼。
           if (weakSelf == nil)
              return;
            sender.userInteractionEnabled = YES;

           //處理失敗邏輯。
      }];

複製程式碼

CoreLocation.framework中的地標解析器類CLGeocoder採用的就是block回撥這種方式來實現非同步通知的。我們來看看類的部分定義:

// geocoding handler, CLPlacemarks are provided in order of most confident to least confident
typedef void (^CLGeocodeCompletionHandler)(NSArray< CLPlacemark *> * __nullable placemarks, NSError * __nullable error);

@interface CLGeocoder : NSObject

// reverse geocode requests
- (void)reverseGeocodeLocation:(CLLocation *)location completionHandler:(CLGeocodeCompletionHandler)completionHandler;

@end
複製程式碼

上面的方法可以看出,當需要從一個CLLocation位置物件解析得到一個CLPlacemark地標物件時,需要建立一個CLGeocoder地標解析器物件,然後呼叫對應的reverseGeocodeLocation方法並指定一個block物件來處理這種非同步返回通知。具體程式碼如下:


   //VC中的某個點選按鈕事件:

-(void)ClickHandle:(UIButton*)sender
{
      sender.userInteractionEnabled = NO;
       __weak XXXVC  *weakSelf = self;
    
      //geocoder也可以是XXXVC裡面的一個屬性,從而可以避免重複建立
      CLGeocoder  *geocoder = [CLGeocoder new];
  
      //假設知道了某個位置物件location
      [geocoder  reverseGeocodeLocation:location 
                      completionHandler:^(NSArray< CLPlacemark *> * placemarks, NSError * error)){
      
          if (weakSelf == nil)
               return;
          sender.userInteractionEnabled = YES;
         if (error == nil)
         {
             //處理placemarks物件
         }
         else
        {
            //處理錯誤
        }
     }];  
}
複製程式碼

對於這種在M層物件中某個請求通過block回撥來通知呼叫者進行非同步更新的機制是我比較推崇的一個機制。一個原則是隻要涉及到M層物件的方法呼叫都儘可能的走標準block回撥這種方式。比如我下面定義的某個類裡面有很多方法:

    @interface  ModelClass
         -(void)fn1:(引數型別)引數  callback:(BlockHandler)callback;
         -(void)fn2:(引數型別)引數  callback:(BlockHandler)callback;
         -(void)fn3:(引數型別)引數  callback:(BlockHandler)callback;
       ...
    @end
複製程式碼

上面的方法實現和呼叫機制看起來都很統一,而且是標準化的。這樣給使用者非常的易懂和明確的感覺。 這裡你有可能會問,如果某個方法並沒有任何非同步動作我是否也要遵循這種模式呢?

我的答案是:儘可能的遵循統一模式。因為有可能這個方法某天會從同步實現為非同步實現。這樣當方法由同步實現為非同步時我們就需要改動C層的程式碼,同時還要改動M的方法的定義比如:

原來不帶block機制並且fn是同步的實現:

     //C層的呼叫
      XXXX *mObj = [XXXX new];
      id retObj = [mObj  fn];
      //處理retObj物件

       .....
      //M層類XXXX的實現

    @implementation XXXX
     -(id)fn{  
         //比如這裡面只是訪問本地快取檔案,不進行網路請求和非同步呼叫
         return  某個物件;
       } 
    @end
複製程式碼

一旦需求有變化fn需要由原來的讀取本地快取,改為請求網路並非同步呼叫。那麼你的C層就必須需要重新改寫程式碼:

     XXXX *mObj = [XXXX new];
     [mObj  fn:^(id retObj, NSError *error){
       // 處理retObj物件。
      }];


    .............
   //同時你的M層的XXXX也必須要重新改寫:
    @implementation XXXX
       -(void)fn:(BlockHandler)callback
        {  
         //請求網路。並在網路返回後非同步呼叫callback(retObj, error);
        } 
    @end
複製程式碼

而如果我們開始就設計為標準block方式呢?

@implementation XXXX
   -(void)fn:(BlockHandler)callback
    {  
           //讀取檔案得到retObj
           callback(retObj, nill);      //這裡面就直接呼叫callback方法即可
     } 
    

 ..............................  
//VC呼叫的方式:
 XXXX *mObj = [XXXX new];
 [mObj  fn:^(id retObj, NSError *error){
       // 處理retObj物件。
 }];
複製程式碼

上面可以看出一旦fn的處理需要改變為走網路請求時你就會發現,只需要調整XXXX的fn的實現機制即可,而VC控制器中的方法保持不變。這樣是不是就達到一種非常好的效果呢?

最後我想說一句的是:到底是否要將M層物件的所有方法都改為非同步並加block這種機制並不是絕對的,這個需要根據你的業務場景,以及各種情況來具體處理。

Block非同步通知和Delegate非同步通知的比較

通過上面介紹我們可以看到蘋果的核心定位庫分別採用了2種方法來實現非同步通知。那麼這兩種有什麼優劣以及差異呢?我們又應該在哪種情況下選用哪種方式呢?這裡可以歸納幾點供大家參考:

  • 如果某個類中具有多個方法,而每個方法又實現了不同的功能,並且方法的非同步返回的資料和這個方法具有很強的關聯性那麼就應該考慮使用block而不用Delegate。

  • 如果類中的方法的非同步方法是那種一次互動就得到一個不同的結果,而且得到的結果和上一次結果沒有什麼關聯。通俗的講就是一錘子買賣的話,那麼就應該考慮使用block而不用Delegate。

  • 如果我們呼叫類中的某個方法,而呼叫前我們設定了一些上下文,而呼叫方法後我們又希望根據這個上下文來處理非同步返回的結果時,那麼就應該考慮使用block而不是Delegate。

  • 如果我們呼叫類裡面的某個方法,而返回的結果不需要和上下文進行關聯那麼就考慮使用Delegate而不用block。

  • 如果要實時的觀察業務類裡面的某個屬性的變化時,我們就應該考慮使用Delegate而不是使用block。

  • 如果業務類裡面的非同步通知可能分為好幾個步驟那麼就考慮使用Delegate而不是使用block。

KVO非同步通知方式

上面介紹了可以通過使用Delegate和block機制來實現業務邏輯的更新監聽以及方法的返回的通知處理。這兩種模式其本質上還是一種觀察者機制。根據任何事物都有兩面性的原則來說,用Delegate和block也是具有一些缺點:

  • Delegate的方式必須要事先定義出一個介面協議來,並且呼叫者和實現者都需要按照這個介面規則來進行通知和資料處理互動,這樣無形中就產生了一定的耦合性。也就是二者之間還是具有隱式的依賴形式。不利於擴充套件和進行完全自定義處理。

  • block方式的缺點則是使用不好則會產生迴圈引用的問題從而產生記憶體洩露,另外就是用block機制在出錯後難以除錯以及難以進行問題跟蹤。 而且block機制其實也是需要在呼叫者和實現之間預先定義一個標準的BlockHandler介面來進行互動和處理。block機制還有一個缺陷是會在程式碼中產生多重巢狀,從而影響程式碼的美觀和可讀性。

  • Delegate和block方式雖然都是一種觀察者實現,但卻不是標準和經典的觀察者模式。因為這兩種模式是無法實現多觀察者的。也就是說當資料更新而進行通知時,只能有一個觀察者進行監聽和處理,不能實現多個觀察者的通知更新處理。

那麼如果我們需要實現變化時讓多個觀察者都能接收並處理呢?答案就是使用KVO或者下面說到的Notification機制。這裡我們先說KVO機制。

KVO機制其實也是一種可用於業務呼叫的通知更新處理機制。這種機制的好處是業務物件和觀察者之間已經完全脫離了耦合性,而且資料變化後的通知完全由系統來處理,不需要新增附加的程式碼和邏輯,而且還可以實現多觀察者來同時監聽一份資料的變化:

經典觀察者模式

很可惜目前iOS的定位庫不支援KVO這種方式,下面的介紹只是設想假如定位庫支援KVO的話應該如何處理的場景。 還是以iOS的定位庫為例。如果在實踐中多個VC頁面都需要對位置的變化進行監聽處理。那麼一個方法是我們在每個VC頁面都建立一個CLLocationManager位置管理物件,然後實現對應的CLLocationManagerDelegate協議,然後呼叫startUpdatingLocation進行監聽,並在CLLocationManagerDelegate協議的對應方法didUpdateLocations中對位置更新的資料進行處理。很明顯這裡存在的一個問題就是我們需要建立多個CLLocationManager物件,並且呼叫多次startUpdatingLocation。雖然我們不知道CLLocationManager的實現如何但是總是感覺這種多次呼叫的機制不是最優的解決方案。我們可以改為建立一個單例的CLLocationManager物件,並在適當的位置比如AppDelegate中的didFinishLaunchingWithOptions裡面建立這個單例物件並且呼叫startUpdatingLocation方法進行監聽。在需要處理實時更新通知的VC頁面裡面通過KVO的方式來監聽單例CLLocationManager物件的location屬性呢。這樣只要進入某個需要監聽的頁面時就通過KVO的方式來監聽這個屬性,而退出頁面時則取消監聽。從而可以完全實現了多觀察者這種方式了,這種方式將不再需要定義和實現delegate協議了。具體程式碼如下:

//再次申明的是CCLocationManager是不支援KVO來監聽位置變化的,這裡只是一個假設支援的話的使用方法。

@interface AppDelegate
    @property(nonatomic, strong)  CLLocationManager *locationManager;
@end

@implementation  AppDelegate

   - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        self.locationManager = [CLLocationManager new];
        [self.locationManager  startUpdatingLocation];  //開始監聽位置變化
    return YES;
}
@end


//第一個頁面
@implementation  VC1

-(void)viewWillAppear:(BOOL)animated
{
     [  [UIApplication sharedApplication].delegate.locationManager  addObserver:self  forKeyPath:@"location" options:NSKeyValueObservingOptionNew context:NULL];
}


-(void)viewWillDisappear:(BOOL)animated
{
      [ [UIApplication sharedApplication].delegate.locationManager  removeObserver:self  forKeyPath:@"location" ];

}

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context
{
     //這裡處理位置變化時的邏輯。
}
@end


//第二個頁面
@implementation  VC2

-(void)viewWillAppear:(BOOL)animated
{
     [  [UIApplication sharedApplication].delegate.locationManager  addObserver:self  forKeyPath:@"location" options:NSKeyValueObservingOptionNew context:NULL];
}


-(void)viewWillDisappear:(BOOL)animated
{
      [ [UIApplication sharedApplication].delegate.locationManager  removeObserver:self  forKeyPath:@"location" ];

}

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context
{
     //這裡處理位置變化時的邏輯。
}
@end

//.. 其他頁面

複製程式碼

那麼什麼場景下我們用KVO這種方式來實現非同步通知回撥呢?下面是幾個總結供大家參考:

  1. 某個物件的同一資料更新可能會引起多個依賴這個物件的物件的更新變化處理。

  2. 如果某個物件的生命週期要比觀察者短則不建議用KVO方式,因為這個有可能會導致系統的崩潰而造成巨大的影響。

  3. 某個物件的某種屬性具有多種狀態,不同的頁面在不同狀態下的處理邏輯和展現會有差異,而物件的狀態是在不停的變化的。這是一個很常見的狀態機應用場景。比如一個訂單的狀態會不停的變化,一個使用者的登入狀態會不停的變化。很多人在這種具有狀態機屬性的實現中,都會在進入頁面後構建一個物件,然後再從伺服器中呼叫對應的狀態獲取的方法,然後再根據當前的狀態來進行不同的處理。就以一個訂單為例:假如我們的應用邏輯裡面一次只能處理一個訂單,而這個訂單又會被不同的頁面訪問,每個頁面都需要根據訂單的當前狀態進行不同的處理。下面一個例子:

不用KVO且多副本模式

上面的圖形中我們可以看出同一個訂單物件在不同的頁面之間產生了副本,這樣狀態也就產生了副本。當副本增多時那麼我們就需要一種機制來統一更新這些副本中的狀態屬性,並且根據最新的狀態來處理這種變化。很明顯因為副本的增多造成維護的困難(資料的不一致性)。那麼如何來解決這個問題呢?既然剛才我們的業務場景是一定的時間只能有一個訂單,那麼我們就應該將這個訂單物件改為只有單一存在的模式。我們可以在頁面之間互相傳遞這個訂單物件,也可以將這個訂單物件設計為單例模式。然後我們再通過KVO的機制來實現當狀態變化時所有需要依賴狀態的頁面都進行處理。

單副本並且通過KVO來實現狀態的監聽並更新

Notification非同步通知方式

KVO模式實現了一種對屬性變化的通知觀察機制。而且這種機制由系統來完成,缺點就是他只是對屬性的變化進行觀察,而不能對某些非同步方法呼叫進行通知處理。而如果我們想要正真的實現觀察者模式而不侷限於屬性呢?答案就是iOS的NSNotificationCenter。也就是說除了用Delegate,Block 這兩種方式來對非同步方法進行通知回撥外,我們還可以用NSNotificationCenter方式來進行通知回撥,並且這種機制是可以實現同時具備多個觀察者的應用場景的。

既然通知這種機制那麼好,那麼為什麼不主動推薦呢?答案是這種機制太過於鬆散了。雖然他解決了多觀察者的問題,但是過於鬆散的結果是給使用者帶來了一定的學習成本。我們知道當通過Delegate或者block時來設計業務層方法的回撥時,可以很清楚的知道業務呼叫方法和實現機制的上下文,因為這些東西在程式碼定義裡面就已經固話了,而在使用這些方法時也很清楚的瞭解應該怎麼使用某個方法,如何去呼叫他最合適。 但是NSNotificationCenter呢?這是完全鬆散而沒有關聯上下文的,我們必須額外的去學習和了解哪些業務層的方法需要新增觀察者哪些不需要,而且程式碼中不管在什麼時候需要都要在初始化時新增一段程式碼上去。通知處理邏輯的可讀寫性以及程式碼的可讀性也比較差。下面是例子程式碼。

@implementation   VC

-(void) viewWillAppear:(BOOL)animated
{
    //這裡必須要預先新增一些觀察者來處理一些不知道上下文的事件
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleA:) name:@"A" object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleB:) name:@"B" object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleC:) name:@"C" object:nil];
    
    
    //這裡注意的是定位庫並不支援通知,這裡只是為了演示
    CLLocationManager *locationManager = [CLLocationManager new];
      self.locationManager = locationManager;
    [locationManager startLocationUpdate];

}

-(void)viewWillDisappear:(BOOL)animated
{
   
       [[NSNotificationCenter defaultCenter] removeObserver:self];
       [self.locationManager  stopLocationUpdate];
}

@end


//這裡因為沒有上下文,所以這個回撥就不是很明確到底是做什麼的。
-(void)handleA:(NSNotification*)noti
{
}
  
-(void)handleB:(NSNotification*)noti
{
}

-(void)handleC:(NSNotification*)noti
{
}

複製程式碼

結束語


上面就是對模型層的設計的方法以及應該遵循的一些規則進行了全面介紹,文章以iOS的定位庫為藍本來進行解構介紹,在設計一個業務層時,首先應該要對業務進行仔細的分析和理解,然後構建出一個類結構圖,這種靜態框架設計好後,就需要對類進行角色和職責劃分,哪些應該設計為資料模型類,哪些應該設計為業務類。然後再設計出一個類裡面應該具有的屬性。最後在設計出類裡面所提供的方法,因為模型層所提供的方法大都具有非同步屬性,因此要選擇一個最合適的非同步呼叫通知模型。當然這些都只是我們在進行業務模型層設計時所做的第一步,那麼我們的業務模型層內部的實現又應該如何進行設計和編碼呢?我將會在後續的日子裡面繼續撰文來介紹如何一個業務模型層的一些具體方法和實踐。敬請期待吧。


最後歡迎大家訪問我的github站點,關注歐陽大哥2013

相關文章