装饰模式

Published on 2016 - 07 - 14

罪恶的成绩单

“中庸”是中国儒教文化的集中体现,说话或做事情都不能太直接,需要有技巧。比如谈话,如果你要批评某个人,你不能一上来就说他这做得不对,那也做得不对,你要先肯定他的成绩,表扬一下优点;然后再指出不足,指出错误的地方,最后再来点激励,你修改了这些缺点后有哪些好处,比如你能带更多的小兵、升职等。如果你一上来就是一顿批评,你瞅瞅看,对方肯定是不服气,甚至是顶撞你说:“此处不养爷,自有养爷处”,于是甩门而去。

这是说话,做事情也是一样。在山寨产品流行之前,假货也是比较“盛行”的。本人2002年买了一部手机,当时老板吹得天花乱坠,承诺这部手机是最新的,我看着也像,壳子是崭新的,包装是崭新的,没有任何瑕疵,就是比正品便宜了很多,于是我买了,因为缺钱啊!用了3个月,坏了,送修检查,结果诊断出这是新壳装旧机,我晕!拿一部旧手机的线路板,找个新的外壳、屏幕、包装就成了新手机,害人不浅啊!

我们不说不开心的事情,今天以什么例子为开场白呢?就说说我上小学的糗事吧。我上小学的时候学习成绩非常差,班级上有40多个同学,我基本上都是排在45名以后,按照老师给我的评价就是:“不是读书的料”。但是我父亲管得很严格,明知道我不是这块料,还是“赶鸭子上架”,每次考完试我都战战兢兢,“竹笋炒肉”是肯定少不了的,但是能少点就少点吧,因为肉可是自己的。四年级期末考试考完,学校出来个很损的招儿(这招儿现在很流行的),打印出成绩单,要家长签字,然后才能上五年级,我那个恐惧呀,不过也就是几秒钟的时间,玩起来什么都忘记了。我们做架构,做设计,任何值得我们回忆的事件都可以通过设计记录下来。当然了,这份成绩单的事情也是可以通过类图表示的,如图1所示。

成绩单的抽象类,然后有一个四年级的成绩单实现类,So Easy,我们先来看抽象类,如代码清单1所示。

public abstract class SchoolReport {
     //成绩单主要展示的就是你的成绩情况
     public abstract void report();     
     //成绩单要家长签字,这个是最要命的
     public abstract void sign();
}

有抽象类了,我们再来看看具体的四年级成绩单FouthGradeSchoolReport,如代码清单2所示。

public class FouthGradeSchoolReport extends SchoolReport {
     //我的成绩单
     public void report() {
             //成绩单的格式是这个样子的
             System.out.println("尊敬的XXX家长:");  
             System.out.println("  ......");
             System.out.println("  语文 62  数学65 体育 98  自然  63");
             System.out.println("  .......");
            System.out.println("               家长签名:       ");
     }
     //家长签名
     public void sign(String name) {
             System.out.println("家长签名为:"+name);
     }
}

成绩单出来,你别看什么62、65之类的成绩,你要知道,在小学低于90分基本上就是中下等了,悲哀呀,爱学习的人咋就那么多!怎么着,那我把这个成绩单给老爸看看?好,我们修改一下类图,成绩单给老爸看,如图2所示。

老爸开始看成绩单,这个成绩单可是最真实的,啥都没有动过,原装,Father类如代码清单3所示。

public class Father {
     public static void main(String[] args) {
             //把成绩单拿过来
             SchoolReport sr = new FouthGradeSchoolReport();
             //看成绩单
             sr.report();
             //签名?休想!
     }
}

运行结果如下:

尊敬的XXX家长:
......
语文 62 数学65 体育 98 自然 63
.......

       家长签名:

就这成绩还要我签字?!老爸就开始找扫帚,我开始做准备:深呼吸,绷紧肌肉,提臀收腹。哈哈,幸运的是,这个不是当时的真实情况,我没有直接把成绩单交给老爸,而是在交给他之前做了点技术工作,我要把成绩单封装一下,封装分类两步来实现,如下所示。

  • 汇报最高成绩

