首页 > 编程技术 > java

Java设计模式之java装饰者模式详解

发布时间:2021-9-16 00:00

介绍

装饰者模式(Decorator Pattern):动态地给一个对象增加一些额外的职责,增加对象功能来说,装饰模式比生成子类实现更为灵活。装饰模式是一种对象结构型模式。

在装饰者模式中,为了让系统具有更好的灵活性和可扩展性,我们通常会定义一个抽象装饰类,而将具体的装饰类作为它的子类

装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任。换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不使用创造更多子类的情况下,将对象的功能加以扩展。

装饰模式的类图如下:

在这里插入图片描述

角色

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

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

示例代码

抽象构件角色

public interface Component {
    public void sampleOperation();
}

具体构件角色

public class ConcreteComponent implements Component {
    @Override
    public void sampleOperation() {
        // 写相关的业务代码
    }
}

装饰角色

public class Decorator implements Component{
    private Component component;
    public Decorator(Component component){
        this.component = component;
    }
    @Override
    public void sampleOperation() {
        // 委派给构件
        component.sampleOperation();
    }
}

具体装饰角色

public class ConcreteDecoratorA extends Decorator {
    public ConcreteDecoratorA(Component component) {
        super(component);
    }
    @Override
    public void sampleOperation() {
     super.sampleOperation();
        // 写相关的业务代码
    }
}

public class ConcreteDecoratorB extends Decorator {
    public ConcreteDecoratorB(Component component) {
        super(component);
    }
    @Override
    public void sampleOperation() {
      super.sampleOperation();
        // 写相关的业务代码
    }
}

星巴克咖啡的例子

在这里插入图片描述

方案一

在这里插入图片描述

加入不同调料的咖啡,例如:蒸奶(Steamed Milk)、豆浆(Soy)、摩卡(Mocha,也就是巧克力风味)或覆盖奶泡。星巴兹会根据所加入的调料收取不同的费用。所以订单系统必须考虑到这些调料部分。

在这里插入图片描述

方案二 :将调料内置到Drink类中

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这种设计虽然满足了现在的需求,但是我们想一下,如果出现下面情况,我们怎么办,

①、调料价钱的改变会使我们更改现有代码。

②、一旦出现新的调料,我们就需要加上新的方法,并改变超类中的cost()方法。

③、以后可能会开发出新饮料。对这些饮料而言(例如:冰茶),某些调料可能并不适合,但是在这个设计方式中,Tea(茶)子类仍将继承那些不适合的方法,例如:hasWhip()(加奶泡)。

④、万一顾客想要双倍摩卡咖啡,怎么办?

很明显,上面的设计并不能够从根本上解决我们所碰到的问题。并且这种设计违反了 开放关闭原则(类应该对扩展开放,对修改关闭。)。

那我们怎么办呢?好啦,装饰者可以非常完美的解决以上的所有问题,让我们有一个设计非常nice的咖啡馆。

方案三:装饰者模式

在这里插入图片描述

这里的Coffee是一个缓冲层,负责将抽取出所有具体咖啡的共同点

在这里插入图片描述

代码演示

饮料抽象类:

public abstract class Drink
{
   protected String decription="";//描述
   public String getDecription() {
      return decription;
   }
   public abstract Integer cost();//返回饮料的价格
}

缓冲层:抽取出所有咖啡类的共同特征,即计算价钱

//缓冲层----所有种类咖啡的共同点抽取出来
public abstract class Coffee extends Drink
{
    //共同特点:计算价格
    @Override
    public Integer cost() {
        //价格从0累加
        return 0;
    }
}

具体的咖啡类:

public class LongBlack extends Coffee
{
    LongBlack()
    {
        decription="美式咖啡";
    }
    @Override
    public Integer cost() {
        return 15;
    }
}

public class ChinaBlack extends Coffee
{
    ChinaBlack()
    {
        decription="中式咖啡";
    }
    @Override
    public Integer cost() {
        return 10;
    }
}

