《Effective Objective-C 2.0》4.協(xié)議與分類

第4章 協(xié)議與分類

第23條:通過(guò)委托與數(shù)據(jù)源協(xié)議進(jìn)行對(duì)象間通信

委托模式(Delegate pattern)

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

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

// 委托協(xié)議名通常是在相關(guān)類名后面加上 Delegate。
// 委托模式:對(duì)象把應(yīng)對(duì)某個(gè)行為的責(zé)任委托給另一個(gè)類。
@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
// 使用屬性定義其委托對(duì)象
// 使用 weak 關(guān)鍵字,避免引用循環(huán)
@property (nonatomic, weak) id<EOCNetworkFetcherDelegate> delegate;
@end

#import "EOCNetworkFetcher.h"

//  EOCNetworkFetcher.m
@interface EOCNetworkFetcher () {
    // ?? 使用 bitfield 數(shù)據(jù)類型
    // 緩存委托對(duì)象是否能響應(yīng)協(xié)議中的相關(guān)方法
    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;
    // ① ??實(shí)現(xiàn)緩存功能
    _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;
    // ??在委托對(duì)象上調(diào)動(dòng)可選方法,必須提前使用類型信息查詢方法判斷這個(gè)委托對(duì)象能否響應(yīng)相關(guān)選擇子
    if ([_delegate respondsToSelector:
            @selector(networkFetcher:didReceiveData:)]) {
        [_delegate networkFetcher:self didReceiveData:data];
    }
    
    float currentProgress = 0.0;
    // ② ??查詢結(jié)構(gòu)體標(biāo)志
    // 不必每次使用類型信息查詢方法,對(duì)需要調(diào)用很多次的方法時(shí),值得進(jìn)行這種優(yōu)化
    if (_delegateFlags.didUpdateProgressTo) {
        [_delegate networkFetcher:self didUpdateProgressTo:currentProgress];
    }
}

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

/**
 EOCDataModel 對(duì)象是 EOCNetworkFetcher 的委托對(duì)象
 */
@interface EOCDataModel : NSObject

@end

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

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

@implementation EOCDataModel

// 2.實(shí)現(xiàn)委托協(xié)議中的方法
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher
        didReceiveData:(NSData *)data {
    // ??調(diào)用 delegate 中的方法時(shí),總是應(yīng)該把發(fā)起委托的實(shí)例也一并傳入方法中,
    // 這樣,delegate 對(duì)象在實(shí)現(xiàn)相關(guān)方法時(shí),就能根據(jù)傳入的實(shí)例分別執(zhí)行不同的代碼了。
    if (fetcher == _myFetcherA) {
        // handle data
    }else if (fetcher == _myFetcherB) {
        // handle data
    }
}

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

@end

要點(diǎn)

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

第24條:將類的實(shí)現(xiàn)代碼分散到便于管理的數(shù)個(gè)分類之中

通過(guò) Objective-C 的"分類"(Category)機(jī)制,把類代碼按邏輯劃入幾個(gè)分區(qū)中,這對(duì)開(kāi)發(fā)與調(diào)試都有好處。

不使用 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 分類機(jī)制:

  • 你可以把整個(gè)類都定義在一個(gè)接口文件中,并將其代碼寫在一個(gè)實(shí)現(xiàn)文件里。
  • 隨著分類數(shù)量的增加,可以把每個(gè)分類提取到各自的文件中去。
    • EOCPerson+Friendship(.h/.m)
    • EOCPerson+Work(.h/.m)
    • EOCPerson+Play(.h/.m)
  • 優(yōu)點(diǎn):可以把類代碼分成很多個(gè)易于管理的小塊,以便單獨(dú)檢視。
  • 優(yōu)點(diǎn):便于調(diào)試。對(duì)于某個(gè)分類中的所有方法,分類名稱都會(huì)出現(xiàn)在其符號(hào)中??梢愿鶕?jù)調(diào)試器回溯信息中的分類名稱,精確定位到類中的方法所屬的功能區(qū)。

要點(diǎn)

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

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

