装饰模式可以拓展对象,继承虽然也能做到,但是装饰模式会更加灵活,并且在拓展功能的同时,不会影响到对象本身。
举例说明,假设我们要给咖啡店里的订单计算价格,场景是这样的:
首先是一杯普通咖啡价格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类没有任何影响。
总结
装饰模式在这种需要对元素进行组合的场景下十分适用,可以避免写出大量的组合类。做到拓展功能的同时,又不影响对象本身。
当然缺点也是存在的,因为装饰模式会产生许多的对象,这些对象本身又是及其相似的,所以这对查错造成了困难,比如普通咖啡经过装饰可能变成加糖咖啡、加奶咖啡或者加糖加奶咖啡,他们都是咖啡对象。