public class Espresso extends Coffee
{
    //设置描述信息
    Espresso()
    {
        decription="意大利咖啡";
    }
    @Override
    public Integer cost() {
        //意大利咖啡20元
        return 20;
    }
}

抽象装饰者

//装饰者
public abstract class Decorator extends Drink
{
    @Override
    public abstract String getDecription();
}

具体装饰者—即调料

public class Milk extends Decorator{
    Drink drink;
    Milk(Drink drink)
    {
        this.drink=drink;
    }
    @Override
    public String getDecription()
    {
        return "加了牛奶的"+this.drink.getDecription();
    }
    @Override
    public Integer cost()
    {
        return this.drink.cost()+3;
    }
}

public class Chocolate extends Decorator{
     //用一个实例变量记录饮料,也就是被装饰者
    Drink drink;
    Chocolate(Drink drink) {
       this.drink=drink;
    }
    @Override
    public String getDecription() {
        return "加了巧克力的"+drink.getDecription();
    }
    @Override
    public Integer cost() {
        //在原有饮料价格的基础上加上调料味的价格
        return 5+drink.cost();
    }
}

测试

public class test
{
    @Test
    public void test()
    {
        //模拟下单
        //首先点一个美式咖啡,不加任何调料
        Drink drink=new LongBlack();
        System.out.println("购买了"+drink.getDecription()+"  花了"+drink.cost());
        //给美式咖啡加一个巧克力
        drink=new Chocolate(drink);
        System.out.println("购买了"+drink.getDecription()+"  花了"+drink.cost());
        //给美式咖啡再加一个牛奶
        drink=new Milk(drink);
        System.out.println("购买了"+drink.getDecription()+"  花了"+drink.cost());
        //再把牛奶和巧克力加一次
        drink=new Chocolate(drink);
        System.out.println("购买了"+drink.getDecription()+"  花了"+drink.cost());
        drink=new Milk(drink);
        System.out.println("购买了"+drink.getDecription()+"  花了"+drink.cost());
        System.out.println("====================================================");
        //简化写法
        Drink d=new Chocolate(new Milk(new ChinaBlack()));
        System.out.println("购买了"+d.getDecription()+"  花了"+d.cost());
    }
}

在这里插入图片描述

装饰者模式的简化

大多数情况下,装饰模式的实现都要比上面给出的示意性例子要简单。

如果只有一个ConcreteComponent类,那么可以考虑去掉抽象的Component类(接口),把Decorator作为一个ConcreteComponent子类。如下图所示:

在这里插入图片描述

如果只有一个ConcreteDecorator类,那么就没有必要建立一个单独的Decorator类,而可以把Decorator和ConcreteDecorator的责任合并成一个类。甚至在只有两个ConcreteDecorator类的情况下,都可以这样做。如下图所示:

在这里插入图片描述

透明性的要求

装饰模式对客户端的透明性要求程序不要声明一个ConcreteComponent类型的变量,而应当声明一个Component类型的变量。

用顶层抽象父类指向具体子类,以多态的形式实现透明性要求

应该像下面这样写:

Drink drink=new LongBlack();
//给美式咖啡加一个巧克力
drink=new Chocolate(drink);

而不是这样写

Drink drink=new LongBlack();
//给美式咖啡加一个巧克力
Chocolate drink=new Chocolate(drink);

半透明的装饰模式

然而,纯粹的装饰模式很难找到。装饰模式的用意是在不改变接口的前提下,增强所考虑的类的性能。

在增强性能的时候,往往需要建立新的公开的方法。

比如巧克力可以单独售卖,即售卖巧克力棒,那么这里巧克力类里面需要新增加一个sell方法,用于单独售卖

这就导致了大多数的装饰模式的实现都是“半透明”的,而不是完全透明的。换言之,允许装饰模式改变接口,增加新的方法。这意味着客户端可以声明ConcreteDecorator类型的变量,从而可以调用ConcreteDecorator类中才有的方法:

Drink drink=new LongBlack();
//给美式咖啡加一个巧克力
Chocolate drink=new Chocolate(drink);
//售卖巧克力棒
drink.sell();

