打赏

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

前言

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

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

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

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

再议继承的缺陷

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

看一个新的例子——做手抓饼的例子,现在要实现一个做手抓饼的点餐程序,一般人都会加一个鸡蛋,或者烤肠,辣条等等,加不同的料,价格肯定也不一样,下面看代码:

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()等;

欢迎关注

dashuai的博客是终身学习践行者,大厂程序员,且专注于工作经验、学习笔记的分享和日常吐槽,包括但不限于互联网行业,附带分享一些PDF电子书,资料,帮忙内推,欢迎拍砖!

 

posted @ 2019-02-02 00:57  dashuai的博客  阅读(685)  评论(0编辑  收藏  举报
Flag Counter欢迎关注微信公众号