以下三点,适用于本系列所有文章。
1、同一个故事,从不同的角度看,就可以引申出不同的模式。本系列中所举模式,未必是当前故事是匹配的,还望大家多提意见,一起讨论,一起提高。
2、类图部分只为突出要点,非重点处不必追究细节。如下面的例子人物应为SingleTon,在“吕布”和“武将”两个Class之间再加一层“董卓武将”似乎更合情理。但若面面俱到,则难免喧宾夺主之嫌,故略之。以后各篇也是如此,不便处敬请谅解。
3、文中所举三国故事,细节上多有虚构戏说成分,旨在说明问题,一笑了之,切莫当真。
Decorator模式(1.虎牢关三英战吕布 2.云长降曹受封赏)
第一个例子取材自三国演义第五回《发矫诏诸镇应曹公 破关兵三英战吕布》,说的是十八路诸候大军逼向虎牢关,吕布率部来迎,连胜盟军数员大将。此时张飞出马,原文精彩部分如下“飞抖擞精神,酣战吕布。连斗五十余合,不分胜负。云长见了,把马一拍,舞八十二斤青龙偃月刀,来夹攻吕布。三匹马丁字儿厮杀。战到三十合,战不倒吕布。刘玄德掣双股剑,骤黄鬃马,刺斜里也来助战。这三个围住吕布。转灯儿般厮杀。八路人马,都看得呆了。吕布架隔遮拦不定,看着玄德面上,虚刺一戟,玄德急闪。吕布荡开阵角,倒拖画戟,飞马便回。三个那里肯舍,拍马赶来。八路军兵,喊声大震,一齐掩杀。”
刘关张何以战败吕布?用的正是装饰者(Decorator)模式。
将故事抽象为如下类图
本例中,两员武将“单挑”,每回合本应各发一招,但通过Decorator模式,张飞聚合了关羽,关羽又聚合了刘备。也就是张飞的“单挑”方法,在对外接口不变(吕布不知情)的情况下,被另外添加了新的行为,导致吕布每回合实际受到了三个人的攻击,故而落败。
代码如下:
namespace ConsoleApplication1
{
public class Client
{
[STAThread]
static void Main(string[] args)
{
武将 吕 = new 吕布();
菜鸟武将 方 = new 方悦();
菜鸟武将 穆 = new 穆顺();
菜鸟武将 武 = new 武安国();
菜鸟武将 公孙 = new 公孙瓒();
桃园英雄 张 = new 张飞(new 关羽(new 刘备(null)));
PK(1,吕,方);
PK(2,吕,穆);
PK(3,吕,武);
PK(4,吕,公孙);
PK(5,吕,张);
Console.ReadLine();
}
private static void PK(int num,武将 吕,武将 诸侯武将)
{
Console.WriteLine("第{0}场 1回合:",num);
吕.单挑();
诸侯武将.单挑();
Console.WriteLine("----------------------");
}
}
public abstract class 武将
{
public abstract void 单挑();
}
public abstract class 十八路诸侯武将 : 武将
{
}
public abstract class 菜鸟武将 : 十八路诸侯武将
{
}
public abstract class 桃园英雄 : 十八路诸侯武将
{
protected 桃园英雄 m_兄弟 = null;
public 桃园英雄(桃园英雄 帮手)
{
m_兄弟 = 帮手;
}
}
public class 吕布 : 武将
{
public 吕布()
{}
public override void 单挑()
{
Console.WriteLine("吕布出手!!!");
}
}
方悦、穆顺、武安国、公孙瓒#region 方悦、穆顺、武安国、公孙瓒
public class 方悦 : 菜鸟武将
{
public 方悦()
{}
public override void 单挑()
{
Console.WriteLine("方悦出手!");
}
}
public class 穆顺 : 菜鸟武将
{
public 穆顺()
{}
public override void 单挑()
{
Console.WriteLine("穆顺出手!");
}
}
public class 武安国 : 菜鸟武将
{
public 武安国()
{}
public override void 单挑()
{
Console.WriteLine("武安国出手!");
}
}
public class 公孙瓒 : 菜鸟武将
{
public 公孙瓒()
{}
public override void 单挑()
{
Console.WriteLine("公孙瓒出手!");
}
}
#endregion
刘、关、张#region 刘、关、张
public class 刘备 : 桃园英雄
{
public 刘备(桃园英雄 帮手):base(帮手)
{}
public override void 单挑()
{
Console.WriteLine("刘备出手:尝尝我双股剑的厉害!");
if (m_兄弟 != null)
m_兄弟.单挑();
}
}
public class 关羽 : 桃园英雄
{
public 关羽(桃园英雄 帮手):base(帮手)
{}
public override void 单挑()
{
Console.WriteLine("关羽出手:看我青龙偃月刀!");
if (m_兄弟 != null)
m_兄弟.单挑();
}
}
public class 张飞 : 桃园英雄
{
public 张飞(桃园英雄 帮手):base(帮手)
{}
public override void 单挑()
{
Console.WriteLine("张飞出手:吃俺老张一矛!");
if (m_兄弟 != null)
m_兄弟.单挑();
}
}
#endregion
}
执行效果图如下:
Decorator模式强调的是在不改变现有对象的基础上,动态修饰对象的行为。而且这种行为的添加,对客户端来说是透明(不知情,即客户端不用改动)的。并且,通过继承+聚合的这种结构关系,使得行为可以无限制地修饰下去。
当然Decorator的局限性也很明显。个人觉得,Decorator模式可能更适合于装饰整个新行为的过程中(新行为所在对象创建时和新行为执行时),不与原有行为发生数据冲突的情况。比如说简单的打印报表,报表的主体不变,页头和页尾都可以用不同的样式来修饰,这时用Decorator就比较合适。但如果主体行为是个update数据库的动作,装饰上的新行为也是一些update和delete的动作,这时用Decorator模式就要多加小心,因为前后几次的行为有数据上的共享,很容易出现逻辑不清,特别是装饰和层次再稍微多些的话,更要开发者必须非常清楚整个流程。
就把前面战吕布的例子作个引申:
张飞策马出阵的途中,在路上看见一瓶三鞭酒,张飞心想,“好啊,二哥斩了华雄,喝的也不过是军中的水酒。俺老张运气好,竟能喝到这喝了后一晚上能长一寸胡子的三鞭酒。战情紧急,待俺打发了吕布,再饮也不迟。”所以,张飞上阵,和吕布虚晃了一招,再回头取酒。“咦?找不到了,酒怎么没了,哪里去了?”
酒哪里去了?原来张飞在和吕布单挑时,将关羽聚合出来帮忙,偏偏关羽实例化时的构造函数有这样一段代码
while(吕布未到())
{
if (发现宝贝())
捡宝贝();
赶路++;
}
结果就是那瓶三鞭酒自然被云长收入怀中了,而这一切张飞并不知情。
所以,对于有可能发生数据冲突的Decorator模式,开发时应当多考虑一下前因后果。
可能有人要问,在上面的UML图中,桃园英雄向自身聚合的那根线,如果不指向自身,而指向“十八路诸侯武将”会是什么样的效果?
其实两者都是Decorator模式,因适用场合不同,所以结构也略有不同。我们不必拘泥于细节,只要抓住Decorator的本质思想就可以了。
如果那根聚合线指向“十八路诸侯武将”,桃园英雄带的帮手就可以不仅仅是刘关张了,可以包括菜鸟武将,当然了,菜鸟武将是不能继续聚合新的武将的(怎么有点象马和驴交配生了骡子,骡子却不能再生育的感觉啊,呵呵)。这是两者的差别,可以体会一下。
再举个例子,出自《三国演义》第二十五回《屯土山关公约三事 救白马曹操解重围》看下图。
上面的类图,如果在使用时,不聚合新对象的话,相当于没有装饰,就是一个普通的多态形式。
且说关羽入了相府之后,官拜汉寿亭候,三日一小宴,五日一大宴,封赏不绝。
我们看在这里如何是使用Decorator的
第一日
使者:“封关羽为汉寿亭侯~~~~”。
关羽:“云长受下,多谢丞相!!!”
代码:关羽.受赏(new 侯爵());
第二日
使者:“送关羽锦袍~~~~”。
关羽:“云长受下,多谢丞相!!!”
代码:关羽.受赏(new 锦袍());
第三日
使者:“送关羽赤兔马~~~~。”
关羽:“云长受下,多谢丞相!!!等等,马屁股上驮的什么?美女哎,知我者,曹丞相也 ^_^”
代码为:关羽.受赏(new 赤兔马(new 美女()));
封爵送袍都可以声张,送美女不能声张,要给云长留些面子。所以美女要装饰到赤兔马上,若二嫂问起,只答送马便是。
上图中,如果让关羽也实现“丞相封赏”这个接口行不行呢?这样不影响关羽受赏,好象功能还多了点?
这取决于系统实际的需求,不需要的功能最好不要加,不然可能会节外生枝,看下面这个例子。
某日,曹操召夏侯渊来见,“妙才啊,你平日出生忘死,屡立战功,今天终于有空,我可要好好奖励你一番,按单去领赏吧。”曹操抛下一张纸。
夏侯渊捡起来一看,“第一个,美女,我喜欢。第二个,金银,我喜欢。第三个,锦袍,我喜欢。第四个。。。关羽?哇靠,丞相,你当我是Gay啊!”