半透明的装饰模式是介于装饰模式和适配器模式之间的。适配器模式的用意是改变所考虑的类的接口,也可以通过改写一个或几个方法,或增加新的方法来增强或改变所考虑的类的功能。大多数的装饰模式实际上是半透明的装饰模式,这样的装饰模式也称做半装饰、半适配器模式。

装饰模式的优点

装饰模式的缺点

装饰模式注意事项

(1) 尽量保持装饰类的接口与被装饰类的接口相同,这样,对于客户端而言,无论是装饰之前的对象还是装饰之后的对象都可以一致对待。这也就是说,在可能的情况下,我们应该尽量使用透明装饰模式。

(2) 尽量保持具体构件类是一个“轻”类,也就是说不要把太多的行为放在具体构件类中,我们可以通过装饰类对其进行扩展。

(3) 如果只有一个具体构件类,那么抽象装饰类可以作为该具体构件类的直接子类。

适用场景

设计模式在JAVA I/O库中的应用

装饰模式在Java语言中的最著名的应用莫过于Java I/O标准库的设计了

由于Java I/O库需要很多性能的各种组合,如果这些性能都是用继承的方法实现的,那么每一种组合都需要一个类,这样就会造成大量性能重复的类出现。而如果采用装饰模式,那么类的数目就会大大减少,性能的重复也可以减至最少。因此装饰模式是Java I/O库的基本模式。

Java I/O库的对象结构图如下,由于Java I/O的对象众多,因此只画出InputStream的部分。

在这里插入图片描述

下面是使用I/O流读取文件内容的简单操作示例。

public class IOTest {
    public static void main(String[] args) throws IOException {
        // 流式读取文件
        DataInputStream dis = null;
        try{
            dis = new DataInputStream(
                    new BufferedInputStream(
                            new FileInputStream("test.txt")
                    )
            );
            //读取文件内容
            byte[] bs = new byte[dis.available()];
            dis.read(bs);
            String content = new String(bs);
            System.out.println(content);
        }finally{
            dis.close();
        }
    }
}

观察上面的代码,会发现最里层是一个FileInputStream对象,然后把它传递给一个BufferedInputStream对象,经过BufferedInputStream处理,再把处理后的对象传递给了DataInputStream对象进行处理,这个过程其实就是装饰器的组装过程,FileInputStream对象相当于原始的被装饰的对象,而BufferedInputStream对象和DataInputStream对象则相当于装饰器。

透明和半透明的装饰模式的区别

理想的装饰模式在对被装饰对象进行功能增强的同时,要求具体构件角色、装饰角色的接口与抽象构件角色的接口完全一致。

而适配器模式则不然,一般而言,适配器模式并不要求对源对象的功能进行增强,但是会改变源对象的接口,以便和目标接口相符合。

装饰模式有透明和半透明两种,这两种的区别就在于装饰角色的接口与抽象构件角色的接口是否完全一致。

透明的装饰模式也就是理想的装饰模式,要求具体构件角色、装饰角色的接口与抽象构件角色的接口完全一致。

相反,如果装饰角色的接口与抽象构件角色接口不一致,也就是说装饰角色的接口比抽象构件角色的接口宽的话,装饰角色实际上已经成了一个适配器角色,这种装饰模式也是可以接受的,称为“半透明”的装饰模式,如下图所示

在这里插入图片描述

在适配器模式里面,适配器类的接口通常会与目标类的接口重叠,但往往并不完全相同。换言之,适配器类的接口会比被装饰的目标类接口宽。

显然,半透明的装饰模式实际上就是处于适配器模式与装饰模式之间的灰色地带。如果将装饰模式与适配器模式合并成为一个“包装模式”的话,那么半透明的装饰模式倒可以成为这种合并后的“包装模式”的代表。

参考文章

设计模式 | 装饰者模式及典型应用

《JAVA与模式》之装饰模式

设计模式之装饰者模式

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注猪先飞的更多内容!

标签:[!--infotagslink--]

您可能感兴趣的文章: