设计模式之装饰者模式

定义

在不改变原有对象的基础上,将功能附加到原有功能上,进行功能的扩展,
动态地给一个对象增加一些额外的功能,装饰模式比生成子类(继承)实现更为灵活。
在装饰者模式中,为了让系统具有更好的灵活性和可扩展性,我们通常会定义一个抽象装饰类,而将具体的装饰类作为它的子类

类型

结构型

UML图

装饰者(Decorator)和具体组件(ConcreteComponent)都继承自组件(Component),具体组件的方法实现不需要依赖于其它对象,而装饰者组合了一个组件,这样它可以装饰其它装饰者或者具体组件。所谓装饰,就是把这个装饰者套在被装饰者之上,从而动态扩展被装饰者的功能。装饰者的方法有一部分是自己的,这属于它的功能,然后调用被装饰者的方法实现,从而也保留了被装饰者的功能。可以看到,具体组件应当是装饰层次的最低层,因为只有具体组件的方法实现不需要依赖于其它对象。
在这里插入图片描述

角色

Component(抽象组件):它是具体组件和抽象装饰类的共同父类,声明了在具体组件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。

ConcreteComponent(具体组件):它是抽象组件类的子类,用于定义具体的组件对象,实现了在抽象组件中声明的方法,装饰器可以给它增加额外的职责(方法)。

Decorator(抽象装饰类):它也是抽象组件类的子类,用于给具体组件增加职责,但是具体职责在其子类中实现。它维护一个指向抽象组件对象的引用,通过该引用可以调用装饰之前组件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。 对于Component(抽象组件)来说,是无需了解Decorator(抽象装饰类)的存在的,Component(抽象组件)只顾做好自己份内的事就行。

ConcreteDecorator(具体装饰类):它是抽象装饰类的子类,负责向组件添加新的职责。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。

由于具体组件类和装饰类都实现了相同的抽象组件接口,因此装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。

装饰模式的核心在于抽象装饰类的设计。

示例:

version1:通过继承给一个类添加功能

煎饼类(实现基本的功能)

public class Battercake {

    protected String getDesc(){
        return "煎饼";
    }
    protected int cost(){
        return 8;
    }

}

加鸡蛋的煎饼类(通过继承给煎饼类添加鸡蛋)

public class BattercakeWithEgg extends Battercake {
    @Override
    public String getDesc() {
        return super.getDesc()+" 加一个鸡蛋";
    }

    @Override
    public int cost() {
        return super.cost()+1;
    }

}

加鸡蛋和香肠的煎饼类(使用继承继续添加新功能)

public class BattercakeWithEggSausage extends BattercakeWithEgg {

    @Override
    public String getDesc() {
        return super.getDesc()+ " 加一根香肠";
    }

    @Override
    public int cost() {
        return super.cost()+2;
    }
}

Test类

public class Test {

    public static void main(String[] args) {
        Battercake battercake = new Battercake();
        System.out.println(battercake.getDesc()+" 销售价格:"+battercake.cost());

        Battercake battercakeWithEgg = new BattercakeWithEgg();
        System.out.println(battercakeWithEgg.getDesc()+" 销售价格:"+battercakeWithEgg.cost());

        Battercake battercakeWithEggSausage = new BattercakeWithEggSausage();
        System.out.println(battercakeWithEggSausage.getDesc()+" 销售价格:"+battercakeWithEggSausage.cost());
    }
}

在这里插入图片描述

version2:通过装饰者模式

抽象组件(抽象煎饼类)

public abstract class ABattercake {

    protected abstract String getDesc();
    protected abstract int cost();

}

具体组件(煎饼类)

public class Battercake extends ABattercake {
    @Override
    protected String getDesc() {
        return "煎饼";
    }

    @Override
    protected int cost() {
        return 8;
    }
}

抽象装饰类,抽象装饰类通过成员属性的方式将煎饼抽象类组合进来,同时也继承了煎饼抽象类,且这里添加了新功能 doSomething()

public abstract class AbstractDecorator extends ABattercake {

    private ABattercake aBattercake;

    public AbstractDecorator(ABattercake aBattercake) {
        this.aBattercake = aBattercake;
    }

    protected abstract void doSomething();

    @Override
    protected String getDesc() {
        return this.aBattercake.getDesc();
    }
    @Override
    protected int cost() {
        return this.aBattercake.cost();
    }
}

鸡蛋装饰器,继承了抽象装饰类,鸡蛋装饰器在父类的基础上增加了一个鸡蛋,同时价格加上 1 块钱

public class EggDecorator extends AbstractDecorator {

    public EggDecorator(ABattercake aBattercake) {
        super(aBattercake);
    }

    @Override
    protected void doSomething() {
        System.out.println("准备好,我要添加鸡蛋了");
    }

    @Override
    protected String getDesc() {
        return super.getDesc()+" 加一个鸡蛋";
    }

    @Override
    protected int cost() {
        return super.cost()+1;
    }


    protected void dance(){
        System.out.println("看我一边跳舞,一边加鸡蛋");
    }


}

