继承与组合的爱恨情仇 —— 装饰者模式

45 阅读3分钟

大家好,我是徒手敲代码。

今天来介绍一下装饰者模式。

这个东西是什么意思呢?顾名思义,就像给物品添加各种装饰品一样。它用于在不改变原有对象结构的前提下,动态地为对象添加新的功能或属性。

想象一下,你正在经营一家咖啡店,客人可以在一杯什么都没有的咖啡基础上,添加各种各样的东西,比如:牛奶、糖、巧克力、茅台等,这些配料就好像一个个的装饰者,它们各自独立,但能够以任意组合的方式附加到咖啡上,创造出丰富多样的口味。

下面来看代码演示。

先写好一个咖啡Coffee类,表示一杯简单的黑咖啡。

public class Coffee {
    private double price;

    public Coffee(double price) {
        this.price = price;
    }
}

现在,客人想要加糖、加奶来个性化他们的咖啡。我们先尝试用继承的方式来实现:

// 加糖咖啡
public class SugarCoffee extends Coffee {
    public SugarCoffee(double price) {
        // 假设加糖加价5元
        super(price + 5.0); 
    }
}

// 加奶咖啡
public class MilkCoffee extends Coffee {
    public MilkCoffee(double price) {
        // 假设加奶加价10元
        super(price + 10.0); 
    }
}

显然,这样的实现方式存在一些缺点:

  • 类爆炸:每增加一种新的口味,就需要创建一个新的类。随着口味组合的增多,类的数量会迅速膨胀,导致代码复杂度和维护难度增加。虽然类还是会多,但是相对直接继承来说,是少了。
  • 高耦合:各个口味类与基础咖啡类紧密耦合,基础咖啡价格的变动会影响到所有口味类。而且,如果想添加一种新口味组合(如加糖加奶),需要创建一个全新的类来表示,无法复用已有的口味类。

针对以上这些问题,装饰者模式就诞生了。

首先,定义一个CoffeeDecorator抽象类,它继承Coffee并持有一个Coffee对象的引用,这样就可以在装饰者中,调用被装饰咖啡的方法:

public abstract class CoffeeDecorator extends Coffee {
    protected Coffee decoratedCoffee;

    public CoffeeDecorator(Coffee coffee) {
        this.decoratedCoffee = coffee;
    }

    @Override
    public double getPrice() {
        return decoratedCoffee.getPrice();
    }
}

接下来,为每种口味创建装饰者类:

public class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public double getPrice() {
        // 加糖加价5元
        return super.getPrice() + 5.0; 
    }
}

public class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public double getPrice() {
        // 加奶加价10元
        return super.getPrice() + 10.0; 
    }
}

现在,调用方可以自由组合口味,只需在创建基础的咖啡之后,根据实际需要,来添加装饰者即可:

Coffee simpleCoffee = new Coffee(10.0);
Coffee customizedCoffee = new SugarDecorator(new MilkDecorator(simpleCoffee));
System.out.println(customizedCoffee.getPrice()); // 输出:25元

这里利用组合的方式,直接就可以创建出加奶和加糖的咖啡,而不至于要另外创建一个类,来继承原来的咖啡类。

这个设计模式在实际开发中,哪些地方有用到呢?最典型的例子是java.io包中的输入输出流设计。

比如:InputStream是所有输入流的基础接口,FilterInputStream作为装饰者基类,提供了对InputStream的装饰功能。

具体的装饰者如BufferedInputStreamDataInputStream等,分别增加了缓冲读取、数据格式化等特定功能,用户可以根据需要将它们叠加到原始输入流上。

今天的分享到这里就结束了。

关注公众号“徒手敲代码”,免费领取由腾讯大佬推荐的Java电子书!