over 4 years ago

連續幾週都是討論Java,但事實上工作一年多來主要使用的語言都是Objective-C。在學時曾因好奇自學Objective-C,但沒有開發過實際的專案,所以沒有體會到Objective-C語言的有趣之處,工作後真正使用Objective-C開發專案,才發現Objective-C是一個很有趣的語言。它是靜態型別編譯式語言,可是所有訊息(呼叫函式)的傳送都是在執行期間才決定而非編譯期間,因此提供很多動態語言才有的特性;它是物件導向語言,有@protocol (UML中的Interface)和@interface (UML中的Class),卻沒有明顯的封裝存取控制,例如今天要討論的主角:protected methods。

會討論這玩意,是因為最近和兩位Objective-C的新人一起pair programming,兩位都問到:怎麼在Objective-C中使用protected methods?對於習慣Java、C++或C#的programmer來說,protected methods是再自然不過的事,但和一開始我不習慣JavaScript一樣,Objective-C並不沒有protected methods (Objective-C雖然有@public@protected@private關鍵字,但只用在修飾成員變數)。這一問,我回頭看自己近幾年的Java專案,我發現我使用protected methods的機會越來越少,可能跟我大量使用interface宣告與實作和減少繼承的深度有關。

Figure 1是一個常見的類別圖,橘色虛線代表一個邊界,虛線內屬於同一個套件或系統,虛線外則是外部系統,首先看套件內,AbstractClass實作SomeInterfaceinterfaceMethod(),然後留下protectedMethodB()這個抽象函式,ConcreteClass繼承AbstractClass並提供protectedMethodB()的實作。接著看外部系統,Context使用ConcreteClass和額外擴充的CustomizedClass。最後,Context實際上能看見的,就只有interfaceMethod()publicMethod()兩個函式。對AbstractClass來說能看到所有的函式,對ConcreteClassCustomizedClass而言,可以看到privateMethod()以外的所有函式。

Figure 1 - A class hierarchy

現在的語言趨勢都是朝向動態語言,因此所謂的可見(Visibility)大多是只在編譯期間的檢查,以Java為例,直接使用private method會出現編譯錯誤,但透過reflection API,呼叫private method是可行的。而Objective-C也是如此,宣告在標頭檔(Header file)中為public methods,直接放在實作檔(Implementation file)中為private methods,但不論是public methods或private methods都能透過Objective-C Runtime所提供的objc_msgSend進行呼叫。所以這裡所討論的模擬,也僅限於讓編譯器檢查時,能夠模擬protected methods的效果

模擬的方法即是使用Extension,將標頭檔分成兩份,一份是Code List 2所示的AbstractClass.h,引入Code List 1所宣告的SomeInterface以及宣告publicMethod(),另一份是Code List 3的AbstractClass_Protected.h,用來宣告 protectedMethodA()protectedMethodB(),接著Code List 4中AbstractClass.m引入AbstractClass_Protected.h後,提供interfaceMethod()publicMethod()protectedMethodA()privateMethod()的實作,Objective-C沒有抽象函式的概念,所以AbstractClass.m是以拋出例外的方式模擬抽象函式。

Code List 1 - The SomeInterface.h
#import <Foundation/Foundation.h>

@protocol SomeInterface <NSObject>

- (void)interfaceMethod;

@end
Code List 2 - The AbstractClass.h
#import <Foundation/Foundation.h>

#import "SomeInterface.h"

@interface AbstractClass : NSObject<SomeInterface>

- (void)publicMethod;

@end
Code List 3 - The AbstractClass_Protected.h
#import "AbstractClass.h"

@interface AbstractClass ()

- (void)protectedMethodA;

- (void)protectedMethodB;

@end
Code List 4 - The AbstractClass.m
#import "AbstractClass.h"

#import "AbstractClass_Protected.h"

@implementation AbstractClass

- (void)interfaceMethod {
    NSLog(@"interface method invoked");
}

- (void)publicMethod {
    NSLog(@"public method invoked");
    [self protectedMethodA];
    [self protectedMethodB];
    [self privateMethod];
}

- (void)protectedMethodA {
    NSLog(@"protected method A invoked");
}

- (void)protectedMethodB {
    [NSException raise:@"AbstractMethodInvokedException" format:@"Abstract method shouldn't be invoked"];
}

- (void)privateMethod {
    NSLog(@"private method invoked");
}

@end

到目前為止,都沒有太大的問題,接著就是繼承的類別能不能看到protectedMethodA()protectedMethoB()了,系統內部的ConcreteClass如Code List 5和Code List 6所示,在引入AbstractClass_Protected.h後,在覆寫的publicMethod()中能夠使用protectedMethodA()protectedMethodB(),並且覆寫protectedMethodB()。似乎解決問題了,但其實只解決了一部分,衍生的問題是AbstractClass_Protected.h要不要開放給外部系統?

Code List 5 - The ConcreteClass.h
#import "AbstractClass.h"

@interface ConcreteClass : AbstractClass

@end
Code List 6 - The ConcreteClass.m
#import "ConcreteClass.h"

#import "AbstractClass_Protected.h"

@implementation ConcreteClass

- (void)publicMethod {
    NSLog(@"overridden protected method A invoked");
    [self protectedMethodA];
    [self protectedMethodB];
}

- (void)protectedMethodB {
    NSLog(@"ConcreteClass protected method B invoked");
}

@end

同樣分標頭檔和實作檔的C++語言,雖然標頭檔會暴露private methods,但編譯器會檢查存取的合法性;而像C#或Java這類沒有標頭檔的語言中,可見度會被編進bytecode中,所以當拿到一個打包過後的DLL檔或JAR檔,還是能知道每個函式的可見度並作檢查。但對Objective-C而言,函式的可見度只依賴能否存取擁有函式宣告的標頭檔,也就是說若開放AbstractClass_Protected.hCustomizedClass能看見protectedMethodAprotectedMethodB,但同樣地,Context也看的到。反之,若不開放,CustomizedClasContext都看不到。

Table 1將用Extension模擬protected methods在開放與不開放的可見度做個比較,o表示可見,x表示不可見,第一欄列出不同的類別,第二欄是普遍預期的可見度(只有Context看不到),第三欄則是開放Extension標頭檔後的可見度(全都看的到),第四欄則是不開放Extension標頭檔的可見度(ContextCustomizedClass看不到)。很可惜,用Extension無法百分之百模擬protected methods,至於開不開放,我想就留給使用的人依據個別情況決定。事實上,開放Extension標頭檔和直接把protected methods宣告在原本標頭檔意義上是差不多的,所以我個人是傾向不開放。

Table 1 - Protected methods visibility comparison

Class Expected Opened Extension Closed Extension
Context x o x
AbstractClass o o o
ConcreteClass o o o
CustomizedClass o o x
← Quick Glance of Java 8 - Parallel Array Sort Protected Methods in Objective C →