香肠装饰器,与鸡蛋装饰器类似,继承了抽象装饰类,给在父类的基础上加上一根香肠,同时价格增加 2 块钱

public class SausageDecorator extends AbstractDecorator {
    public SausageDecorator(ABattercake aBattercake) {
        super(aBattercake);
    }

    @Override
    protected void doSomething() {
        System.out.println("准备好了没有,我要开始加香肠了");
    }

    @Override
    protected String getDesc() {
        return super.getDesc()+" 加一根香肠";
    }

    @Override
    protected int cost() {
        return super.cost()+2;
    }
    protected void sing(){
        System.out.println("are you ok? 我要开始加香肠了,烤面筋啊我的烤面筋,搞错了,再来");
    }
}

测试Test

public class Test {
    public static void main(String[] args) {
        ABattercake aBattercake;
        aBattercake = new Battercake();
        System.out.println(aBattercake.getDesc()+" 销售价格:"+aBattercake.cost());

        aBattercake = new EggDecorator(aBattercake);
        System.out.println(aBattercake.getDesc()+" 销售价格:"+aBattercake.cost());

//        System.out.println(aBattercake.doSomething());
//        System.out.println(aBattercake.dance());

        aBattercake = new SausageDecorator(aBattercake);
        System.out.println(aBattercake.getDesc()+" 销售价格:"+aBattercake.cost());

        //半透明装饰模式
        EggDecorator eggDecorator = new EggDecorator(aBattercake);
        eggDecorator.doSomething();
        eggDecorator.dance();

        SausageDecorator sausageDecorator = new SausageDecorator(aBattercake);
        sausageDecorator.doSomething();
        sausageDecorator.sing();

//        AbstractDecorator abstractDecorator = new EggDecorator(aBattercake);
//        abstractDecorator.doSomething();
//        abstractDecorator.dance();

    }
}

煎饼类对象还是那个煎饼类对象,但是会有装饰类对象来给他增加新东西,并且各个装饰类对象的装饰并没有先后关系,都是直接装饰原始煎饼类对象。多个装饰如果有先后依赖关系(比如存储数据之前,做一下数据的过滤和加密。如果先加密了数据,那就不好过滤数据了,要先过滤数据,再对数据进行加密),那就要注意装饰顺序了。不过最好将装饰的东西之间设计成独立的,没有先后依赖关系,能以任何顺序进行组合。
在这里插入图片描述

透明装饰模式和半透明装饰模式

1.透明装饰模式

在透明装饰模式中要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该将对象声明为具体组件类型或具体装饰类型,而应该全部声明为抽象组件类型。对客户端而言,具体组件类和具体装饰类对象没有任何区别。(缺点,无法单独调用装饰类的独有功能)

上面的例子就是透明装饰模式,装饰后的对象都是通过抽象组间类类型 ABattercake 的变量来引用的,然后通过组合方式注入到其他具体装饰对象实例里面,但是这样的话就只能调用抽象组间类里面已经定义的方法(具体装饰类针对这些定义的方法做了一些装饰)

ABattercake aBattercake;
aBattercake = new EggDecorator(aBattercake);
2.半透明装饰模式

用具体装饰类型来定义装饰后的对象,而具体组件类型仍然可以使用抽象组件类型来定义,可以单独调用装饰的独有方法。(缺点:无法多次进行装饰)

透明装饰模式的设计难度较大,而且有时我们需要单独调用新增的业务方法。为了能够调用到新增方法,我们不得不用具体装饰类型来定义装饰之后的对象,而具体组件类型还是可以使用抽象组件类型来定义,这种装饰模式即为半透明装饰模式。

半透明装饰模式可以给系统带来更多的灵活性,设计相对简单,使用起来也非常方便;但是其最大的缺点在于不能实现对同一个对象的多次装饰,而且客户端需要有区别地对待装饰之前的对象和装饰之后的对象。

ABattercake aBattercake;
EggDecorator eggDecorator = new EggDecorator(aBattercake);
eggDecorator.doSomething();
eggDecorator.dance();

SausageDecorator sausageDecorator = new SausageDecorator(aBattercake);
sausageDecorator.doSomething();
sausageDecorator.sing();

在这里插入图片描述

装饰者模式总结

优点

  • 通过使用不同装饰类以及这些类的排列组合,可以实现不同的效果。
  • 装饰者模式和继承关系的目的都是要扩展原有对象的功能,但是装饰者模式可以提供比继承更多的灵活型,不会导致类的个数急剧增加
  • 类应该对扩展开放,对修改关闭(符合开闭原则): 具体组件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体组件类和具体装饰类,原有类库代码无须改变。
  • 具体组件类的职责更加清晰简洁,有效的把类的核心职责和装饰功能区分开,不会产生很多冗余的功能,简化了原有的类,只有在需要某功能的时候,才由装饰者装饰添加功能。并且这样也能去除相关类中重复的装饰逻辑

