《Effective Objective-C 2.0》4.協議與分類

第4章 協議與分類

第23條:通過委托與數據源協議進行對象間通信

委托模式(Delegate pattern)

主旨:定義一套接口,某個對象若想接受另一個對象的委托,則需要遵從此接口,以便成為其“委托對象”(delegate)。而這“另一個對象”則可以給其委托對象回傳一些信息,也可以在發生相關事件時通知委托對象。

EOCNetworkFetcherDelegate.h
#import <Foundation/Foundation.h>
@class EOCNetworkFetcher;

// 委托協議名通常是在相關類名后面加上 Delegate。
// 委托模式:對象把應對某個行為的責任委托給另一個類。
@protocol EOCNetworkFetcherDelegate <NSObject>
@optional
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher
        didReceiveData:(NSData *)data;
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher
      didFailWithError:(NSError *)error;
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher
        didUpdateProgressTo:(float)progress;
- (BOOL)networkFetcher:(EOCNetworkFetcher *)fetcher
        shouldFollowRedirectToURL:(NSURL *)url;
@end
EOCNetworkFetcher 類
//  EOCNetworkFetcher.h
#import <Foundation/Foundation.h>
#import "EOCNetworkFetcherDelegate.h"

@interface EOCNetworkFetcher : NSObject
// 使用屬性定義其委托對象
// 使用 weak 關鍵字,避免引用循環
@property (nonatomic, weak) id<EOCNetworkFetcherDelegate> delegate;
@end

#import "EOCNetworkFetcher.h"

//  EOCNetworkFetcher.m
@interface EOCNetworkFetcher () {
    // ?? 使用 bitfield 數據類型
    // 緩存委托對象是否能響應協議中的相關方法
    struct {
        unsigned int didReceiveData            : 1;
        unsigned int didFailWithError          : 1;
        unsigned int didUpdateProgressTo       : 1;
        unsigned int shouldFollowRedirectToURL : 1;
    } _delegateFlags;
}
@end
@implementation EOCNetworkFetcher

- (void)setDelegate:(id<EOCNetworkFetcherDelegate>)delegate {
    _delegate = delegate;
    // ① ??實現緩存功能
    _delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];
    _delegateFlags.didFailWithError = [delegate respondsToSelector:@selector(networkFetcher:didFailWithError:)];
    _delegateFlags.didUpdateProgressTo = [delegate respondsToSelector:@selector(networkFetcher:didUpdateProgressTo:)];
    _delegateFlags.shouldFollowRedirectToURL = [delegate respondsToSelector:@selector(networkFetcher:shouldFollowRedirectToURL:)];
}

- (void)testMethod {
    // data obtained from network
    NSData *data;
    // ??在委托對象上調動可選方法,必須提前使用類型信息查詢方法判斷這個委托對象能否響應相關選擇子
    if ([_delegate respondsToSelector:
            @selector(networkFetcher:didReceiveData:)]) {
        [_delegate networkFetcher:self didReceiveData:data];
    }
    
    float currentProgress = 0.0;
    // ② ??查詢結構體標志
    // 不必每次使用類型信息查詢方法,對需要調用很多次的方法時,值得進行這種優化
    if (_delegateFlags.didUpdateProgressTo) {
        [_delegate networkFetcher:self didUpdateProgressTo:currentProgress];
    }
}

@end
EOCDataModel
//  EOCDataModel.h
#import <Foundation/Foundation.h>

/**
 EOCDataModel 對象是 EOCNetworkFetcher 的委托對象
 */
@interface EOCDataModel : NSObject

@end

//  EOCDataModel.m
#import "EOCDataModel.h"
#import "EOCNetworkFetcherDelegate.h"
#import "EOCNetworkFetcher.h"

// 1.聲明此類遵從委托協議
@interface EOCDataModel () <EOCNetworkFetcherDelegate>
@property (nonatomic, strong) EOCNetworkFetcher *myFetcherA;
@property (nonatomic, strong) EOCNetworkFetcher *myFetcherB;
@end

@implementation EOCDataModel

// 2.實現委托協議中的方法
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher
        didReceiveData:(NSData *)data {
    // ??調用 delegate 中的方法時,總是應該把發起委托的實例也一并傳入方法中,
    // 這樣,delegate 對象在實現相關方法時,就能根據傳入的實例分別執行不同的代碼了。
    if (fetcher == _myFetcherA) {
        // handle data
    }else if (fetcher == _myFetcherB) {
        // handle data
    }
}

- (void)networkFetcher:(EOCNetworkFetcher *)fetcher
      didFailWithError:(NSError *)error {
    // handle error
}

@end