跟老爸说各个科目的最高分,语文最高是75,数学是78,自然是80,然后老爸觉得我的成绩与最高分数相差不多,考的还是不错的嘛!这个是实情,但是不知道是什么原因,反正期末考试都考得不怎么样,但是基本上都集中在70分以上,我这60多分基本上还是垫底的角色。

  • 汇报排名情况

在老爸看完成绩单后,告诉他我在全班排第38名,这个也是实情,为啥呢?有将近十个同学退学了!这个情况我是不会说的。不知道是不是当时第一次发成绩单时学校没有考虑清楚,没有写上总共有多少同学,排第几名,反正是被我钻了个空子。

那修饰是说完了,我们看看类图如何修改,如图3所示。

我想这是大家最容易想到的类图,通过直接增加了一个子类,重写report方法,很容易地解决了这个问题,是不是这样?是的,这确实是一个比较好的办法,我们来看具体的实现,如代码清单4所示。

public class SugarFouthGradeSchoolReport extends FouthGradeSchoolReport {
     //首先要定义你要美化的方法,先给老爸说学校最高成绩
     private void reportHighScore(){
             System.out.println("这次考试语文最高是75,数学是78,自然是80");
     }
     //在老爸看完毕成绩单后,我再汇报学校的排名情况
     private void reportSort(){
             System.out.println("我是排名第38名...");
     }
     //由于汇报的内容已经发生变更,那所以要重写父类
     @Override
     public void report(){
             this.reportHighScore();  //先说最高成绩
             super.report();  //然后老爸看成绩单
             this.reportSort(); //然后告诉老爸学习学校排名
     }
}

然后对Father类稍做修改就可以看到美化后的成绩单,如代码清单5所示。

public class Father {
     public static void main(String[] args) {
             //把美化过的成绩单拿过来
             SchoolReport sr= new SugarFouthGradeSchoolReport();
             //看成绩单
             sr.report();
             //然后老爸,一看,很开心,就签名了
             sr.sign("老三");  //我叫小三,老爸当然叫老三
     }
}

运行结果如下所示:

这次考试语文最高是75,数学是78,自然是80
尊敬的XXX家长:
......
语文 62 数学65 体育 98 自然 63
.......
        家长签名:
我是排名第38名...
家长签名为:老三

通过继承确实能够解决这个问题,老爸看成绩单很开心,然后就给签字了,但现实的情况是很复杂的,可能老爸听我汇报最高成绩后,就直接乐开花了,直接签名了,后面的排名就没必要看了,或者老爸要先看排名情况,那怎么办?继续扩展?你能扩展多少个类?这还是一个比较简单的场景,一旦需要装饰的条件非常多,比如20个,你还通过继承来解决,你想象的子类有多少个?你是不是马上就要崩溃了!

好,你也看到通过继承情况确实出现了问题,类爆炸,类的数量激增,光写这些类不累死你才怪,而且还要想想以后维护怎么办,谁愿意接收这么一大摊本质相似的代码维护工作?并且在面向对象的设计中,如果超过两层继承,你就应该想想是不是出设计问题了,是不是应该重新找一条康庄大道了,这是经验值,不是什么绝对的,继承层次越多以后的维护成本越多,问题这么多,那怎么办?好办,我们定义一批专门负责装饰的类,然后根据实际情况来决定是否需要进行装饰,类图稍做修正,如图4所示。

增加一个抽象类和两个实现类,其中Decorator的作用是封装SchoolReport类,如果大家还记得代理模式,那么很容易看懂这个类图,装饰类的作用也就是一个特殊的代理类,真实的执行者还是被代理的角色FouthGradeSchoolReport,如代码清单6所示。

public abstract class Decorator extends SchoolReport{
     //首先我要知道是哪个成绩单
     private SchoolReport sr;
     //构造函数,传递成绩单过来
     public Decorator(SchoolReport sr){
             this.sr = sr;
     }
     //成绩单还是要被看到的
     public void report(){
             this.sr.report();
     }
     //看完还是要签名的
     public void sign(String name){
             this.sr.sign(name);
     }
}