缺点

  • 更多的代码,更多的类,增加程序的复杂性。
  • 动态装饰时、多层装饰时会使系统更复杂。
  • 越灵活也意味着更容易出错。装饰模式提供了一种比继承更加灵活机动的解决方案,但同时也意味着比继承更加易于出错,排错也更困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐
  • 使用装饰模式进行系统设计时将产生很多小对象(大对象包小对象,通过组合的形式将小对象注入到大对象里面),大量小对象的产生势必会占用更多的系统资源,在一定程序上影响程序的性能。

适用场景

  • 扩展一个类的功能或者给一个类添加附加职责
  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责
  • 需要动态地给一个对象增加功能,这些功能也可以动态地被撤销
  • 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类已定义为不能被继承(如Java语言中的final类)。

相关设计模式比较

  • 装饰者模式和代理模式: 装饰者模式关注的是对象功能的动态添加。而代理模式关注的是对对象的控制访问,对它的用户隐藏对象的具体信息。
  • 装饰者模式和适配器模式:装饰者模式和被装饰的类要实现同一个接口,或者装饰类是被装饰的类的子类。 适配器模式和被适配的类具有不同的接口。

应用:

Java I/O中的装饰者模式

在这里插入图片描述
由上图可知在Java中应用程序通过输入流(InputStream)的Read方法从源地址处读取字节,然后通过输出流(OutputStream)的Write方法将流写入到目的地址。流的来源主要有三种:
①本地的文件(File)
②控制台输入
③通过socket实现的网络通信

我们可以通过其他博主的图片看一看java IO的装饰者模式,
下面的图可以看出Java中的装饰者类和被装饰者类以及它们之间的关系,这里只列出了InputStream中的关系:
在这里插入图片描述
由上图可以看出只要继承了FilterInputStream的类就是装饰者类,可以用于包装其他的流,装饰者类还可以对装饰者和类进行再包装

如下代码是使用的半透明装饰模式

public class StreanDemo {

    public static void main(String[] args) throws IOException {

        DataInputStream in=new DataInputStream(new BufferedInputStream(new FileInputStream("D:\\JAVAworkspace\\ProgramTest\\src\\StreamDemo.java")));
        while(in.available()!=0)
        {
            System.out.print((char)in.readByte());
        }
        in.close();
    }

}

上面程序中对流进行了两次包装,先用 BufferedInputStream将FileInputStream包装成缓冲流也就是给FileInputStream增加缓冲功能,再DataInputStream进一步包装方便数据处理。

自定义装饰类
同理,你可以继承抽象装饰类,来实现自己定义的具体装饰类
如下代码:将输入流中的所有大写字母变成小写字母

public class LowerCaseInputStream extends FilterInputStream {

    protected LowerCaseInputStream(InputStream in) {
        super(in);
    }

    @Override
    public int read() throws IOException {
        int c=super.read();

        return (c==-1?c:Character.toLowerCase(c));
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        int result=super.read(b, off, len);
        for(int i=off;i<off+result;i++)
        {
            b[i]=(byte)Character.toUpperCase((char)b[i]);
        }
        return  result;
    }

    public static void main(String[] args) throws IOException {
        int c;
        try (InputStream in = new LowerCaseInputStream(new FileInputStream(文件路径))) {
            try {
                while ((c = in.read()) >= 0) {
                    System.out.print((char) c);
                }
            } finally {
                in.close();
            }
        }
    }

}

结果展示
在这里插入图片描述

几种常用流的应用场景:

流名称应用场景
ByteArrayInputStream访问数组,把内存中的一个缓冲区作为 InputStream 使用,CPU从缓存区读取数据比从存储介质的速率快10倍以上
StringBufferInputStream把一个 String 对象作为。InputStream。不建议使用,在转换字符的问题上有缺陷
FileInputStream访问文件,把一个文件作为 InputStream ,实现对文件的读取操作
PipedInputStream访问管道,主要在线程中使用,一个线程通过管道输出流发送数据,而另一个线程通过管道输入流读取数据,这样可实现两个线程间的通讯
SequenceInputStream把多个 InputStream 合并为一个 InputStream . “序列输入流”类允许应用程序把几个输入流连续地合并起来
DataInputStream特殊流,读各种基本类型数据,如byte、int、String的功能
ObjectInputStream对象流,读对象的功能
PushBackInputStream推回输入流,可以把读取进来的某些数据重新回退到输入流的缓冲区之中
BufferedInputStream缓冲流,增加了缓冲功能

References:

  • https://coding.imooc.com/class/270.html
  • https://www.pdai.tech/md/dev-spec/pattern/12_decorator.html
  • https://whirlys.blog.csdn.net/article/details/82764333
  • https://blog.csdn.net/u013309870/article/details/75735676
  • 《大话设计模式》
  • https://blog.csdn.net/qq_37960603/article/details/104087989
  • https://blog.csdn.net/weixin_44045828/article/details/110490324

(写博客主要是对自己学习的归纳整理,资料大部分来源于书籍和网络资料,整理不易,但是难免有不足之处,如有错误,请大家评论区批评指正。同时感谢广大博主和广大作者辛苦整理出来的资源。)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值