前言
装饰模式顾名思义就是在不改变原对象的前提下,将新功能优雅的附加(装饰)到该对象上,可以实现对复合算法(策略)的优雅封装、对需要协作的算法(策略)进行有机组合。
装饰模式和策略模式用法类似,但是也有明显区别——策略模式运行时只能运行一个算法,且其各个算法(算法族)之间必须相互独立,不能有联系,装饰模式没有这些约束。
装饰模式和策略模式一样,也是对继承的一种替代方案——使用对象组合的方式,做到运行时装饰对象,从而优雅的替代死板的继承。
另外,装饰模式是对象的结构型模式。
再议继承的缺陷
在策略模式:继承、组合和接口用法——策略模式复习总结,里有一个鸭子的案例,说到了继承的种种局限性,这可以用策略模式(组合+接口)改进。同样,我们也可以用装饰模式。
看一个新的例子——做手抓饼的例子,现在要实现一个做手抓饼的点餐程序,一般人都会加一个鸡蛋,或者烤肠,辣条等等,加不同的料,价格肯定也不一样,下面看代码:
public class Cake { // 代表饼 protected String getInfo() { return "这是一个白饼"; } protected double getCost() { return 2.0D; // 卖两元 } }
下面是加了鸡蛋的饼,和加鸡蛋和烤肠的饼
public class CakeWithEgg extends Cake { @Override public String getInfo() { return super.getInfo() + "加1鸡蛋"; } @Override public double getCost() { return super.getCost() + 2; } } ////////////////////////////// public class CakeWithEggSausage extends CakeWithEgg { @Override public String getInfo() { return super.getInfo() + "加1个烤肠"; } @Override public double getCost() { return super.getCost() + 2; } }
客户端调用如下:
public class Main { public static void main(String[] args) { Cake cake = new Cake(); System.out.println(cake.getInfo() + ",价格:" + cake.getCost()); Cake cake1 = new CakeWithEgg(); System.out.println(cake1.getInfo() + ",价格:" + cake1.getCost()); Cake cake2 = new CakeWithEggSausage(); System.out.println(cake2.getInfo() + ",价格:" + cake2.getCost()); } }
很简单,很快就实现好了,UML 类图如下:
看似很正常,很完美,但是此时,有新的需求了——加 N 个鸡蛋或者烤肠等。。。此时会发现,该程序已经无法正常的扩展了,如果还是依样画葫芦的添加新类,通过继承。。。那么必将导致系统的类“爆炸”——成为垃圾代码的典范案例。。。
小结
利用继承扩展类的行为,在代码编译期间就决定了行为是什么了,且所有它的子类都要无脑的接收父类的内容。。。这在某些场景下是不太合理的(联系策略模式里鸭子的例子),而使用组合就可以规避这样的问题。
组合可以动态的组合对象,在不改变现有类的基础上,给这个类添加新的行为,既不会对旧代码引入 bug,也能增加新功能,这就是 OCP(open close principle)——开闭原则的体现。
设计原则:开闭原则(OCP)
额外提一句:软件设计在考虑基本原则之前,也要综合考虑 deadline,项目规模,等因素,一句话——屈于现实,有时候允许代码写的不符合设计原则,但是心中要有一条‘准线‘,而不是一直听之任之,甚至随波逐流,无所谓的态度。。。
OCP定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
核心思想:面向抽象编程——用抽象构建框架,用实现扩展细节,其实我觉得核心就是一句话,所有的开源框架和设计模式的精髓——加间接层,只要有变化,或者感觉别扭,就可以抽象不变的代码,然后加间接层,扩展功能。
OCP 的目的在于面对需求的改变而保持系统的相对稳定,从而使得系统可以很容易的从一个版本升级到另一个版本。而实际生产环境里,绝对封闭的系统是不存在的。无论模块怎么封闭,到最后总还是有一些无法封闭的变化。
设计的基本思路
既然不能做到完全封闭,那就应该只封闭变化的部分:
1、做粗粒度的隔离:把变化的模块和稳定的模块先区分开
2、做细粒度的隔离:在变化的模块里,尝试对逻辑段落进行封装,把那些实在无法封闭的部分抽象出来,即进行细粒度的隔离——比如使用抽象类,接口等
3、要允许扩展,当系统的变化来临时,要及时的做出反应。而不是一味的就知道修改已有的业务代码
矛盾
实际上,变化来的越早,抽象就越容易,代码的维护也就越容易
但是,当项目接近于完成时,才来的一些需求,则会使抽象变得很困难。这个困难并不是抽象本身的困难,抽象本身不难,难在系统的架构已经完成,修改牵扯的方面太多而使得抽象工作变得很困难。
这是一个矛盾点
拆分原则
总的来说:
1、不能害怕改变——当新需求到来时,首先要做的不是上来就修改已有的业务代码,而是尽可能的将变化抽象出来进行隔离,然后再进行扩展。
2、面对需求的变化,对程序的修改应该是尽可能通过添加代码来实现,而不是通过修改已有代码来实现。当然,现实中,各种情况一综合,大家也都知道,呵呵了。。。
组合和委托的运用——装饰模式的实现
下面采取一些新的设计思路;以饼Cake为主体,我们认为它是被装饰的实体,现在要实现,在运行时动态的用鸡蛋,烤肠,辣条等装饰者,去装饰饼 cake。全程只 new 一个饼即可,如果客户想要一个加鸡蛋的饼,就用鸡蛋去装饰这个饼,如果想要 N 个鸡蛋,就用 N 个鸡蛋去装饰这个饼,烤肠等同理。最后再调用 cost,info 等方法,将饼的信息和价钱算出来。
其实,装饰理解为包装更合适。联系 Java 的 I/O API,其实就是这么用的,各种 buffer 的流去包装字节,字符流。
如此一来,我们可以把看似不同的东西,饼和鸡蛋不同,看做同一种东西来使用,如何实现呢?自然就用到了组合+委托的设计思想。
既然饼和鸡蛋两个不相干的东西能被当做一个东西来用,它们肯定都要继承同一个类型:手抓饼是具体的被装饰的类——Cake,我们可以给它设计一个抽象的父类,只代表饼——DefaultCake,而鸡蛋是一个具体的装饰类——EggDecorator,它要有一个抽象的(或者非抽象)父类——装饰者类 Decorator。同时我们让装饰者类 Decorator 也继承 DefaultCake 即可。
下面看代码:
public abstract class DefaultCake { // 代表饼——抽象的饼类,也就是抽象的被装饰类 // 注意,该类也可以用接口实现,且这个角色不是必须的,也可以直接就是具体的被装饰类 protected abstract String getInfo(); protected abstract double getCost(); } ////////////////////////////////////////// public class Cake2 extends DefaultCake { // 具体的被装饰类 @Override protected String getInfo() { return "这是一个白饼"; } @Override protected double getCost() { return 2.0D; } }
如上能解决两个类的类型一致问题,下面让两个类关联起来——使用组合+委托,我们目的是让装饰者——EggDecorator去装饰被装饰的类——Cake,可以把抽象的被装饰类DefaultCake组合到装饰者Decorator类里,来关联装饰类和被装饰类,然后联系策略模式,通过构造器或者 setter 方法,把抽象的被装饰类DefaultCake注入进装饰类Decorator——这就叫委托,即装饰类Decorator将新的功能,委托给被装饰类DefaultCake去实现。
public class Decorator extends DefaultCake { // 装饰类的父类。这个类也可以设计为抽象的,前提装饰类要额外做一些事情的时候 private DefaultCake defaultCake; // 通过组合抽象的被装饰父类,来关联装饰类和具体的被装饰类 public Decorator(DefaultCake defaultCake) { // 通过构造器注入 // 这样可以把被装饰类DefaultCake传入装饰类Decorator——这也叫委托,即装饰类Decorator将装饰的功能,委托给被装饰类DefaultCake去实现 this.defaultCake = defaultCake; } @Override protected String getInfo() { return this.defaultCake.getInfo(); } @Override protected double getCost() { return this.defaultCake.getCost(); } } ////////////////////////////// public class EggDecorator extends Decorator { // 具体的装饰类——鸡蛋 public EggDecorator(DefaultCake defaultCake) { // 这里必须要实现有参构造器,因为父类写了有参构造器 super(defaultCake); } @Override protected String getInfo() { return super.getInfo() + " 加1个鸡蛋"; } @Override protected double getCost() { return super.getCost() + 2; } } ///////////////////////////// public class SausageDcorator extends Decorator { // 具体的装饰类——烤肠 public SausageDcorator(DefaultCake defaultCake) { super(defaultCake); } @Override protected String getInfo() { return super.getInfo() + " 加1个烤肠"; } @Override protected double getCost() { return super.getCost() + 2; } }
客户端调用:
public class Main2 { public static void main(String[] args) { DefaultCake cake = new Cake2(); cake = new EggDecorator(cake); // 用鸡蛋这个具体的装饰者去包装被装饰者——饼cake,看起来很像 I/O API cake = new EggDecorator(cake); // 再加一个鸡蛋 cake = new EggDecorator(cake); // 理论上可以加 N 个,但是我们只用一个类就能解决这个需求 cake = new SausageDcorator(cake); // 再加一个烤肠 System.out.println(cake.getInfo() + ",价格:" + cake.getCost()); // 这是一个白饼 加1个鸡蛋 加1个鸡蛋 加1个鸡蛋 加1个烤肠,价格:10.0 } }
其实也不难,下面是类图:
装饰模式的特点
1、装饰者和被装饰者要有相同的超类型
类型的一致性,通过继承共同父类解决
2、装饰者要把装饰的责任委托给被装饰者,这样处理,装饰者能在委托之前或者之后,加上自己的专属行为,以达到特殊目的
3、通过组合+委托,实现动态的,运行时,不限量的为对象增加新功能
4、保持了接口的透明性——OCP 原则
因为装饰者和被装饰者类型具有一致性,所以,它们的 API 也具有一致性,也就实现了接口的透明性,即不会有继承的弊端——覆盖掉父类方法,具体的被装饰类不论被装饰多少次,其父类(或者接口)的 API 都不会被修改,同时这也体现了递归的思想。
再次强调:装饰模式使用继承,是为了实现类型的一致性,而不是为了扩展类的功能。
装饰模式的适用场景
1、适合替代继承,给类添加新的职责
2、比策略模式更灵活——可以在运行时动态的给一个对象添加新功能,也可以撤销已经添加的新功能,可以同时封装多个算法(策略)去完成一件事。
装饰模式的优点
其实没必要说太多了,前面包括策略模式已经说到了很多继承的缺陷,也说到了策略模式的一个缺点——算法族的算法必须独立和平等,装饰模式可以弥补这些缺陷
1、装饰模式比继承灵活,可以在不改变对象的前提下,扩展对象。如果只使用继承扩展类,那么当新功能较多时,会导致类的膨胀和复杂,且子类不一定都要具备父类的这些特性,联系策略模式里鸭子的例子。而且,继承对类的扩展是静态的,在编译时就要确定,而装饰模式是动态的扩展
2、说到算法族的平等和独立,这是策略模式的局限性,但是装饰模式就可以通过排列组合,实现算法之间的协作和组合,也是题目的意思
不叫缺点的“缺点”
增加了系统的类数量,增加了代码量,代码量多了,自然程序就显得复杂一些,但是我认为,瑕不掩瑜,无所谓,可以说是硬给扣的一些帽子,大胆的用吧,如果有人看不懂,只能说对方编程能力比较水,OO 能力比较差。
JDK 使用装饰模式的例子
I/O 包:源码没什么分析的必要了,很直观
还有对集合的线程安全的包装API,比如java.util 包里的 Collections.synchronizedMap()等;
原文地址:https://www.cnblogs.com/kubixuesheng/p/10344505.html