看到没,装饰类还是把动作的执行委托给需要装饰的对象,Decorator抽象类的目的很简单,就是要让子类来封装SchoolReport的子类,怎么封装?重写report方法!先看HighScoreDecorator实现类,如代码清单7所示。

public class HighScoreDecorator extends Decorator {
     //构造函数
     public HighScoreDecorator(SchoolReport sr){
             super(sr);
     }
     //我要汇报最高成绩
     private void reportHighScore(){
             System.out.println("这次考试语文最高是75,数学是78,自然是80");
     }
     //我要在老爸看成绩单前告诉他最高成绩,否则等他一看,就抡起扫帚揍我,我哪里还有机会说啊
     @Override
     public void report(){
             this.reportHighScore();
             super.report();
     }
}

重写了report方法,先调用具体装饰类的装饰方法reportHighScore,然后再调用具体构件的方法,我们再来看怎么汇报学校排序情况SortDecorator代码,如代码清单8所示。

public class SortDecorator extends Decorator {
     //构造函数
     public SortDecorator(SchoolReport sr){
             super(sr);
     }
     //告诉老爸学校的排名情况
     private void reportSort(){
             System.out.println("我是排名第38名...");
     }  
     //老爸看完成绩单后再告诉他,加强作用
     @Override
     public void report(){
             super.report();
             this.reportSort();
     }
}

我准备好了这两个强力的修饰工具,然后就“毫不畏惧”地把成绩单交给老爸,看看老爸怎么看成绩单的,如代码清单9所示。

public class Father {
     public static void main(String[] args) {
             //把成绩单拿过来
             SchoolReport sr;
             //原装的成绩单
             sr = new FouthGradeSchoolReport();
             //加了最高分说明的成绩单
            sr = new HighScoreDecorator(sr);
             //又加了成绩排名的说明
             sr = new SortDecorator(sr);
             //看成绩单
             sr.report();
             //然后老爸一看,很开心,就签名了
             sr.sign("老三");  //我叫小三,老爸当然叫老三
     }
}

老爸一看成绩单,听我这么一说,非常开心,儿子有进步呀,从40多名进步到30多名,进步很大,躲过了一顿海扁。想想看,如果我还要增加其他的修饰条件,是不是就非常容易了,只要实现Decorator类就可以了!这就是装饰模式。

装饰模式的定义

装饰模式(Decorator Pattern)是一种比较常见的模式,其定义如下:Attach additional responsibilities to an object dynamically keeping the same interface.Decorators provide a flexible alternative to subclassing for extending functionality.(动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更为灵活。)

装饰模式的通用类图如图5所示。

在类图中,有四个角色需要说明:

  • Component抽象构件

Component是一个接口或者是抽象类,就是定义我们最核心的对象,也就是最原始的对象,如上面的成绩单。

注意 在装饰模式中,必然有一个最基本、最核心、最原始的接口或抽象类充当Component抽象构件。

  • ConcreteComponent 具体构件

ConcreteComponent是最核心、最原始、最基本的接口或抽象类的实现,你要装饰的就是它。

  • Decorator装饰角色

一般是一个抽象类,做什么用呢?实现接口或者抽象方法,它里面可不一定有抽象的方法呀,在它的属性里必然有一个private变量指向Component抽象构件。

  • 具体装饰角色

ConcreteDecoratorA和ConcreteDecoratorB是两个具体的装饰类,你要把你最核心的、最原始的、最基本的东西装饰成其他东西,上面的例子就是把一个比较平庸的成绩单装饰成家长认可的成绩单。

装饰模式的所有角色都已经解释完毕,我们来看看如何实现,先看抽象构件,如代码清单10所示。

public abstract class Component {
     //抽象的方法
     public abstract void operate();
}

具体构件如代码清单11所示。

public class ConcreteComponent extends Component {
     //具体实现
     @Override
     public void operate() {
             System.out.println("do Something");
     }
}

装饰角色通常是一个抽象类,如代码清单12所示。

public abstract class Decorator extends Component {
     private Component component = null;        
     //通过构造函数传递被修饰者
     public Decorator(Component _component){
             this.component = _component;
     }
     //委托给被修饰者执行
     @Override
     public void operate() {
             this.component.operate();
     }
}

