——.NET設(shè)計(jì)模式系列之十
Terrylee
,
2006
年
3
月
概述
在軟件系統(tǒng)中,有時(shí)候我們會(huì)使用繼承來(lái)擴(kuò)展對(duì)象的功能,但是由于繼承為類型引入的靜態(tài)特質(zhì),使得這種擴(kuò)展方式缺乏靈活性;并且隨著子類的增多(擴(kuò)展功能的增多),各種子類的組合(擴(kuò)展功能的組合)會(huì)導(dǎo)致更多子類的膨脹。如何使“對(duì)象功能的擴(kuò)展”能夠根據(jù)需要來(lái)動(dòng)態(tài)地實(shí)現(xiàn)?同時(shí)避免“擴(kuò)展功能的增多”帶來(lái)的子類膨脹問(wèn)題?從而使得任何“功能擴(kuò)展變化”所導(dǎo)致的影響將為最低?這就是本文要講的
Decorator
模式。
意圖
動(dòng)態(tài)地給一個(gè)對(duì)象添加一些額外的職責(zé)。就增加功能來(lái)說(shuō),
Decorator
模式相比生成子類更為靈活。
[GOF
《設(shè)計(jì)模式》
]
結(jié)構(gòu)圖
圖
1 Decorator
模式結(jié)構(gòu)圖
生活中的例子
裝飾模式動(dòng)態(tài)地給一個(gè)對(duì)象添加額外的職責(zé)。不論一幅畫有沒(méi)有畫框都可以掛在墻上,但是通常都是有畫框的,并且實(shí)際上是畫框被掛在墻上。在掛在墻上之前,畫可以被蒙上玻璃,裝到框子里;這時(shí)畫、玻璃和畫框形成了一個(gè)物體。
圖
2
使用有畫框的畫作為例子的裝飾模式對(duì)象圖
裝飾模式解說(shuō)
在軟件開(kāi)發(fā)中,經(jīng)常會(huì)遇到動(dòng)態(tài)地為一個(gè)對(duì)象而不是整個(gè)類增加一些功能的問(wèn)題,還是以我慣用的記錄日志的例子來(lái)說(shuō)明吧(也許在
Decorator
模式里面用這個(gè)例子不是特別合適)。現(xiàn)在要求我們開(kāi)發(fā)的記錄日志的組件,除了要支持?jǐn)?shù)據(jù)庫(kù)記錄
DatabaseLog
和文本文件記錄
TextFileLog
兩種方式外,我們還需要在不同的應(yīng)用環(huán)境中增加一些額外的功能,比如需要記錄日志信息的錯(cuò)誤嚴(yán)重級(jí)別,需要記錄日志信息的優(yōu)先級(jí)別,還有日志信息的擴(kuò)展屬性等功能。在這里,如果我們不去考慮設(shè)計(jì)模式,解決問(wèn)題的方法其實(shí)很簡(jiǎn)單,可以通過(guò)繼承機(jī)制去實(shí)現(xiàn),日志類結(jié)構(gòu)圖如下:
圖
3
實(shí)現(xiàn)代碼如下:
public
abstract
class
Log
{
public
abstract
void
Write(
string
log);
}
public
class
DatabaseLog
:
Log
{
public
override
void
Write(
string
log)
{
//......
記錄到數(shù)據(jù)庫(kù)中
}
}
public
class
TextFileLog
:
Log
{
public
override
void
Write(
string
log)
{
//......
記錄到文本文件中
}
}
需要記錄日志信息的錯(cuò)誤嚴(yán)重級(jí)別功能和記錄日志信息優(yōu)先級(jí)別的功能,只要在原來(lái)子類
DatabaseLog
和
TextFileLog
的基礎(chǔ)上再生成子類即可,同時(shí)需要引進(jìn)兩個(gè)新的接口
IError
和
I
Priority
,類結(jié)構(gòu)圖如下:
圖
4
實(shí)現(xiàn)代碼如下:
public
interface
IError
{
void
SetError();
}
public
interface
IPriority
{
void
SetPriority();
}
public
class
DBErrorLog
:
DatabaseLog
,
IError
{
public
override
void
Write(
string
log)
{
base
.Write(log);
}
public
void
SetError()
{
//......
功能擴(kuò)展,實(shí)現(xiàn)了記錄錯(cuò)誤嚴(yán)重級(jí)別
}
}
public
class
DBPriorityLog
:
DatabaseLog
,
IPriority
{
public
override
void
Write(
string
log)
{
base
.Write(log);
}
public
void
SetPriority()
{
//......
功能擴(kuò)展,實(shí)現(xiàn)了記錄優(yōu)先級(jí)別
}
}
public
class
TFErrorLog
:
TextFileLog
,
IError
{
public
override
void
Write(
string
log)
{
base
.Write(log);
}
public
void
SetError()
{
//......
功能擴(kuò)展,實(shí)現(xiàn)了記錄錯(cuò)誤嚴(yán)重級(jí)別
}
}
public
class
TFPriorityLog
:
TextFileLog
,
IPriority
{
public
override
void
Write(
string
log)
{
base
.Write(log);
}
public
void
SetPriority()
{
//......
功能擴(kuò)展,實(shí)現(xiàn)了記錄優(yōu)先級(jí)別
}
}
此時(shí)可以看到,如果需要相應(yīng)的功能,直接使用這些子類就可以了。這里我們采用了類的繼承方式來(lái)解決了對(duì)象功能的擴(kuò)展問(wèn)題,這種方式是可以達(dá)到我們預(yù)期的目的。然而,它卻帶來(lái)了一系列的問(wèn)題。首先,前面的分析只是進(jìn)行了一種功能的擴(kuò)展,如果既需要記錄錯(cuò)誤嚴(yán)重級(jí)別,又需要記錄優(yōu)先級(jí)時(shí),子類就需要進(jìn)行接口的多重繼承,這在某些情況下會(huì)違反類的單一職責(zé)原則,注意下圖中的藍(lán)色區(qū)域:
圖
5
實(shí)現(xiàn)代碼:
public
class
DBEPLog
:
DatabaseLog
,
IError
,
IPriority
{
public
override
void
Write(
string
log)
{
SetError();
SetPriority();
base
.Write(log);
}
public
void
SetError()
{
//......
功能擴(kuò)展,實(shí)現(xiàn)了記錄錯(cuò)誤嚴(yán)重級(jí)別
}
public
void
SetPriority()
{
//......
功能擴(kuò)展,實(shí)現(xiàn)了記錄優(yōu)先級(jí)別
}
}
public
class
TFEPLog
:
DatabaseLog
,
IError
,
IPriority
{
public
override
void
Write(
string
log)
{
SetError();
SetPriority();
base
.Write(log);
}
public
void
SetError()
{
//......
功能擴(kuò)展,實(shí)現(xiàn)了記錄錯(cuò)誤嚴(yán)重級(jí)別
}
public
void
SetPriority()
{
//......
功能擴(kuò)展,實(shí)現(xiàn)了記錄優(yōu)先級(jí)別
}
}
其次,隨著以后擴(kuò)展功能的增多,子類會(huì)迅速的膨脹,可以看到,子類的出現(xiàn)其實(shí)是
DatabaseLog
和
TextFileLog
兩個(gè)子類與新增加的接口的一種排列組合關(guān)系,所以類結(jié)構(gòu)會(huì)變得很復(fù)雜而難以維護(hù),正如象李建忠老師說(shuō)的那樣“子類復(fù)子類,子類何其多”;最后,這種方式的擴(kuò)展是一種靜態(tài)的擴(kuò)展方式,并沒(méi)有能夠真正實(shí)現(xiàn)擴(kuò)展功能的動(dòng)態(tài)添加,客戶程序不能選擇添加擴(kuò)展功能的方式和時(shí)機(jī)。
現(xiàn)在又該是
Decorator
模式出場(chǎng)的時(shí)候了,解決方案是把
Log
對(duì)象嵌入到另一個(gè)對(duì)象中,由這個(gè)對(duì)象來(lái)擴(kuò)展功能。首先我們要定義一個(gè)抽象的包裝類
LogWrapper
,讓它繼承于
Log
類,結(jié)構(gòu)圖如下:
圖
6
實(shí)現(xiàn)代碼如下:
public
abstract
class
LogWrapper
:
Log
{
private
Log
_log;
public
LogWrapper(
Log
log)
{
_log = log;
}
public
override
void
Write(
string
log)
{
_log.Write(log);
}
}
現(xiàn)在對(duì)于每個(gè)擴(kuò)展的功能,都增加一個(gè)包裝類的子類,讓它們來(lái)實(shí)現(xiàn)具體的擴(kuò)展功能,如下圖中綠色的區(qū)域:
圖
7
實(shí)現(xiàn)代碼如下:
public
class
LogErrorWrapper
:
LogWrapper
{
public
LogErrorWrapper(
Log
_log)
:
base
(_log)
{
}
public
override
void
Write(
string
log)
{
SetError();
//......
功能擴(kuò)展
base
.Write(log);
}
public
void
SetError()
{
//......
實(shí)現(xiàn)了記錄錯(cuò)誤嚴(yán)重級(jí)別
}
}
public
class
LogPriorityWrapper
:
LogWrapper
{
public
LogPriorityWrapper(
Log
_log)
:
base
(_log)
{
}
public
override
void
Write(
string
log)
{
SetPriority();
//......
功能擴(kuò)展
base
.Write(log);
}
public
void
SetPriority()
{
//......
實(shí)現(xiàn)了記錄優(yōu)先級(jí)別
}
}
到這里,
LogErrorWrapper
類和
LogPriorityWrapper
類真正實(shí)現(xiàn)了對(duì)錯(cuò)誤嚴(yán)重級(jí)別和優(yōu)先級(jí)別的功能的擴(kuò)展。我們來(lái)看一下客戶程序如何去調(diào)用它:
public
class
Program
{
public
static
void
Main(
string
[] args)
{
Log
log =
new
DatabaseLog
();
LogWrapper
lew1 =
new
LogErrorWrapper
(log);
//
擴(kuò)展了記錄錯(cuò)誤嚴(yán)重級(jí)別
lew1.Write(
"Log Message"
);
LogPriorityWrapper
lpw1 =
new
LogPriorityWrapper
(log);
//
擴(kuò)展了記錄優(yōu)先級(jí)別
lpw1.Write(
"Log Message"
);
LogWrapper
lew2 =
new
LogErrorWrapper
(log);
LogPriorityWrapper
lpw2 =
new
LogPriorityWrapper
(lew2);
//
這里是lew2
//
同時(shí)擴(kuò)展了錯(cuò)誤嚴(yán)重級(jí)別和優(yōu)先級(jí)別
lpw2.Write(
"Log Message"
);
}
}
注意在上面程序中的第三段裝飾才真正體現(xiàn)出了
Decorator
模式的精妙所在,這里總共包裝了兩次:第一次對(duì)
log
對(duì)象進(jìn)行錯(cuò)誤嚴(yán)重級(jí)別的裝飾,變成了
lew2
對(duì)象,第二次再對(duì)
lew2
對(duì)象進(jìn)行裝飾,于是變成了
lpw2
對(duì)象,此時(shí)的
lpw2
對(duì)象同時(shí)擴(kuò)展了錯(cuò)誤嚴(yán)重級(jí)別和優(yōu)先級(jí)別的功能。也就是說(shuō)我們需要哪些功能,就可以這樣繼續(xù)包裝下去。到這里也許有人會(huì)說(shuō)
LogPriorityWrapper
類的構(gòu)造函數(shù)接收的是一個(gè)Log對(duì)象,為什么這里可以傳入LogErrorWrapper對(duì)象呢?通過(guò)類結(jié)構(gòu)圖就能發(fā)現(xiàn),LogErrorWrapper類其實(shí)也是Log類的一個(gè)子類。
我們分析一下這樣會(huì)帶來(lái)什么好處?首先對(duì)于擴(kuò)展功能已經(jīng)實(shí)現(xiàn)了真正的動(dòng)態(tài)增加,只在需要某種功能的時(shí)候才進(jìn)行包裝;其次,如果再出現(xiàn)一種新的擴(kuò)展功能,只需要增加一個(gè)對(duì)應(yīng)的包裝子類(注意:這一點(diǎn)任何時(shí)候都是避免不了的),而無(wú)需再進(jìn)行很多子類的繼承,不會(huì)出現(xiàn)子類的膨脹,同時(shí)
Decorator
模式也很好的符合了面向?qū)ο笤O(shè)計(jì)原則中的“優(yōu)先使用對(duì)象組合而非繼承”和“開(kāi)放
-
封閉”原則。
.NET
中的裝飾模式
1
.
.NET
中
Decorator
模式一個(gè)典型的運(yùn)用就是關(guān)于
Stream
,它存在著如下的類結(jié)構(gòu):
圖
8
可以看到,
BufferedStream
和
CryptoStream
其實(shí)就是兩個(gè)包裝類,這里的
Decorator
模式省略了抽象裝飾角色(
Decorator
),示例代碼如下:
class
Program
{
public
static
void
Main(
string
[] args)
{
MemoryStream
ms =
new
MemoryStream
(
new
byte
[] { 100,456,864,222,567});
//
擴(kuò)展了緩沖的功能
BufferedStream
buff =
new
BufferedStream
(ms);
//
擴(kuò)展了緩沖,加密的功能
CryptoStream
crypto =
new
CryptoStream
(buff);
}
}
通過(guò)反編譯,可以看到
BufferedStream
類的代碼(只列出部分),它是繼承于
Stream
類:
public
sealed
class
BufferedStream
: Stream
{
// Methods
private
BufferedStream();
public
BufferedStream(Stream stream);
public
BufferedStream(Stream stream,
int
bufferSize);
// Fields
private
int
_bufferSize;
private
Stream _s;
}
2
.在
Enterprise Library
中的
DAAB
中有一個(gè)
DbCommandWrapper
的包裝類,它實(shí)現(xiàn)了對(duì)
IDbCommand
類的包裝并提供了參數(shù)處理的功能。結(jié)構(gòu)圖如下:
圖
9
示意性代碼如下:
public
abstract
class
DBCommandWrapper
:
MarshalByRefObject
,
IDisposable
{
}
public
class
SqlCommandWrapper
:
DBCommandWrapper
{
}
public
class
OracleCommandWrapper
:
DBCommandWrapper
{
}
效果及實(shí)現(xiàn)要點(diǎn)
1
.Component類在Decorator模式中充當(dāng)抽象接口的角色,不應(yīng)該去實(shí)現(xiàn)具體的行為。而且Decorator類對(duì)于Component類應(yīng)該透明,換言之Component類無(wú)需知道Decorator類,Decorator類是從外部來(lái)擴(kuò)展Component類的功能。
2
.Decorator類在接口上表現(xiàn)為is-a Component的繼承關(guān)系,即Decorator類繼承了Component類所具有的接口。但在實(shí)現(xiàn)上又表現(xiàn)為has-a Component的組合關(guān)系,即Decorator類又使用了另外一個(gè)Component類。我們可以使用一個(gè)或者多個(gè)Decorator對(duì)象來(lái)“裝飾”一個(gè)Component對(duì)象,且裝飾后的對(duì)象仍然是一個(gè)Component對(duì)象。
3
.Decortor模式并非解決“多子類衍生的多繼承”問(wèn)題,Decorator模式的應(yīng)用要點(diǎn)在于解決“主體類在多個(gè)方向上的擴(kuò)展功能”——是為“裝飾”的含義。
4
.對(duì)于Decorator模式在實(shí)際中的運(yùn)用可以很靈活。
如果只有一個(gè)ConcreteComponent類而沒(méi)有抽象的Component類,那么Decorator類可以是ConcreteComponent的一個(gè)子類。
圖10
如果只有一個(gè)ConcreteDecorator類,那么就沒(méi)有必要建立一個(gè)單獨(dú)的Decorator類,而可以把Decorator和ConcreteDecorator的責(zé)任合并成一個(gè)類。
圖11
5
.Decorator模式的優(yōu)點(diǎn)是提供了比繼承更加靈活的擴(kuò)展,
通過(guò)使用不同的具體裝飾類以及這些裝飾類的排列組合,可以創(chuàng)造出很多不同行為的組合。
6
.由于使用裝飾模式,可以比使用繼承關(guān)系需要較少數(shù)目的類。使用較少的類,當(dāng)然使設(shè)計(jì)比較易于進(jìn)行。但是,在另一方面,使用裝飾模式會(huì)產(chǎn)生比使用繼承關(guān)系更多的對(duì)象。更多的對(duì)象會(huì)使得查錯(cuò)變得困難,特別是這些對(duì)象看上去都很相像。
適用性
在以下情況下應(yīng)當(dāng)使用裝飾模式:
1.
需要擴(kuò)展一個(gè)類的功能,或給一個(gè)類增加附加責(zé)任。
2.
需要?jiǎng)討B(tài)地給一個(gè)對(duì)象增加功能,這些功能可以再動(dòng)態(tài)地撤銷。
3.
需要增加由一些基本功能的排列組合而產(chǎn)生的非常大量的功能,從而使繼承關(guān)系變得不現(xiàn)實(shí)。
總結(jié)
Decorator
模式采用對(duì)象組合而非繼承的手法,實(shí)現(xiàn)了在運(yùn)行時(shí)動(dòng)態(tài)的擴(kuò)展對(duì)象功能的能力,而且可以根據(jù)需要擴(kuò)展多個(gè)功能,避免了單獨(dú)使用繼承帶來(lái)的“靈活性差”和“多子類衍生問(wèn)題”。同時(shí)它很好地符合面向?qū)ο笤O(shè)計(jì)原則中“優(yōu)先使用對(duì)象組合而非繼承”和“開(kāi)放
-
封閉”原則。
參考資料
閻宏,《
Java
與模式》,電子工業(yè)出版社
James W. Cooper
,《
C#
設(shè)計(jì)模式》,電子工業(yè)出版社
Alan Shalloway James R. Trott
,《
Design Patterns Explained
》,中國(guó)電力出版社
MSDN WebCast
《
C#
面向?qū)ο笤O(shè)計(jì)模式縱橫談
(10) Decorator
裝飾模式
(
結(jié)構(gòu)型模式
)
》
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺(jué)我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
