文章中的源代码:http://download.csdn.net/detail/zoharxmj/7100873
假设有这样一家游戏制作公司,最初他们想做一款RPG游戏,主角拿着钱和盾,名字叫林克……林克是用了一个叫RpgMan的类来实现的。此类实现了许多接口,对应着林克的各种功能,其中有一个IEquip的接口,它要求RpgMan能实现查看角色当前的装备的功能。顺带一提,林克可以装备盾和剑,所以在实现查看装备的时候,会把此二者显示出来。当然,RpgMan实际上不仅仅实现了IEquip,它同时也应该实现了许多RPG角色拥有的复杂功能,这里都先省略不表。
public interface IEquip {
public void viewEquip();
}
public class RpgMan implements IEquip {
private String sword;
private String shield;
public void equip(String sword, String shield) {
this.sword = sword;
this.shield = shield;
}
@Override
public void viewEquip() {
System.out.println("--Equip List--");
System.out.println("Sword: "+sword);
System.out.println("Shield: "+shield);
}
}
过了好多年,公司又想做一款游戏。RpgMan作为一个功能强大、稳定成熟的类,希望能够拿来复用。但是有一个问题就是,新游戏中的角色不只能装备盾和剑,还能装备铠甲、头盔、鞋。那么仅仅只能显示剑和盾的viewEquip明显是满足不了需要的。而且更不妙的是,丢失了RpgMan的源码,所以无法对RpgMan直接进行修改。
这时候就会用到装饰模式了,它使我们在不修改已有的类的基础上对它进行装饰性扩展。比如在上面的情况中提到的,在显示原有的装备剑和盾以外,还要显示更多的装备。为此,首先我们创建装饰者的抽象父类,里面设置一个成员函数保存我们要装饰的对象man,在构造时得到此对象给它赋值。
public abstract class EquipDecorator implements IEquip {
protected IEquip man;
public EquipDecorator(IEquip man) {
this.man = man;
}
}
这是装饰模式的一个特点,它会用待装饰的对象构造装饰对象。另外一个特点是,此装饰父类需要实现被装饰的接口,也就是IEquip。IEquip中的方法就是我们要装饰的方法,即viewEquip。在此例子中,也就是需要用一个林克去构造一个装饰装备功能后的林克,当我们查看装饰后的林克时,会看到他还装备了头盔、铠甲和鞋。
接下来我们继承实现抽象装饰父类,让装饰类中还可以装备另外三样防具。接下来就是重点,对viewEquip方法具体进行装饰,在显示原来的装备的基础上再显示新的三件装备。
public class FighterDecorator extends EquipDecorator {
private String hat;
private String armor;
private String shoe;
public FighterDecorator(IEquip man) {
super(man);
}
public void newEquip(String hat, String armor, String shoe){
this.hat = hat;
this.armor = armor;
this.shoe = shoe;
}
@Override
public void viewEquip() {
man.viewEquip();
System.out.println("Hat: "+hat);
System.out.println("Armor: "+armor);
System.out.println("Shoe: "+shoe);
}
}
然后写一个测试类,测试程序。记得构造装饰类的时候,要用旧的对象来构造。
public class FinalFantasy {
public static void main(String[] args) {
RpgMan rpgMan = new RpgMan();
rpgMan.equip("wood sword", "saint shield");
FighterDecorator fighter = new FighterDecorator(rpgMan);
fighter.newEquip("leaf hat", "iron armor", "flying boots");
System.out.println("*Fighter*");
fighter.viewEquip();
}
}
运行结果:
*Fighter*
--Equip List--
Sword: wood sword
Shield: saint shield
Hat: leaf hat
Armor: iron armor
Shoe: flying boots
游戏大受好评,于是一年后团队打算再出续作,这次要加入魔石和职业系统。魔法师职业可以装备魔石,战士职业可以装备头盔铠甲和鞋,还有可以装备一切的魔法战士。
假设因为之前写的那个战士装饰类功能完善内容丰富,所以打算继续复用。所以我们只能再加装饰类,好在我们之前已经装饰过一次了,所以这次直接继承上次写的抽象装饰父类就可以了。
public class MagicDecorator extends EquipDecorator {
private List<String> stones = new ArrayList<String>();
public MagicDecorator(IEquip man) {
super(man);
}
public void addStone(String stone){
stones.add(stone);
}
@Override
public void viewEquip() {
man.viewEquip();
for (int i=0; i<stones.size(); i++){
System.out.println("Stone"+(i+1)+": "+stones.get(i));
}
}
}
魔石装饰类,原理同之前写的战士装饰类。写完之后测试一下
public class FinalFantasy {
public static void main(String[] args) {
RpgMan rpgMan = new RpgMan();
rpgMan.equip("wood sword", "saint shield");
FighterDecorator fighter = new FighterDecorator(rpgMan);
fighter.newEquip("leaf hat", "iron armor", "flying boots");
System.out.println("*Fighter*");
fighter.viewEquip();
MagicDecorator oldMage = new MagicDecorator(rpgMan);
oldMage.addStone("Fire");
oldMage.addStone("Thuder");
System.out.println("*Old Mage*");
oldMage.viewEquip();
MagicDecorator magicFighter = new MagicDecorator(fighter);
magicFighter.addStone("Odin");
magicFighter.addStone("Leviathan");
magicFighter.addStone("Bahamut");
System.out.println("*Magic Fighter*");
magicFighter.viewEquip();
}
}
运行结果:
*Fighter*
--Equip List--
Sword: wood sword
Shield: saint shield
Hat: leaf hat
Armor: iron armor
Shoe: flying boots
*Old Mage*
--Equip List--
Sword: wood sword
Shield: saint shield
Stone1: Fire
Stone2: Thuder
*Magic Fighter*
--Equip List--
Sword: wood sword
Shield: saint shield
Hat: leaf hat
Armor: iron armor
Shoe: flying boots
Stone1: Odin
Stone2: Leviathan
Stone3: Bahamut
注意魔法战士这个对象是有两层的装饰。先是林克,然后将林克装饰成战士,再将战士装饰成了魔法战士。装饰是可以这样一层套一层的,并且层是有顺序的(比如可以先用魔石装饰,再用战士装饰,显示装备的顺序就不一样了),这就是装饰模式的第三个特点。
这就是装饰模式了,一种结构设计模式,然后我们看一下装饰模式的示意图(图片来自wiki)。
最开始是组件和组件的实现,也就是我们的IEquip和RpgMan。当我们想装饰的时候,就会建立对应的装饰者和装饰者实现,即我们的EquipDecorator,以及它的实现FighterDecorator和MagicDecorator。
我们装饰的是组件接口里声明的方法,也就是viewEquip这个方法,让它能够支持更丰富地显示功能。事实上添加新装备的那些方法,并不能算是装饰,只是用来辅助viewEquip而存在的。
不如想象一下如果这个游戏远比我们写的复杂,除了这个IEquip组件,RpgMan还有控制其HP和MP的组件,还有战斗用的各种功能的组件,而我们都可以通过写这些组件的装饰类来进它们进行扩展。将一个原本系统简单的游戏扩展成一个系统更加丰富多彩的系统,而最关键的是,RpgMan自身的那些核心的复杂稳定的逻辑实现会完整地保留复用。
这就是装饰模式的作用.
另外装饰模式也存在着两种简化的形式,上面描写的情况是一种大型的、理想的设计的情况,简化的装饰模式是为了应对其他不够大型或者不够理想的情况。比如我们要装饰的对象并不是这种一个对象实现多个组件接口的设计,甚至,根本没有接口,只有一个RpgMan。
我们要装饰这样的对象时,那么就只好让我们的EquipDecorator去继承RpgMan了,这就是第一种简化。另一种简化是说,假如我们只打算实现一次装饰,之后只需要一直维护这个装饰就可以了,那么就没必要写装饰抽象父类EquipDecorator了,直接写实现类就可以,由实现类去实现IEquip接口。
最后举一个将以上两种简化都用到的简单例子,我们写一个叫DateDect的类来装饰JAVA里自带的Date类。
Date类默认的getYear得到的数字是今年的年份-1900年。比如今年是2014年,那得到的就是114.而getMonth得到的月份,则是从0开始的,也就是假如现在是三月,得到的是2……。也就是说我们每次用new Date()得到现在的时间,如果想显示的话,都要做一下年份+1900和月份+1的处理……
这个问题可以用一个装饰类解决,新建DateDect然后直接继承Date,在里面我们装饰一下getYear和getMonth
public class DateDect extends Date {
private Date date;
public DateDect(Date date) {
super();
this.date = date;
}
@Override
public int getMonth() {
return date.getMonth()+1;
}
@Override
public int getYear() {
return date.getYear()+1900;
}
}
写个测试类:
public class TestDateDect {
public static void main(String[] args) {
Date date = new Date();
System.out.println("before decorator");
System.out.println(date.getYear()+"年"+date.getMonth()+"月"+date.getDate()+"日");
DateDect dateDect = new DateDect(date);
System.out.println("after decorator");
System.out.println(dateDect.getYear()+"年"+dateDect.getMonth()+"月"+dateDect.getDate()+"日");
}
}
运行结果:
before decorator
114年2月26日
after decorator
2014年3月26日
这样就得到了理想的数值了。而且此装饰模式非常简单,就是因为应用了上面的两种简化。
所以从理论上看似很复杂的装饰模式,其实完全可以在平日里就简单轻松地使用。