设计模式之装饰模式
设计模式--装饰模式
现在我们来学习装饰模式。说实话,真不想写这个,因为提到这个装饰,程序员就很伤感(我也是),就想到了遥远地她和虚无缥缈地房子。房子都还没着落,谈什么装修和粉饰啊。一堵粗糙的墙,刷上白白地粉,再贴上几张壁画,整个一焕然一新。多美的事啊。哎,既然想到了,就咬着牙多想会,至少心里还有个期盼。真心祝愿大家看完这篇文章后都能够梦想成真。
生活中的装饰是很好理解的,我们打两个比方。先还是说房子的装修(我不是故意的),装修无非就是要在墙上刷上粉贴上壁纸挂上饰物,让房屋显得更加雅致美观。但墙还是那堵墙,本质不会改变,只是多了一层包装而已。再看一个例子,假设说你有一个一个的列车车厢,每一个车厢都有对运输功能作一些不同的增强,然后你选取一些这样的车厢,连接起来,形成一个专列,这个专列的功能就是组成它的那些车厢的功能的叠加。这些都是生活中比较典型的装饰模式。
看了《大话设计模式》后,凭自己的理解写了一个90坦克的小程序,当然不是玩的,紧为了表现出这种设计模式的好处,所以用控制台应用程序。
大鸟:“如果让你写一个90坦克的程序,坦克能吃各种宝贝起到不同的作用,现在就拿3中宝贝,帽子(受到攻击无影响)、枪(双弹、并能射穿钢板,而且能抵一命)、船(能在水里行走,也能抵一命),说说你的思路?”
“这还不简单,写一个接口,里面封装一些属性和方法,然后写不同的子类,在子类中实现接口不就行了!”小鸟得意地回答,心想这样可把面向对象编程用进去了啊,还用到多态,多灵活啊。
“呵呵,小菜就是小菜啊,表面上用到了多态接口,你有没有算下,你要写多少个子类?我和你算一下,什么都没吃的最原始坦克、吃帽子的坦克、吃枪的坦克、吃船的坦克、同时吃了帽子和枪的坦克、同时吃了帽子和船的坦克、同时吃了枪和船的坦克、同时吃帽子船和枪的坦克,一共2的3次方种,也就是8种组合呢,这才3种宝贝呢,真正的游戏里面那么多种,共2的n次种呢”大鸟道。
“有什么好办法呢?”
“不懂就学,其实也没什么稀罕的,这可以用一个非常有意思的设计模式来实现。”
装饰模式
装饰模式,动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。
记住,面向对象编程其实就是模拟现实,当你遇到一个问题苦思冥想得不出答案的时候,不防走出屋子,灵感或许来自于现实。
现实生活中,U盘、MP3播放器、手机等具有USB接口的电子产品一般具有USB最基本的功能,那就是数据传输----写数据和读数据,因此,抽象出一个接口:
/// <summary>
/// USB接口
/// </summary>
public interface IUSB
{
/// <summary>
/// 读数据
/// </summary>
void ReadData();
/// <summary>
/// 写数据
/// </summary>
void WriteData();
}
分别让U盘和MP3播放器来实现数据传输功能:
/// <summary>
/// U盘
/// </summary>
public class UDisk:IUSB
{
#region IUSB 成员
public void ReadData()
{
Console.WriteLine("U盘读数据.....");
}
public void WriteData()
{
Console.WriteLine("U盘写数据....");
}
#endregion
}
public class MP3Player:IUSB
{
#region IUSB 成员
public void ReadData()
{
Console.WriteLine("Mp3播放器读数据.....");
}
public void WriteData()
{
Console.WriteLine("Mp3播放器写数据....");
}
#endregion
}
/// <summary>
/// 装饰者
/// </summary>
public class Decorator:IUSB
{
private IUSB usb;
public Decorator(IUSB usb)
{
this.usb = usb;
}
#region IUSB 成员
public virtual void ReadData() //其实执行的是IUSB的ReadData
{
usb.ReadData();
}
public virtual void WriteData() //其实执行的是IUSB的WriteData
{
usb.WriteData();
}
#endregion
}
这里装饰者Decorator与IUSB之间的关系满足is – a关系,即Decorator is-a IUSB,即子类一定是父类,而父类不一定是子类。这里虽然是接口,但在这儿一样适用,话句话说,人一定是动物,而是动物就不一定是人了。
装饰者Decorator同时还与IUSB直接满足has – a的关系。两个类之间要进行通信,常在一个类中引用另一个类,这个在WinForm种的窗体之间通信用的多,一个窗体如果要访问另外一个窗体的public属性的属性或方法,常引用要访问的窗体作为私有成员变量。这是UML种的关联关系。
public class People //人类
{
private Climate climate; //气候
}
人类和气候不是彼此独立的,气候的变化会影响到人类不同的属性或行为,因此在人类中引用气候。
进一步研究发现,在Decorator的构造函数执行后,同时也确定了接口,也就是它们同时生成,满足合成关系。
class Bird
{
private Wing wing;
public Bird()
{
wing = new Wing();//在鸟Bird类中,初始化时,实例化翅膀Wing,它们之间同时生成
}
}
上面的USB接口例子,例如MP3播放器除了读数据和传输数据的基本功能外,还有播放音乐的功能、阅读电子书、照相等功能。你可以更改类,添加新的方法或字段属性来达到目的,但这样违背了开闭原则,况且有的Mp3播放器没有照相功能。装饰模式是为已有功能动态地添加更多功能的一种方式。
/// <summary>
/// 音乐播放器,提供播放音乐的功能
/// </summary>
public class MusicPlayer:Decorator
{
public MusicPlayer(IUSB usb)
: base(usb)
{ }
/// <summary>
/// 播放音乐,新增的功能
/// </summary>
public void PlayMusic()
{
Console.WriteLine("播放音乐.....");
}
}
/// <summary>
/// 照相机
/// </summary>
public class Camera:Decorator
{
public Camera(IUSB usb)
: base(usb)
{ }
/// <summary>
/// 照相,新增的功能
/// </summary>
public void TakePhotoes()
{
Console.WriteLine("照相.....");
}
}
客户端代码:
class Program
{
static void Main(string[] args)
{
IUSB mp3player = new MP3Player(); //只能读写数据的mp3
MusicPlayer player= new MusicPlayer(mp3player); //装饰音乐播放
player.ReadData();
player.WriteData();
player.PlayMusic(); //具有播放音乐的功能
Camera mp3Carm = new Camera(player);
mp3Carm.TakePhotoes(); //具有照相的功能
Console.ReadKey();
}
}
运行结果截图:
经过两次“装饰后”,原本只能读写数据的播放器现在能播放音乐和照相了,这都是在客户端动态添加的功能。共2^2=4种组合,只能传输数据、能传输数据能播放音乐、能传输数据能照相、能传输数据能播放音乐也能照相。以下是组合,或许没有只能传输数据的MP3吧,就当是坏了的吧,当U盘用O(∩_∩)O~。
① 只能读写数据的MP3: IUSB mp3player = new MP3Player(); //只能读写数据的mp3
②能传输数据并能播放音乐的Mp3组合:
IUSB mp3player = new MP3Player(); //只能读写数据的mp3
MusicPlayer player= new MusicPlayer(mp3player); //装饰音乐播放
装饰后的player即是。
③能传输数据能照相的MP3组合:
IUSB mp3player = new MP3Player(); //只能读写数据的mp3
Camera mp3 = new Camera(mp3player); //新增照相功能
④能传输数据也能播放音乐和照相的Mp3组合
IUSB mp3player = new MP3Player(); //只能读写数据的mp3
Camera mp3 = new Camera(mp3player); //新增照相功能
MusicPlayer mp3MusicCarm = new MusicPlayer(mp3);
“小鸟,看了这个例子后刚才的90坦克小游戏能设计了吧?”大鸟问道。
“没问题。”小鸟很自信地回答。下面是小鸟的分析过程及代码
90坦克小游戏
首先,不管是敌方坦克还是我方坦克已及各种各样的坦克,把公共部分抽取出,写一个接口:
public interface ITank
{
/// <summary>
/// 剩余生命条数
/// </summary>
int LifeCount
{
set;
get;
}
/// <summary>
/// 生命值,能承受多少枪爆炸
/// </summary>
int HP
{
set;
get;
}
/// <summary>
/// 射击其它坦克
/// </summary>
void Shoot(ITank tank);
/// <summary>
/// 被攻击
/// </summary>
void Attacked();
}
分别写一个我方和敌方的最原始最简单的坦克,实现接口:
/// <summary>
/// 我方最初始的坦克
/// </summary>
public class MySimpleTank : ITank
{
private int _lifeCount = 3; //初始3条命
private int _hp = 1; //初始被攻击一次就挂掉
#region ITank 成员
/// <summary>
/// 剩余生命条数
/// </summary>
public int LifeCount
{
get
{
return this._lifeCount;
}
set
{
this._lifeCount = value;
}
}
/// <summary>
/// 生命值,能承受多少枪爆炸
/// </summary>
public int HP
{
get
{
return this._hp;
}
set
{
this._hp = value;
}
}
/// <summary>
/// 攻击其他坦克
/// </summary>
/// <param name="tank"></param>
public void Shoot(ITank tank)
{
Console.WriteLine("我方坦克开始攻击敌方....");
tank.Attacked();
}
/// <summary>
/// 被攻击
/// </summary>
public void Attacked()
{
this._hp--;
Console.WriteLine("我方坦克受到攻击,HP减少...剩余HP={0}", this._hp);
if (this._hp == 0)
{
this._lifeCount--;
Console.WriteLine("我方坦克受到攻击后爆炸.....");
Console.WriteLine("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
}
}
#endregion
}
/// <summary>
/// 敌方坦克
/// </summary>
public class EnemySimpleTank : ITank
{
private int _lifeCount = 1; //敌方坦克初始1条命
private int _hp = 2; //敌方坦克初始HP=2;
#region ITank 成员
/// <summary>
/// 剩余生命条数
/// </summary>
public int LifeCount
{
get
{
return this._lifeCount;
}
set
{
this._lifeCount = value;
}
}
/// <summary>
/// 生命值,能承受多少枪爆炸
/// </summary>
public int HP
{
get
{
return this._hp;
}
set
{
this._hp = value;
}
}
/// <summary>
/// 攻击其他坦克
/// </summary>
/// <param name="tank"></param>
public void Shoot(ITank tank)
{
Console.WriteLine("敌方坦克开始攻击.....");
tank.Attacked();
}
public void Attacked()
{
this._hp--;
Console.WriteLine("敌方坦克受到攻击,HP减少...,剩余HP={0}", this._hp);
if (this._hp == 0)
{
this._lifeCount--;
Console.WriteLine("敌方坦克受到攻击后爆炸.....");
Console.WriteLine("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
}
}
#endregion
}
写一个装饰者Decorator类,实现接口,不过具体都是调用接口的方法和属性,在Decorator类中引用接口ITank,并在Decorator的构造函数中初始化Itank.
/// <summary>
/// 装饰者
/// </summary>
public class Decorator:ITank
{
private ITank _tank;
public Decorator(ITank tank)
{
this._tank = tank;
}
#region ITank 成员
public virtual int LifeCount
{
get
{
return _tank.LifeCount;
}
set
{
_tank.LifeCount = value;
}
}
public virtual int HP
{
get
{
return _tank.HP;
}
set
{
_tank.HP = value;
}
}
public virtual void Shoot(ITank tank)
{
_tank.Shoot(tank);
}
public virtual void Attacked()
{
_tank.Attacked();
}
#endregion
}
分别写穿了防弹衣的坦克、吃了枪的超强坦克、吃了船的坦克。
吃了帽子的坦克被攻击后HP不会减少,故要重写父类的Attacked方法。
/// <summary>
/// 吃了防弹衣的坦克,无视受到攻击
/// </summary>
public class MyProtectedTank:Decorator
{
public MyProtectedTank(ITank tank)
: base(tank)
{
Console.WriteLine("我方坦克穿上防弹衣.....");
}
//重写被攻击
public override void Attacked()
{
Console.WriteLine("我方坦克受到攻击,由于有防弹衣,故无影响");
//base.Attacked();
}
//额外增加的属性
private string coat = "防弹衣";
public string Coat
{
get { return coat; }
}
}
吃了枪的坦克威力大增,并且能抵一条命,即HP增一,并出现新的功能-----子弹能穿钢板。因此要新增一个方法。
/// <summary>
/// 我方吃了枪的双枪超强坦克
/// </summary>
public class MySuperDoubleGunTank : Decorator
{
public MySuperDoubleGunTank(ITank tank)
: base(tank)
{
Console.WriteLine("坦克现在吃了枪,威力大增.......");
if (tank.HP < 3)
{
tank.HP++;
Console.WriteLine("坦克吃了枪,HP增1,现在HP={0}",tank.HP);
}
}
//额外增加的功能
/// <summary>
/// 迟枪后,能干掉钢板
/// </summary>
public void DestorySteel()
{
Console.WriteLine("消灭钢板。。。。。");
}
}
吃了船的坦克,船能抵一命,能穿越水域,因此也要新增一个方法:
/// <summary>
/// 我方开在穿上的坦克,能穿越水
/// </summary>
public class MyBoatTank:Decorator
{
public MyBoatTank(ITank tank)
: base(tank)
{
Console.WriteLine("我方坦克开上船,现在能穿越水了......");
if (tank.HP < 3)
{
tank.HP++;
Console.WriteLine("我方坦克上船,HP增1,现在HP={0}",tank.HP);
}
}
/// <summary>
/// 新增的方法,能穿越水域
/// </summary>
public void CrossWater()
{
Console.WriteLine("穿越水域......");
}
}
客户端演示代码:
class Program
{
static void Main(string[] args)
{
ITank myTank = new MySimpleTank();//我方最原始坦克,最垃圾的枪,HP=1,共3条命
ITank enemyTank = new EnemySimpleTank();//敌方初始坦克
myTank.Shoot(enemyTank);//第一次攻击敌方坦克
MyProtectedTank myProtectedTank = new MyProtectedTank(myTank); //我方原始坦克穿上防弹衣
Console.WriteLine(myProtectedTank.Coat); //新增的属性
enemyTank.Shoot(myProtectedTank); //敌方坦克还击,重写了被攻击的方法
MySuperDoubleGunTank mySuperDoubleGunTank = new MySuperDoubleGunTank(myProtectedTank); //吃了枪,超强坦克,HP=2,新增功能穿钢板
mySuperDoubleGunTank.Shoot(enemyTank); //第二次攻击敌方坦克
mySuperDoubleGunTank.DestorySteel(); //新增的方法,能消灭钢板
MyBoatTank myBoatTank = new MyBoatTank(mySuperDoubleGunTank);//吃了船,HP=3,新增功能穿越水域
myBoatTank.CrossWater(); //穿越水
Console.ReadKey();
}
}
运行结果截图:
上面的例子演示了我方坦克从最初到最后演变成具有多重属性的坦克,集防弹衣、双杆枪、乘船等于一身,正果过程都是动态添加的功能。
装饰模式总结
装饰模式是为已有的功能动态地添加更多功能的一种方式。
当系统需要新功能的时候,若向旧的类中添加新的代码,这些新加的代码通常装饰了原有类的核心职责或主要行为,但这种做法的问题在于,它们在主类中加入了新的字段,新的方法和新的逻辑,从而增加了主类的复杂度,而这些新加入的东西仅仅是为了满足一些只在某种特定情况下才会执行的特殊行为的需要。而装饰模式切提供了一个非常好的解决方案,他把每个要装饰的功能都放在单独的类中,并让这个类包装他所要装饰的对象,因此,当需要执行特殊行为时,客户端代码就可以在运行时更具需要有选择地、顺序地使用装饰功能包装对象了。
适用性
在以下情况下应当使用装饰模式:
1.需要扩展一个类的功能,或给一个类增加附加责任。
2.需要动态地给一个对象增加功能,这些功能能再动态地撤销。
3.需要增加由一些基本功能的排列组合而产生的非常大量的功能,从而使继承关系变得不现实。
杨盛超
2010-11-14