当然了,若只有一个装饰类,则可以没有抽象装饰角色,直接实现具体的装饰角色即可。具体的装饰类如代码清单13所示。

public class ConcreteDecorator1 extends Decorator {
     //定义被修饰者
     public ConcreteDecorator1(Component _component){
             super(_component);
     }
     //定义自己的修饰方法
     private void method1(){
             System.out.println("method1 修饰");
     }
     //重写父类的Operation方法
     public void operate(){
             this.method1();
             super.operate();
     }
}
public class ConcreteDecorator2 extends Decorator {
     //定义被修饰者
     public ConcreteDecorator2(Component _component){
             super(_component);
     }
     //定义自己的修饰方法
     private void method2(){
             System.out.println("method2修饰");
     }
     //重写父类的Operation方法
     public void operate(){
             super.operate();
             this.method2();
     }
}

注意 原始方法和装饰方法的执行顺序在具体的装饰类是固定的,可以通过方法重载实现多种执行顺序。

我们通过Client类来模拟高层模块的耦合关系,看看装饰模式是如何运行的,如代码清单14所示。

public class Client {
     public static void main(String[] args) {
             Component component = new ConcreteComponent();
             //第一次修饰
             component = new ConcreteDecorator1(component);
             //第二次修饰
             component = new ConcreteDecorator2(component);
             //修饰后运行
             component.operate();
     }
}

装饰模式应用

装饰模式的优点

  • 装饰类和被装饰类可以独立发展,而不会相互耦合。换句话说,Component类无须知道Decorator类,Decorator类是从外部来扩展Component类的功能,而Decorator也不用知道具体的构件。
  • 装饰模式是继承关系的一个替代方案。我们看装饰类Decorator,不管装饰多少层,返回的对象还是Component,实现的还是is-a的关系。
  • 装饰模式可以动态地扩展一个实现类的功能,这不需要多说,装饰模式的定义就是如此。

装饰模式的缺点

对于装饰模式记住一点就足够了:多层的装饰是比较复杂的。为什么会复杂呢?你想想看,就像剥洋葱一样,你剥到了最后才发现是最里层的装饰出现了问题,想象一下工作量吧,因此,尽量减少装饰类的数量,以便降低系统的复杂度。

装饰模式的使用场景

  • 需要扩展一个类的功能,或给一个类增加附加功能。
  • 需要动态地给一个对象增加功能,这些功能可以再动态地撤销。
  • 需要为一批的兄弟类进行改装或加装功能,当然是首选装饰模式。

最佳实践

装饰模式是对继承的有力补充。你要知道继承不是万能的,继承可以解决实际的问题,但是在项目中你要考虑诸如易维护、易扩展、易复用等,而且在一些情况下(比如上面那个成绩单例子)你要是用继承就会增加很多子类,而且灵活性非常差,那当然维护也不容易了,也就是说装饰模式可以替代继承,解决我们类膨胀的问题。同时,你还要知道继承是静态地给类增加功能,而装饰模式则是动态地增加功能,在上面的那个例子中,我不想要SortDecorator这层的封装也很简单,于是直接在Father中去掉就可以了,如果你用继承就必须修改程序。

装饰模式还有一个非常好的优点:扩展性非常好。在一个项目中,你会有非常多的因素考虑不到,特别是业务的变更,不时地冒出一个需求,尤其是提出一个令项目大量延迟的需求时,那种心情是相当的难受!装饰模式可以给我们很好的帮助,通过装饰模式重新封装一个类,而不是通过继承来完成,简单点说,三个继承关系Father、Son、GrandSon三个类,我要在Son类上增强一些功能怎么办?我想你会坚决地顶回去!不允许,对了,为什么呢?你增强的功能是修改Son类中的方法吗?增加方法吗?对GrandSon的影响呢?特别是GrandSon有多个的情况,你会怎么办?这个评估的工作量就够你受的,所以这是不允许的,那还是要解决问题的呀,怎么办?通过建立SonDecorator类来修饰Son,相当于创建了一个新的类,这个对原有程序没有变更,通过扩展很好地完成了这次变更。

参考文档