要點

  • 委托模式為對象提供了一套接口,使其可由此將相關事件告知其他對象。
  • 將委托對象應該支持的接口定義成協議,在協議中把可能需要處理的事件定義成方法。
  • 當某對象需要從另外一個對象中獲取數據時,可使用委托模式。在這種情況下,該模式亦稱數據源協議(data source protocal)。
  • 若有必要,可實現含有位段的結構體,將委托對象是否能響應相關協議方法這一信息緩存至其中。

第24條:將類的實現代碼分散到便于管理的數個分類之中

通過 Objective-C 的"分類"(Category)機制,把類代碼按邏輯劃入幾個分區中,這對開發與調試都有好處。

不使用 Category 分類:
#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSSet *friends;

- (instancetype)initWithFirstName:(NSString *)firstName
                      andLastName:(NSString *)lastName;

/** Friendship methods */
- (void)addFirend:(EOCPerson *)person;
- (void)removeFriend:(EOCPerson *)person;
- (BOOL)isFriendsWith:(EOCPerson *)person;

/* Word methods */
- (void)performDaysWork;
- (void)takeVacationFromWork;

/** Play methods */
- (void)goToTheCinema;
- (void)goToSportsGame;

@end
使用 Category 分類:
//  EOCPerson.h
#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSSet *friends;

- (instancetype)initWithFirstName:(NSString *)firstName
                      andLastName:(NSString *)lastName;
@end

@interface EOCPerson (Friendship)
- (void)addFirend:(EOCPerson *)person;
- (void)removeFriend:(EOCPerson *)person;
- (BOOL)isFriendsWith:(EOCPerson *)person;
@end

@interface EOCPerson (Work)
- (void)performDaysWork;
- (void)takeVacationFromWork;
@end

@interface EOCPerson (Play)
- (void)goToTheCinema;
- (void)goToSportsGame;
@end

使用 Category 分類機制:

  • 你可以把整個類都定義在一個接口文件中,并將其代碼寫在一個實現文件里。
  • 隨著分類數量的增加,可以把每個分類提取到各自的文件中去。
    • EOCPerson+Friendship(.h/.m)
    • EOCPerson+Work(.h/.m)
    • EOCPerson+Play(.h/.m)
  • 優點:可以把類代碼分成很多個易于管理的小塊,以便單獨檢視。
  • 優點:便于調試。對于某個分類中的所有方法,分類名稱都會出現在其符號中。可以根據調試器回溯信息中的分類名稱,精確定位到類中的方法所屬的功能區。

要點

  • 使用分類機制把類的實現代碼劃分成易于管理的小塊。
  • 將應該視為私有的方法歸入名叫 Private 的分類中,以隱藏實現細節。以編寫“自我描述式代碼”(self-documenting code)。

第25條:總是為第三方類的分類名稱加前綴

使用分類的問題:

  1. 【將分類方法加入類中】這一操作是在【運行期系統加載分類時】完成的。運行期系統會把分類中所實現的每個方法都加入類的方法列表中。
  2. 如果類中本來就有此方法,而分類又實現了一次,那么分類中的方法會覆蓋原來那一份實現代碼。

解決方法:

  1. 以命名空間來區別各個分類的名稱與其中所定義的方法
  2. 給相關名稱都加上某個共用的前綴。
//  NSString+ABC_HTTP.h
#import <Foundation/Foundation.h>

@interface NSString (ABC_HTTP)

// Encode a string with URL encoding
- (NSString *)abc_urlEncodedString;

// Decode a URL encoded string
- (NSString *)abc_urlDecodedString;

@end

要點

  • 向第三方類中添加分類時,總應給其名稱加上你專用的前綴。
  • 向第三方類中添加分類時,總應給其中的方法名加上你專用的前綴。

第26條:勿在分類中聲明屬性

  • Category 分類無法把實現屬性所需的實例變量合成出來,即無法自動實現存取方法。
  • 關聯對象(參考第10條)能夠解決在分類中不能合成實例變量的問題,但是不推薦。
  • 所有屬性都應該定義在主接口中。
  • Category 分類的作用是擴展類的功能,而非封裝數據。
  • 可以在 Category 分類中使用只讀(readonly)屬性:
//  NSCalendar+EOC_Additions.h
#import <Foundation/Foundation.h>

@interface NSCalendar (EOC_Additions)
@property (nonatomic, strong, readonly) NSArray *eoc_allMonths;

- ()
@end

//  NSCalendar+EOC_Additions.m
#import "NSCalendar+EOC_Additions.h"

@implementation NSCalendar (EOC_Additions)
// ?? readonly,不需要設置 set 方法。
// ?? get方法不會訪問類數據,屬性也不需要由實例變量來實現
- (NSArray *)eoc_allMonths {
    if ([self.calendarIdentifier
            isEqualToString:NSCalendarIdentifierGregorian]) {
        return @[@"January",@"Feburary",
                 @"March",@"April",
                 @"May",@"June",
                 @"July",@"August",
                 @"September",@"October",
                 @"November",@"December",];
    }else if (/** other calendar identifier */) {
        /** return months for other calendars */
    }
}
@end

