什麼時候應該要使用 Category

如果想要擴充某個 class 功能,增加新的成員變數與 method,我們又沒有這個 class 的程式碼,正規作法就是繼承、建立新的subclass。那,我們需要在不用繼承,就直接增加 method這種作法的重要理由,就是我們想要擴充的 class 很難繼承。

我能想到的,大概有幾種狀況:

  1. Foundation 物件
  2. 用 Factory Method Pattern 實作的物件
  3. Singleton 物件
  4. 在專案中出現次數已經多不勝數 的物件。

Foundation 物件

Foundation 裡頭的基本物件,像是 NSString、NSArray、NSDictionary 等 Class 的底層實作,除了可以透過 Objective-C的介面呼叫之外,也可以透過另外一個 C 的介面,叫做 Core Foundation,像NSString 其實會對應到 Core Foundation 裡頭的 CFStringRef,NSArray 對應到 CFArrayRef,而你甚至可以直接把 Foundation 物件 cast成 Core Foundation 的型別,當你遇到一個需要傳入 CFStringRef 的function的時候,只要建立 NSString 然後 cast 成 CFStringRef 傳入就可以了。

所以,當你使用 alloc、init 產生一個 Foundation物件的時候,其實會得到一個同時有 Foundation 與 Core Foundation 實作的subclass,而實際產生出來的物件,往往與你的認知會有很大的差距,例如,我們以為 NSMutableString 繼承自 NSString,但建立 NSString ,呼叫alloc、init 的時候,我們真正拿到的是 __NSCFConstantString,而建立 NSMutableString ,拿到 __NSCFString,而 __NSCFConstantString 其實繼承自 __NSCFString!

我們來寫點程式檢查 Foundation 物件其實屬於哪些 Class:

#define CLS(x) NSStringFromClass([x class])
NSLog(@"NSString:%@", CLS([NSString string]));
NSLog(@"NSMutableString:%@", CLS([NSMutableString string]));
NSLog(@"NSNumber:%@", CLS([NSNumber numberWithInt:1]));
#undef CLS

執行結果: :

NSString:__NSCFConstantString
NSMutableString:__NSCFString
NSNumber:__NSCFNumber

因此,當我們嘗試建立 Foundation 物件的 subclass 之後,像是繼承 NSString,建立我們自己的 MyString,假如果我們並沒有 override 原本關於建立 instance 的 method,我們也不能保證,建立出來的就是 MyString 的 instance。

用 Factory Method Pattern 實作的物件

Wikipedia 上對 Factory Method Pattern 的解釋是:

...the factory method pattern is a creational pattern which uses factory methods to deal with the problem of creating objects without specifying the exact class of object that will be created.

翻譯成中文:Factory Method Pattern 是一套用來解決不用特別指定是哪個 class,就可以建立物件的方法。比方說,某個 class底下,其實有一堆 subclass,但對外部來說並不需要確實知道這些 subclass而是只要 對最上層的class,輸入指定的條件,就會從挑選一個符合指定條件的 subclass、建立 instance 回傳

在 UIKit 中,UIButton 就是個好例子。在某些版本的 iOS 當中,在我們在建立 UIButton 物件的時候,並不是呼叫 init 或是 initWithFrame:,而是呼叫 UIButton 的 class method:buttonWithType:,透過傳遞按鈕的 type建立按鈕物件。在大多數狀況下,會回傳 UIButton 物件,但假如我們傳入的type 是UIButtonTypeRoundedRect,卻會回傳繼承自 UIButton 的UIRoundedRectButton

檢查一下:

#define CLS(x) NSStringFromClass([x class])
NSLog(@"UIButtonTypeCustom %@",
    CLS([UIButton buttonWithType:UIButtonTypeCustom]));
NSLog(@"UsIButtonTypeRoundedRect %@",
    CLS([UIButton buttonWithType:UIButtonTypeRoundedRect]));
#undef CLS

輸出結果: :

UIButtonTypeCustom UIButton
UIButtonTypeRoundedRect UIRoundedRectButton

我們想要擴充 UIButton,但拿到的卻是 UIRoundedRectButton,而 UIRoundedRectButton 卻無法繼承,因為這個物件不在公開的 header中,我們也不能夠保證以後傳入 UIButtonTypeRoundedRect 就一定會拿到 UIRoundedRectButton。如此一來,就造成我們難以繼承 UIButton

或這麼說:假使今天我們的需求是想要改動某個上層的 class,讓底下所有的 subclass 也都增加了一個新的 method,我們又無法改動這個上層 class的程式,就會採用 category。比方說,我們今天希望所有的 UIViewController都有一個新 method,如此我們整個應用程式中每個 UIViewController 的subclass 都可以呼叫這個 method,但,我們就是無法改動UIViewController

Singleton 物件

Singleton 物件是指: 某個 class 只有、也只該有一個instance,每次都只對這個 instance 操作,而不是建立新的 instance 。像UIApplication、 NSUserDefault、NSNotificationCenter 以及 Mac OS X上的 NSWorkSpace 等,都採用 singleton 設計。

之所以說 singleton 物件很難繼承,我們先來看怎麼實作singleton:我們會有一個 static的物件,然後每次都回傳這個物件。宣告部分如下:

@interface MyClass : NSObject
+ (MyClass *)sharedInstance;
@end

實作部分:

static MyClass *sharedInstance = nil;

@implementation MyClass
+ (MyClass *)sharedInstance
{
    return sharedInstance ?
           sharedInstance :
           (sharedInstance = [[MyClass alloc] init]);
}
@end

其實現在 Singleton 大多會使用 GCD 的 dispatch_once 實作,但是在我們還沒有提到 GCD 之前,我們先使用這樣的寫法。我們會在討論 Threading 的時候繼續討論 GCD,至於用 GCD 實作 Singleton 的細節,請參見 再談 Singleton 這一章。

我們如果 subclass 了 MyClass,卻沒有 override 掉sharedInstance,那麼, sharedInstance 回傳的還是 MyClass 的 singleton instance。而想要 override 掉 sharedInstance 又不見得這麼簡單,因為這個method 裡頭很可能又做了許多其他事情,很可能會把一些 initiailize時該做的事情,反而放在這邊做(這不是很好的作法,但就是可能發生)。例如MyClass 可能這麼寫:

+ (MyClass *)sharedInstance
{
    if (!sharedInstance) {
        sharedInstance = [[MyClass alloc] init];
        [sharedInstance doSomething];
        [sharedInstance doAnotherThine];
    }
    return sharedInstance;
}

如果我們並沒有 MyClass 的程式碼,這個 class 是在其他的 library 或是 framework 中,我們直接 override 了sharedInstance,就很有可能有事情沒做,而產生不符合預期的結果。

在專案中出現次數已經多不勝數

隨著專案不斷成長,某些 class已經頻繁使用到了到處都是,而我們現在需求改變,必須要增加新的method,我們卻也沒有力氣可以把所有用到的地方統統換成新的subclass。Category 就是解決這種狀況的救星。

results matching ""

    No results matching ""