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

這篇文章是論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

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

推薦閱讀更多精彩內容

  • 國家電網公司企業標準(Q/GDW)- 面向對象的用電信息數據交換協議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 11,067評論 6 13
  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結起來就是把...
    Dove_iOS閱讀 27,197評論 30 471
  • 基礎 1. 為什么說Objective-C是一門動態的語言? 2. 講一下MVC和MVVM,MVP? 3. 為...
    波妞和醬豆子閱讀 3,351評論 0 46
  • 設計模式 1.delegate和notification什么區別,什么情況使用? 2.描述一下KVO和KVC。 K...
    丶逐漸閱讀 1,968評論 3 2
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,821評論 18 139