使用分類的問(wèn)題:

  1. 【將分類方法加入類中】這一操作是在【運(yùn)行期系統(tǒng)加載分類時(shí)】完成的。運(yùn)行期系統(tǒng)會(huì)把分類中所實(shí)現(xiàn)的每個(gè)方法都加入類的方法列表中。
  2. 如果類中本來(lái)就有此方法,而分類又實(shí)現(xiàn)了一次,那么分類中的方法會(huì)覆蓋原來(lái)那一份實(shí)現(xiàn)代碼。

解決方法:

  1. 以命名空間來(lái)區(qū)別各個(gè)分類的名稱與其中所定義的方法。
  2. 給相關(guān)名稱都加上某個(gè)共用的前綴。
//  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

要點(diǎn)

  • 向第三方類中添加分類時(shí),總應(yīng)給其名稱加上你專用的前綴。
  • 向第三方類中添加分類時(shí),總應(yīng)給其中的方法名加上你專用的前綴。

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

  • Category 分類無(wú)法把實(shí)現(xiàn)屬性所需的實(shí)例變量合成出來(lái),即無(wú)法自動(dòng)實(shí)現(xiàn)存取方法。
  • 關(guān)聯(lián)對(duì)象(參考第10條)能夠解決在分類中不能合成實(shí)例變量的問(wèn)題,但是不推薦。
  • 所有屬性都應(yīng)該定義在主接口中。
  • Category 分類的作用是擴(kuò)展類的功能,而非封裝數(shù)據(jù)。
  • 可以在 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,不需要設(shè)置 set 方法。
// ?? get方法不會(huì)訪問(wèn)類數(shù)據(jù),屬性也不需要由實(shí)例變量來(lái)實(shí)現(xiàn)
- (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

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

#import <Foundation/Foundation.h>

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

要點(diǎn)

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

第27條:使用 "class-continuation分類" 隱藏實(shí)現(xiàn)細(xì)節(jié)

Objective-C Class Extension

@interface <#class name#> ()

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

要點(diǎn)

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

第28條:通過(guò)協(xié)議提供匿名對(duì)象

  • 將返回的對(duì)象設(shè)計(jì)為遵從協(xié)議的純 id 類型。

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

    @property (nonatomic, weak) id<EOCNetworkFetcherDelegate> delegate;
    
  • 有時(shí)候?qū)ο箢愋筒⒉恢匾匾氖菍?duì)象有沒(méi)有實(shí)現(xiàn)某些方法。

示例代碼

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

讓 Cells 可復(fù)用

有時(shí)多種 model 對(duì)象需要用同一類型的 cell 來(lái)表示,這種情況下,我們可以進(jìn)一步讓 cell 可以復(fù)用。首先,我們給 cell 定義一個(gè) protocol,需要用這個(gè) cell 顯示的對(duì)象必須遵循這個(gè) protocol。然后簡(jiǎn)單修改 category 中的設(shè)置方法,讓它可以接受遵循這個(gè) protocol 的任何對(duì)象。這些簡(jiǎn)單的步驟讓 cell 和任何特殊的 model 對(duì)象之間得以解耦,讓它可適應(yīng)不同的數(shù)據(jù)類型。

// ****************************************************
//  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 模型:圖片 + 標(biāo)題 + 指示箭頭>
 */
- (void)hql_configureForModel:(id<HQLTableViewCellConfigureDelegate>)model;


/**
 配置數(shù)據(jù)顯示 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

要點(diǎn)

  • 協(xié)議可在某種程度上提供匿名類型。具體的對(duì)象類型可以淡化成遵從某協(xié)議的 id 類型,協(xié)議里規(guī)定了對(duì)象所應(yīng)實(shí)現(xiàn)的方法。
  • 使用匿名對(duì)象來(lái)隱藏類型名稱(或類名)。
  • 如果具體類型不重要,重要的是對(duì)象能夠響應(yīng)(定義在協(xié)議里的)特定方法,那么可使用匿名對(duì)象來(lái)表示。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容