Java设计模式之装饰模式

160 阅读4分钟

装饰模式可以拓展对象,继承虽然也能做到,但是装饰模式会更加灵活,并且在拓展功能的同时,不会影响到对象本身。

举例说明,假设我们要给咖啡店里的订单计算价格,场景是这样的:

首先是一杯普通咖啡价格10元,加糖的话要额外收5元,加牛奶就要额外收10元,
调料可以随意组合,比如只加糖,或者加糖和加奶,
用户按需选择,最后计算总价格。
然后需要可以拓展,因为后面可能增加蜂蜜、香草、巧克力等调料,价格也不一样。

首先在没有考虑装饰模式的情况下,我们尝试一下可以怎么写。

普通方法实现

创建一个咖啡实体类:

package com.cc.decorator;
​
/**
 * 定义一个咖啡实体类
 * @author cc
 * @date 21-12-17 0:41
 */
public class Coffee {
    private int price;
​
    public Coffee() {
        this.price = 10;
    }
​
    public void setPrice(int price) {
        this.price = price;
    }
​
    // 获取价格
    public int getPrice() {
        return price;
    }
​
    // 获取名字
    public String getName() {
        return "普通咖啡";
    }
​
    // 添加调料的方法
    public void addSeasoning(Seasoning seasoning) {
        this.price += seasoning.getPrice();
    }
}
package com.cc.decorator;
​
/**
 * 调料抽象类
 * @author cc
 * @date 2021-12-17 11:24
 */
public abstract class Seasoning {
    protected int price;
​
    public int getPrice() {
        return price;
    }
​
    public void setPrice(int price) {
        this.price = price;
    }
}
package com.cc.decorator;
​
/**
 * 牛奶实现类
 * @author cc
 * @date 2021-12-17 11:33
 */
public class Milk extends Seasoning {
    public Milk() {
        this.price = 10;
    }
}
package com.cc.decorator;
​
/**
 * 糖实现类
 * @author cc
 * @date 2021-12-17 11:33
 */
public class Suger extends Seasoning {
    public Suger() {
        this.price = 5;
    }
}

现在可以测试一下:

package com.cc.decorator;
​
public class Main {
    public static void main(String[] args) {
        {
            Coffee coffee = new Coffee();
            System.out.println("普通咖啡价格:" + coffee.getPrice());
        }
        {
            Coffee coffee = new Coffee();
            coffee.addSeasoning(new Suger());
            System.out.println("普通咖啡加糖后价格:" + coffee.getPrice());
        }
        {
            Coffee coffee = new Coffee();
            coffee.addSeasoning(new Suger());
            coffee.addSeasoning(new Milk());
            System.out.println("普通咖啡加糖加奶后价格:" + coffee.getPrice());
        }
​
    }
}

结果:

普通咖啡价格:10
普通咖啡加糖后价格:15
普通咖啡加糖加奶后价格:25

这种方式可以实现,并且拓展也容易,只要新增调料抽象类的子类就行,但是有什么问题呢,这里是把价格处理的逻辑放到了咖啡实体类里面,当价格计算方式出现变化,需要改动Coffee代码,这违背了开闭原则,并且如果想要增加其他的组件,比如咖啡打包模块,又要在Coffee里添加对应的函数,这种方式就很不妥。

装饰模式来实现

package com.cc.decorator;
​
/**
 * 定义一个咖啡抽象基类
 * @author cc
 * @date 21-12-17 0:41
 */
public abstract class Coffee {
    // 获取价格
    abstract int getPrice();
​
    // 获取名字
    abstract String getName();
}
package com.cc.decorator;
​
/**
 * 定义一个抽象装饰类,继承Coffee
 * @author cc
 * @date 21-12-17 0:43
 */
public abstract class Decorator extends Coffee{
    protected Coffee mCoffee;
​
    // 通过组合的方式把Coffee传递进来
    public Decorator(Coffee coffee) {
        this.mCoffee = coffee;
    }
}
package com.cc.decorator;
​
/**
 * 普通咖啡实体类
 * @author cc
 * @date 21-12-17 0:56
 */
public class SimpleCoffee extends Coffee {
    @Override
    int getPrice() {
        return 10;
    }
​
    @Override
    String getName() {
        return "普通咖啡";
    }
}
package com.cc.decorator;
​
/**
 * 装饰器:调料加糖
 * @author cc
 * @date 21-12-17 0:57
 */
public class SugerDecorator extends Decorator {
    public SugerDecorator(Coffee coffee) {
        super(coffee);
    }
​
    @Override
    int getPrice() {
        return mCoffee.getPrice() + 5;
    }
​
    @Override
    String getName() {
        return mCoffee.getName() + "添加调料:糖|";
    }
}
package com.cc.decorator;
​
/**
 * 装饰器:调料加牛奶
 * @author cc
 * @date 21-12-17 0:57
 */
public class MilkDecorator extends Decorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }
​
    @Override
    int getPrice() {
        return mCoffee.getPrice() + 10;
    }
​
    @Override
    String getName() {
        return mCoffee.getName() + "添加调料:牛奶|";
    }
}

测试一下:

package com.cc.decorator;
​
public class Main {
    public static void main(String[] args) {
        {
            Coffee coffee = new SimpleCoffee();
            System.out.println(coffee.getName() + ",价格是:" + coffee.getPrice());
        }
        {
            Coffee coffee = new SugerDecorator(new SimpleCoffee());
            System.out.println(coffee.getName() + ",价格是:" + coffee.getPrice());
        }
        {
            Coffee coffee = new MilkDecorator(new SimpleCoffee());
            System.out.println(coffee.getName() + ",价格是:" + coffee.getPrice());
        }
        {
            Coffee coffee = new SugerDecorator(new MilkDecorator(new SimpleCoffee()));
            System.out.println(coffee.getName() + ",价格是:" + coffee.getPrice());
        }
    }
}

结果:

普通咖啡,价格是:10
普通咖啡添加调料:糖|,价格是:15
普通咖啡添加调料:牛奶|,价格是:20
普通咖啡添加调料:牛奶|添加调料:糖|,价格是:25

现在用装饰模式改造之后,咖啡添加调料时价格的变动和咖啡实体类本身没有影响,价格由调料装饰器去处理,每当咖啡被一层调料装饰器包装后,价格就会发生变化。

关于调料拓展:当增加了新的调料,只需要创建新的调料装饰器即可。

关于功能拓展:需要增加打包模块,并计算价格的时候,创建一个打包的抽象装饰类,继承Coffee,参考Decorator类,然后编写相应的价格更新逻辑,然后继续包装Coffee类即可,整个过程对Coffee类没有任何影响。

总结

装饰模式在这种需要对元素进行组合的场景下十分适用,可以避免写出大量的组合类。做到拓展功能的同时,又不影响对象本身。

当然缺点也是存在的,因为装饰模式会产生许多的对象,这些对象本身又是及其相似的,所以这对查错造成了困难,比如普通咖啡经过装饰可能变成加糖咖啡、加奶咖啡或者加糖加奶咖啡,他们都是咖啡对象。