为什么说装饰器模式是运行时扩充,而继承是编译时扩充
装饰器模式主要是为了扩充一个类的功能,也就是说,它把一个类进行了一定的装饰,使它有了新的功能,但保证了类原有的功能。实现的方法通常是把原有类作为装饰类构造函数的一个参数进行传入。
咋一看,好像装饰器模式和继承没什么区别,都是保证原有的功能,然后在扩充功能。但其实区别还是挺大的,装饰器模式最主要的优势是比较灵活,因为它修饰哪个类是在运行时才确定的;而继承中,继承哪个类是在编写哪个继承类的时候就要确定下来的,也就是说继承是编译时确认的。什么,有点晕?且看下面的例子:
1 //Component 英雄接口 2 public interface Hero { 3 //学习技能 4 void learnSkills(); 5 } 6 //ConcreteComponent 具体英雄盲僧,,注意,可以有多个实现,虽然我这里只实现了一个 7 public class BlindMonk implements Hero { 8 9 private String name; 10 11 public BlindMonk(String name) { 12 this.name = name; 13 } 14 15 @Override 16 public void learnSkills() { 17 System.out.println(name + "学习了以上技能!"); 18 } 19 } 20 //Decorator 技能栏 21 public class Skills implements Hero{ 22 23 //持有一个英雄对象接口 24 private Hero hero; 25 26 public Skills(Hero hero) { 27 this.hero = hero; 28 } 29 30 @Override 31 public void learnSkills() { 32 if(hero != null) 33 hero.learnSkills(); 34 } 35 } 36 //ConreteDecorator 技能:Q 37 public class Skill_Q extends Skills{ 38 39 private String skillName; 40 41 public Skill_Q(Hero hero,String skillName) { 42 super(hero); 43 this.skillName = skillName; 44 } 45 46 @Override 47 public void learnSkills() { 48 System.out.println("学习了技能Q:" +skillName); 49 super.learnSkills(); 50 } 51 } 52 //ConreteDecorator 技能:W 53 public class Skill_W extends Skills{ 54 55 private String skillName; 56 57 public Skill_W(Hero hero,String skillName) { 58 super(hero); 59 this.skillName = skillName; 60 } 61 62 @Override 63 public void learnSkills() { 64 System.out.println("学习了技能W:" + skillName); 65 super.learnSkills(); 66 } 67 } 68 //ConreteDecorator 技能:E 69 public class Skill_E extends Skills{ 70 71 private String skillName; 72 73 public Skill_E(Hero hero,String skillName) { 74 super(hero); 75 this.skillName = skillName; 76 } 77 78 @Override 79 public void learnSkills() { 80 System.out.println("学习了技能E:"+skillName); 81 super.learnSkills(); 82 } 83 } 84 //ConreteDecorator 技能:R 85 public class Skill_R extends Skills{ 86 87 private String skillName; 88 89 public Skill_R(Hero hero,String skillName) { 90 super(hero); 91 this.skillName = skillName; 92 } 93 94 @Override 95 public void learnSkills() { 96 System.out.println("学习了技能R:" +skillName ); 97 super.learnSkills(); 98 } 99 } 100 //客户端:召唤师 101 public class Player { 102 public static void main(String[] args) { 103 //选择英雄 104 Hero hero = new BlindMonk("李青"); 105 106 Skills skills = new Skills(hero); 107 Skills r = new Skill_R(skills,"猛龙摆尾"); 108 Skills e = new Skill_E(r,"天雷破/摧筋断骨"); 109 Skills w = new Skill_W(e,"金钟罩/铁布衫"); 110 Skills q = new Skill_Q(w,"天音波/回音击"); 111 //学习技能 112 q.learnSkills(); 113 } 114 }
在上面的例子中,如果我要扩充hero的技能,而且我们不管这个hero到底是BlinMonk还是别的什么英雄,我们都想扩充他的技能。如果此时采用继承,那么我们必须为每一个英雄都派生一个子类,比如我要扩充盲僧的技能,那我就要从盲僧那里扩充一个类,要扩充提莫的技能,我又的从提莫那里扩充一个类(上面没有写提莫,如果要写的话,照着盲僧差不多写一个就行了),非常的不灵活。但是如果我们使用装饰模式,则只需要从他们的顶层类hero派生一个装饰类,并且这个装饰类的构造函数接受一个hero类型的对象作为传入参数。因为这个传入参数可以是任何hero的子类,所以他可以传入任何的英雄,无论是盲僧、刀妹还是提莫,并且因为这些子类都能够实现多态,因此实现了对所传入的子类对象的装饰。传入什么东西,不是由装饰器的编写者决定的,而是由装饰器的使用者决定的,所以对于库类的编写者来说,装饰器装饰什么东西(即传入什么参数)是它的代码已经交付之后发生的事了,所以我们说这是运行时决定的(动态库类代码可能确实已经在系统运行了)。
同时,为了扩充装饰类本身,实现多样化装饰,我们可以先派生一个顶层的抽象装饰类,再从这个抽象的装饰类派生一些具体的装饰类。注意,这些派生出来的具体的装饰类具体增加什么新功能,和他们要装饰哪个类是无关的,因为他们要求能够装饰所有的hero而不是某一个hero,而是和他们要增加什么具体功能有关,比如增加R技能还是Q技能。
另外,上面的例子还展示了装饰器类一个强大的功能,就是装饰器对象在构造的时候,可以传入另一个装饰器对象(因为它的顶层父类就是一个hero),从而能够使用另一个装饰类的方法,非常强大。但这里隐含了一个需要注意的地方,那就是装饰器必须实现被装饰类(此处为hero)的所有方法,并在这些方法中一般还要求调用被装饰类的对应方法,一次实现对被修饰类对象数据的操作(对象的数据一般都只能通过它自己的方法接口进行操作)。
因此可以看出,装饰器模式其实是充分地利用了继承多态的优势,让“扩充哪个类”这个决定也交给了运行时多态去确认,可谓灵活至极啊。