上例中,直接聲明一個方法或許更好:

#import <Foundation/Foundation.h>

@interface NSCalendar (EOC_Additions)
- (NSArray *)eoc_allMonths;
@end

要點

  • 把封裝數據所用的全部屬性都定義在主接口里。
  • 在 "class-continuation" 分類之外的其他分類中,可以定義存取方法,但盡量不要定義屬性。

第27條:使用 "class-continuation分類" 隱藏實現細節

Objective-C Class Extension

@interface <#class name#> ()

@end
  • 可以將私有實例變量和私有方法聲明在 "class-continuation分類" 中,實現對外隱藏。
  • 將實例變量添加到 "class-continuation分類" 中與添加到 @implementation 實現塊中是等效的。
  • 編寫 Objective-C++ 代碼時使用 "class-continuation分類" 也尤為有用。
  • "class-continuation分類" 還可以將 public 接口中聲明為 readonly 的屬性擴展為 readwrite。【參見:第18條:盡量使用不可變對象】
  • 若對象所遵從的協議(delegate)只應視為私有,則可以在 "class-continuation分類" 中聲明。

要點

  • 通過 "class-continuation分類" 向類中新增實例變量。
  • 如果某屬性在主接口中聲明為"只讀",而類的內部又要用設置方法修改此屬性,那么就在 "class-continuation分類" 中將其擴展為"可讀寫"。
  • 把私有方法的原型聲明在 "class-continuation分類" 里面。
  • 若想使類所遵循的協議不為人所知,則可于 "class-continuation分類" 中聲明。

第28條:通過協議提供匿名對象

  • 將返回的對象設計為遵從協議的純 id 類型。

  • "匿名對象"(anonymous object):

    @property (nonatomic, weak) id<EOCNetworkFetcherDelegate> delegate;
    
  • 有時候對象類型并不重要,重要的是對象有沒有實現某些方法。

示例代碼

以下示例是參考自 objc中國期刊:整潔的 Table View 代碼

讓 Cells 可復用

有時多種 model 對象需要用同一類型的 cell 來表示,這種情況下,我們可以進一步讓 cell 可以復用。首先,我們給 cell 定義一個 protocol,需要用這個 cell 顯示的對象必須遵循這個 protocol。然后簡單修改 category 中的設置方法,讓它可以接受遵循這個 protocol 的任何對象。這些簡單的步驟讓 cell 和任何特殊的 model 對象之間得以解耦,讓它可適應不同的數據類型。

// ****************************************************
//  UITableViewCell+ConfigureModel.h
#import <UIKit/UIKit.h>

@protocol HQLTableViewCellConfigureDelegate <NSObject>
@required
- (NSString *)imageName;
- (NSString *)titleLabelText;
@end

@protocol HQLTableViewCellKeyValueConfigureDelegate <NSObject>
@required
- (NSString *)titleLabelText;
- (NSString *)detailLabelText;
@end

@interface UITableViewCell (ConfigureModel)

/**
 配置查詢功能 Cell

 @param model 模型:圖片 + 標題 + 指示箭頭>
 */
- (void)hql_configureForModel:(id<HQLTableViewCellConfigureDelegate>)model;


/**
 配置數據顯示 Cell

 @param model 模型:titleLabel + detailLabel
 */
- (void)hql_configureForKeyValueModel:(id<HQLTableViewCellKeyValueConfigureDelegate>)model;

@end
  
// ****************************************************
//  UITableViewCell+ConfigureModel.m
#import "UITableViewCell+ConfigureModel.h"

@implementation UITableViewCell (ConfigureModel)

- (void)hql_configureForModel:(id<HQLTableViewCellConfigureDelegate>)model {
    self.imageView.image = [UIImage imageNamed:model.imageName];
    self.textLabel.text  = model.titleLabelText;
    self.accessoryType   = UITableViewCellAccessoryDisclosureIndicator;
}

- (void)hql_configureForKeyValueModel:(id<HQLTableViewCellKeyValueConfigureDelegate>)model {
    self.textLabel.text       = model.titleLabelText;
    self.detailTextLabel.text = model.detailLabelText;
}

@end

要點

  • 協議可在某種程度上提供匿名類型。具體的對象類型可以淡化成遵從某協議的 id 類型,協議里規定了對象所應實現的方法。
  • 使用匿名對象來隱藏類型名稱(或類名)。
  • 如果具體類型不重要,重要的是對象能夠響應(定義在協議里的)特定方法,那么可使用匿名對象來表示。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容