对复合(协做)算法/策略的封装方法——装饰模式总结

前言

装饰模式顾名思义就是在不改变原对象的前提下,将新功能优雅的附加(装饰)到该对象上,能够实现对复合算法(策略)的优雅封装、对须要协做的算法(策略)进行有机组合。html

装饰模式和策略模式用法相似,可是也有明显区别——策略模式运行时只能运行一个算法,且其各个算法(算法族)之间必须相互独立,不能有联系,装饰模式没有这些约束。java

装饰模式和策略模式同样,也是对继承的一种替代方案——使用对象组合的方式,作到运行时装饰对象,从而优雅的替代死板的继承。算法

另外,装饰模式是对象的结构型模式。编程

再议继承的缺陷

在策略模式:继承、组合和接口用法——策略模式复习总结,里有一个鸭子的案例,说到了继承的种种局限性,这能够用策略模式(组合+接口)改进。一样,咱们也能够用装饰模式。设计模式

看一个新的例子——作手抓饼的例子,如今要实现一个作手抓饼的点餐程序,通常人都会加一个鸡蛋,或者烤肠,辣条等等,加不一样的料,价格确定也不同,下面看代码:安全

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 类图以下:ide

看似很正常,很完美,可是此时,有新的需求了——加 N 个鸡蛋或者烤肠等。。。此时会发现,该程序已经没法正常的扩展了,若是仍是依样画葫芦的添加新类,经过继承。。。那么必将致使系统的类“爆炸”——成为垃圾代码的典范案例。。。函数

小结

利用继承扩展类的行为,在代码编译期间就决定了行为是什么了,且全部它的子类都要无脑的接收父类的内容。。。这在某些场景下是不太合理的(联系策略模式里鸭子的例子),而使用组合就能够规避这样的问题。

组合能够动态的组合对象,在不改变现有类的基础上,给这个类添加新的行为,既不会对旧代码引入 bug,也能增长新功能,这就是 OCP(open close principle)——开闭原则的体现。

设计原则:开闭原则(OCP)

额外提一句:软件设计在考虑基本原则以前,也要综合考虑 deadline,项目规模,等因素,一句话——屈于现实,有时候容许代码写的不符合设计原则,可是心中要有一条'准线',而不是一直听之任之,甚至随波逐流,无所谓的态度。。。

OCP定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

核心思想:面向抽象编程——用抽象构建框架,用实现扩展细节,其实我以为核心就是一句话,全部的开源框架和设计模式的精髓——加间接层,只要有变化,或者感受别扭,就能够抽象不变的代码,而后加间接层,扩展功能。

OCP 的目的在于面对需求的改变而保持系统的相对稳定,从而使得系统能够很容易的从一个版本升级到另外一个版本。而实际生产环境里,绝对封闭的系统是不存在的。不管模块怎么封闭,到最后总仍是有一些没法封闭的变化。

设计的基本思路

既然不能作到彻底封闭,那就应该只封闭变化的部分:

一、作粗粒度的隔离:把变化的模块和稳定的模块先区分开

二、作细粒度的隔离:在变化的模块里,尝试对逻辑段落进行封装,把那些实在没法封闭的部分抽象出来,即进行细粒度的隔离——好比使用抽象类,接口等

三、要容许扩展,当系统的变化来临时,要及时的作出反应。而不是一味的就知道修改已有的业务代码

矛盾

实际上,变化来的越早,抽象就越容易,代码的维护也就越容易

可是,当项目接近于完成时,才来的一些需求,则会使抽象变得很困难。这个困难并非抽象自己的困难,抽象自己不难,难在系统的架构已经完成,修改牵扯的方面太多而使得抽象工做变得很困难。

这是一个矛盾点

拆分原则

总的来讲:

一、不能惧怕改变——当新需求到来时,首先要作的不是上来就修改已有的业务代码,而是尽量的将变化抽象出来进行隔离,而后再进行扩展。

二、面对需求的变化,对程序的修改应该是尽量经过添加代码来实现,而不是经过修改已有代码来实现。固然,现实中,各类状况一综合,你们也都知道,呵呵了。。。

组合和委托的运用——装饰模式的实现

下面采起一些新的设计思路;以饼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
    }
}

其实也不难,下面是类图:

装饰模式的特色

一、装饰者和被装饰者要有相同的超类型

类型的一致性,经过继承共同父类解决

二、装饰者要把装饰的责任委托给被装饰者,这样处理,装饰者能在委托以前或者以后,加上本身的专属行为,以达到特殊目的

三、经过组合+委托,实现动态的,运行时,不限量的为对象增长新功能

四、保持了接口的透明性——OCP 原则

由于装饰者和被装饰者类型具备一致性,因此,它们的 API 也具备一致性,也就实现了接口的透明性,即不会有继承的弊端——覆盖掉父类方法,具体的被装饰类不论被装饰多少次,其父类(或者接口)的 API 都不会被修改,同时这也体现了递归的思想。

再次强调:装饰模式使用继承,是为了实现类型的一致性,而不是为了扩展类的功能。

装饰模式的适用场景

一、适合替代继承,给类添加新的职责

二、比策略模式更灵活——能够在运行时动态的给一个对象添加新功能,也能够撤销已经添加的新功能,能够同时封装多个算法(策略)去完成一件事。

装饰模式的优势

其实不必说太多了,前面包括策略模式已经说到了不少继承的缺陷,也说到了策略模式的一个缺点——算法族的算法必须独立和平等,装饰模式能够弥补这些缺陷

一、装饰模式比继承灵活,能够在不改变对象的前提下,扩展对象。若是只使用继承扩展类,那么当新功能较多时,会致使类的膨胀和复杂,且子类不必定都要具有父类的这些特性,联系策略模式里鸭子的例子。并且,继承对类的扩展是静态的,在编译时就要肯定,而装饰模式是动态的扩展

二、说到算法族的平等和独立,这是策略模式的局限性,可是装饰模式就能够经过排列组合,实现算法之间的协做和组合,也是题目的意思

不叫缺点的“缺点”

增长了系统的类数量,增长了代码量,代码量多了,天然程序就显得复杂一些,可是我认为,瑕不掩瑜,无所谓,能够说是硬给扣的一些帽子,大胆的用吧,若是有人看不懂,只能说对方编程能力比较水,OO 能力比较差。

JDK 使用装饰模式的例子

I/O 包:源码没什么分析的必要了,很直观

还有对集合的线程安全的包装API,好比java.util 包里的 Collections.synchronizedMap()等;

相关文章
相关标签/搜索