#裝飾原則(Decorator pattern)
##簡介
Decorator裝飾模式是一種結構型模式,它主要是解決:“過度地使用了繼承來擴展對象的功能”,由于繼承為類型引入的靜態特質,使得這種擴展方式缺乏靈活性;并且隨著子類的增多(擴展功能的增多),各種子類的組合(擴展功能的組合)會導致更多子類的膨脹(多繼承)。繼承為類型引入的靜態特質的意思是說以繼承的方式使某一類型要獲得功能是在編譯時。所謂靜態,是指在編譯時;動態,是指在運行時。
修飾模式,是面向對象編程領域中,一種動態地往一個類中添加新的行為的設計模式。就功能而言,修飾模式相比生成子類更為靈活,這樣可以給某個對象而不是整個類添加一些功能。
GoF《設計模式》中說道:動態的給一個對象添加一些額外的職責。就增加功能而言,Decorator模式比生成子類更為靈活。
下面來看看Decorator模式的結構:

看這個結構好像不是很明白,下面我根據代碼講解一下這個結構。我想了一個場景:我們現在用的手機功能很多,我就用Decorator模式實現一下對某個手機的GSP和藍牙功能擴展。
首先,我們需要一個手機的接口或者是抽象類,我這里就用抽象類來實現,代碼如下:
```
public abstract class AbstractCellPhone
{
public abstract string CallNumber();
public abstract string SendMessage();
}
```
AbstractCellPhone也就是結構圖中的Component,然后,我再來實現Nokia和Moto的手機類,這類要繼承AbstractCellPhone,也就是圖中ConcreteComponent類要繼承Component,實現代碼如下:
```
public class NokiaPhone : AbstractCellPhone
{
public override string CallNumber()
{
return "NokiaPhone call sombody";
}
public override string SendMessage()
{
return "NokiaPhone send a message to somebody";
}
}
public class MotoPhone : AbstractCellPhone
{
public override string CallNumber()
{
return "MotoPhone call sombody";
}
public override string SendMessage()
{
return "MotoPhone send a message to somebody";
}
}
```
接下來我需要一個Decorator接口或者抽象類,實現代碼如下:
```
public abstract class Decorator:AbstractCellPhone
{
AbstractCellPhone _phone;
public Decorator(AbstractCellPhone phone)
{
_phone = phone;
}
public override string CallNumber()
{
return _phone.CallNumber();
}
public override string SendMessage()
{
return _phone.SendMessage();
}
}
```
正如結構圖中,這個Decorator即繼承了AbstractCellPhone,又包含了一個私有的AbstractCellPhone的對象。這樣做的意義是:Decorator類又使用了另外一個Component類。我們可以使用一個或多個Decorator對象來“裝飾”一個Component對象,且裝飾后的對象仍然是一個Component對象。在下來,我要實現GSP和藍牙的功能擴展,它們要繼承自Decorator,代碼如下:
```
public class DecoratorGPS : Decorator
{
public DecoratorGPS(AbstractCellPhone phone)
: base(phone)
{ }
public override string CallNumber()
{
return base.CallNumber() + " with GPS";
}
public override string SendMessage()
{
return base.SendMessage() + " with GPS";
}
}
public class DecoratorBlueTooth : Decorator
{
public DecoratorBlueTooth(AbstractCellPhone phone)
: base(phone)
{ }
public override string CallNumber()
{
return base.CallNumber() + " with BlueTooth";
}
public override string SendMessage()
{
return base.SendMessage() + " with BlueTooth";
}
}
```
最后,用客戶端程序驗證一下:
```
static void Main(string[] args)
{
AbstractCellPhone phone = new NokiaPhone();
Console.WriteLine(phone.CallNumber());
Console.WriteLine(phone.SendMessage());
DecoratorGPS gps = new DecoratorGPS(phone); //add GSP
Console.WriteLine(gps.CallNumber());
Console.WriteLine(gps.SendMessage());
DecoratorBlueTooth bluetooth = new DecoratorBlueTooth(gps); //add GSP and bluetooth
Console.WriteLine(bluetooth.CallNumber());
Console.WriteLine(bluetooth.SendMessage());
Console.Read();
}
```
執行結果:
NokiaPhone call sombody
NokiaPhone send a message to somebody
NokiaPhone call sombody with GPS
NokiaPhone send a message to somebody with GPS
NokiaPhone call sombody with GPS with BlueTooth
NokiaPhone send a message to somebody with GPS with BlueTooth
從執行的結果不難看出擴展功能已被添加。最后再說說Decorator裝飾模式的幾點要點:
1. 通過采用組合、而非繼承的手法,Decorator模式實現了在運行時動態的擴展對象功能的能力,而且可以根據需要擴展多個功能。避免了單獨使用繼承帶來的“靈活性差”和“多子類衍生問題”。
2. Component類在Decorator模式中充當抽象接口的角色,不應該去實現具體的行為。而且Decorator類對于Component類應該透明——換言之Component類無需知道Decorator類,Decorator類是從外部來擴展Component類的功能。
3. Decorator類在接口上表現為is-a Component的繼承關系,即Decorator類繼承了Component類所具有的接口。但在實現上又表現為has-a Component的組合關系,即Decorator類又使用了另外一個Component類。我們可以使用一個或多個Decorator對象來“裝飾”一個Component對象,且裝飾后的對象仍然是一個Component對象。(在這里我想談一下我的理解:當我們實例化一個Component對象后,要給這個對象擴展功能,這時我們把這個Component對象當作參數傳給Decorator的子類的構造函數——也就是擴展方法的功能類。對于引用類型傳參時,實際上只是傳遞對象的地址,這樣,在功能擴展是,操作的應該是同一個對象)
4. Decorator模式并非解決“多子類衍生的多繼承”問題,Decorator模式應用的要點在于解決“主體類在多個方向上的擴展功能”——是為“裝飾”的含義。Decorator是在運行時對功能進行組合。
##實例
###Category
Objective-C 中的 Category 就是對裝飾模式的一種具體實現。它的主要作用是在不改變原有類的前提下,動態地給這個類添加一些方法。在 Objective-C 中的具體體現為:實例(類)方法、屬性和協議。是的,在 Objective-C 中可以用 Category 來實現協議。本文將結合 runtime(我下載的是當前的最新版本 objc4-646.tar.gz) 的源碼來探究它實現的原理。
###使用場景
根據蘋果官方文檔對 Category 的描述,它的使用場景主要有三個:
* 給現有的類添加方法.
* 將一個類的實現拆分成多個獨立的源文件.
* 聲明私有的方法.
其中,第 1 個是最典型的使用場景,應用最廣泛。
注:Category 有一個非常容易誤用的場景,那就是用 Category 來覆寫父類或主類的方法。雖然目前 Objective-C 是允許這么做的,但是這種使用場景是非常不推薦的。使用 Category 來覆寫方法有很多缺點,比如不能覆寫 Category 中的方法、無法調用主類中的原始實現等,且很容易造成無法預估的行為。
我們知道,無論我們有沒有主動引入 Category 的頭文件,Category 中的方法都會被添加進主類中。我們可以通過 - performSelector: 等方式對 Category 中的相應方法進行調用,之所以需要在調用的地方引入 Category 的頭文件,只是為了“照顧”編譯器同學的感受。
下面,我們將結合 runtime 的源碼探究下 Category 的實現原理。打開 runtime 源碼工程,在文件 objc-runtime-new.mm 中找到以下函數:
```
void _read_images(header_info **hList, uint32_t hCount)
{
...
_free_internal(resolvedFutureClasses);
}
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
***
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
BOOL classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
if (cat->classMethods || cat->protocols
/* || cat->classProperties */)
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
// Category discovery MUST BE LAST to avoid potential races
// when other threads call the new category code before
// this thread finishes its fixups.
// +load handled by prepare_load_methods()
...
}
```
從第 27-58 行的關鍵代碼,我們可以知道在這個函數中對 Category 做了如下處理:
* 將 Category 和它的主類(或元類)注冊到哈希表中;
* 如果主類(或元類)已實現,那么重建它的方法列表。
在這里分了兩種情況進行處理:Category 中的實例方法和屬性被整合到主類中;而類方法則被整合到元類中(關于對象、類和元類的更多細節,可以參考博文[Objective-C 對象模型](http://blog.leichunfeng.com/blog/2015/04/25/objective-c-object-model/)。另外,對協議的處理比較特殊,Category 中的協議被同時整合到了主類和元類中。
我們注意到,不管是哪種情況,最終都是通過調用 static void remethodizeClass(Class cls) 函數來重新整理類的數據的。
```
static void remethodizeClass(Class cls)
{
...
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
// Update methods, properties, protocols
attachCategoryMethods(cls, cats, YES);
newproperties = buildPropertyList(nil, cats, isMeta);
if (newproperties) {
newproperties->next = cls->data()->properties;
cls->data()->properties = newproperties;
}
newprotos = buildProtocolList(cats, nil, cls->data()->protocols);
if (cls->data()->protocols && cls->data()->protocols != newprotos) {
_free_internal(cls->data()->protocols);
}
cls->data()->protocols = newprotos;
_free_internal(cats);
}
}
```
這個函數的主要作用是將 Category 中的方法、屬性和協議整合到類(主類或元類)中,更新類的數據字段 ```data()``` 中 ```method_lists```(或 ```method_list```)、```properties``` 和 `protocols` 的值。進一步,我們通過 `attachCategoryMethods` 函數的源碼可以找到真正處理 Category 方法的 `attachMethodLists` 函數:
```
static void
attachMethodLists(Class cls, method_list_t **addedLists, int addedCount,
bool baseMethods, bool methodsFromBundle,
bool flushCaches)
{
...
newLists[newCount++] = mlist;
}
// Copy old methods to the method list array
for (i = 0; i < oldCount; i++) {
newLists[newCount++] = oldLists[i];
}
if (oldLists && oldLists != oldBuf) free(oldLists);
// nil-terminate
newLists[newCount] = nil;
if (newCount > 1) {
assert(newLists != newBuf);
cls->data()->method_lists = newLists;
cls->setInfo(RW_METHOD_ARRAY);
} else {
assert(newLists == newBuf);
cls->data()->method_list = newLists[0];
assert(!(cls->data()->flags & RW_METHOD_ARRAY));
}
}
```
這個函數的代碼量看上去比較多,但是我們并不難理解它的目的。它的主要作用就是將類中的舊有方法和 Category 中新添加的方法整合成一個新的方法列表,并賦值給 method_lists 或 method_list 。通過探究這個處理過程,我們也印證了一個結論,那就是主類中的方法和 Category 中的方法在 runtime 看來并沒有區別,它們是被同等對待的,都保存在主類的方法列表中。
不過,類的方法列表字段有一點特殊,它的結構是聯合體,method_lists 和 method_list 共用同一塊內存地址。當 newCount 的個數大于 1 時,使用 method_lists 來保存 newLists ,并將方法列表的標志位置為 RW_METHOD_ARRAY ,此時類的方法列表字段是 method_list_t 類型的指針數組;否則,使用 method_list 來保存 newLists ,并將方法列表的標志位置空,此時類的方法列表字段是 method_list_t 類型的指針。
```
// class's method list is an array of method lists
#define RW_METHOD_ARRAY (1<<20)
union {
method_list_t **method_lists; // RW_METHOD_ARRAY == 1
method_list_t *method_list; // RW_METHOD_ARRAY == 0
};
```
我們注意到 runtime 對 Category 中方法的處理過程并沒有對 +load 方法進行什么特殊地處理。因此,嚴格意義上講 Category 中的 +load 方法跟普通方法一樣也會對主類中的 +load 方法造成覆蓋,只不過 runtime 在自動調用主類和 Category 中的 +load 方法時是直接使用各自方法的指針進行調用的。所以才會使我們覺得主類和 Category 中的 +load 方法好像互不影響一樣。因此,當我們手動給主類發送 +load 消息時,調用的一直會是分類中的 +load 方法,you should give it a try yourself 。