文章目录
- 1. 概述
- 2.实现方式
- 2.1.原始商品类及计算接口
- 2.2.加入装饰器
- 2.3.装饰器的组合使用
- 3.总结
1. 概述
装饰器模式(Decorator Pattern)是一种结构型的设计模式,使用组合的方式来替代了继承,它的核心作用是在不修改对象本身的基础上动态地给一个对象添加新的职责或功能,装饰器类一般与被装饰的类有同一个父类(或父接口),其后缀名一般Decorator或者Wrapper。
讲到这里,如果熟悉设计模式的同学肯定能发现,这个定义与上一篇设计模式的文章《代理模式的实现方式与使用场景》中提到了代理模式是高度类似的,事实上也确实如此,都在一定程度上对原始对象的行为进行某种形式的包裹或扩展。
那么,装饰模式和代理模式有什么区别呢?
它们在功能实现上有很多地方都可以互换,两者只是在应用场景上划分了不同的区域:
- 代理模式:适合于做与业务流程无关的增强,例如对象的访问控制、接口鉴权、RPC远程访问等
- 装饰模式:适合于对业务功能动态添加额外的职责,这些职责和业务功能流程是紧密关联的。
也就是说,判断选择两种模式的使用,只需要看这次的拓展与业务流程是否有关就可以了,接下来看一下装饰模式的实现方式。
2.实现方式
先用类图来进行说明,在原来的功能接口(或功能抽象类)和功能实现类的基础上,定义了一个装饰器的抽象类,装饰器和功能类实现同一个接口,并且通过组合的方式依赖顶层的接口。如果对类图不熟悉的同学,可以参考《类图(Class Diagram)》。
这么做了之后,装饰器相对于原有的功能接口既是is-a
的关系,也是has-a
的关系,不管装饰器有多少个、包装了多少层,它始终都是原有功能接口的子类,并且在每一层中都会持有对上一层装饰器(或原有功能对象)的引用。
下面通过一个简单Demo场景来体验一下装饰器模式,如何在不改变原有的接口和实现类的情况下,通过装饰器动态的添加新的功能。
假如现在有一个商城服务,里面有一个计算购物车中商品的价格的功能,有这么几个需求:
- 可以通过用户的优惠券做额度抵扣
- 如果用户是会员,可以打九折
- 如果不满足上面两种情景,则需要原价支付
按照需求,我们可以首先创建一个计算价格的接口,然后新增一个装饰器的抽象用于修改最终价格,最后通过两个不同的装饰器实例(优惠券、打折)来实现价格的重新计算。
2.1.原始商品类及计算接口
- 商品类
@Data public class Product { /** * 商家价格(单位:分) */ private Long price; /** * 商品名称 */ private String name; }
- 价格计算接口及其实现
public interface PriceCalculate { Long calculatePrice(List<Product> products); } public class PriceCalculateImpl implements PriceCalculate { @Override public Long calculatePrice(List<Product> products) { return products.stream().mapToLong(Product::getPrice).sum(); } }
2.2.加入装饰器
- 抽象的价格计算装饰器
通过组合的方式依赖价格计算的接口,再通过模板方法将价格的折扣与优惠券抵扣交给各自的实现类去实现。public abstract class PriceCalculateDecorator implements PriceCalculate { protected PriceCalculate priceCalculate; public PriceCalculateDecorator(PriceCalculate priceCalculate) { this.priceCalculate = priceCalculate; } @Override public Long calculatePrice(List<Product> products) { return this.discountPrice(products); } public abstract Long discountPrice(List<Product> products); }
- 会员折扣装饰器
这里做了简单的处理,直接写死折扣,实际的场景中应该从会员折扣配置中获取到折扣的值,并且应该通过Bigdecimal对金额做计算。
/**
* 会员折扣装饰器
*/
public class PriceVipDiscountDecorator extends PriceCalculateDecorator {
public PriceVipDiscountDecorator(PriceCalculate priceCalculate) {
super(priceCalculate);
}
@Override
public Long discountPrice(List<Product> products) {
Long originPrice = priceCalculate.calculatePrice(products);
// 会员九折(此处为简化,实际应该从会员服务获取会员折扣)
return originPrice * 9 / 10;
}
}
- 优惠券抵扣装饰器
和上面的会员抵扣一样,做简化处理/** * 优惠券抵扣装饰器 */ public class PriceCouponDeductionDecorator extends PriceCalculateDecorator { public PriceCouponDeductionDecorator(PriceCalculate priceCalculate) { super(priceCalculate); } @Override public Long discountPrice(List<Product> products) { Long originPrice = priceCalculate.calculatePrice(products); // 优惠券抵扣(此处为简化,实际应该从优惠券服务获取优惠券抵扣金额) return Math.max(originPrice - 5000, 0); } }
最后,写一个测试来验证一下,在这个测试中,分别用VIP折扣和优惠券来做抵扣。
public void testWrapper() {
List<Product> products = getProduct();
PriceCalculate priceCalculate = new PriceCalculateImpl();
Long price = priceCalculate.calculatePrice(products);
System.out.println("原价:" + price + "分");
// 会员折扣
PriceCalculate vipDiscountDecorator = new PriceVipDiscountDecorator(priceCalculate);
Long vipPrice = vipDiscountDecorator.calculatePrice(products);
System.out.println("会员折扣后,价格为:" + vipPrice + "分");
// 优惠券抵扣
PriceCalculate couponDiscountDecorator = new PriceCouponDeductionDecorator(priceCalculate);
Long couponPrice = couponDiscountDecorator.calculatePrice(products);
System.out.println("优惠券抵扣后,价格为:" + couponPrice + "分");
}
private List<Product> getProduct() {
List<Product> products = new ArrayList<>();
// 创建商品
Product product = new Product();
product.setName("商品1");
product.setPrice(100L * 100);
products.add(product);
Product product2 = new Product();
product2.setName("商品2");
product2.setPrice(200L * 100);
products.add(product2);
return products;
}
2.3.装饰器的组合使用
除了上面的分别计算以外,两个装饰器实例还可以组合起来使用,例如现在有个用户既是VIP会有,又有50块的优惠券,按照先打折再优惠券抵扣的规则,可以将VIP装饰器实例通过构造方法直接传入到优惠券装饰器中,修改如下:
List<Product> products = getProduct();
PriceCalculate priceCalculate = new PriceCalculateImpl();
Long price = priceCalculate.calculatePrice(products);
System.out.println("原价:" + price + "分");
// 先打折,再优惠券抵扣
PriceCalculate vipDiscountDecorator = new PriceVipDiscountDecorator(priceCalculate);
PriceCouponDeductionDecorator priceCouponDeductionDecorator = new PriceCouponDeductionDecorator(vipDiscountDecorator);
Long discountPrice = priceCouponDeductionDecorator.calculatePrice(products);
System.out.println("折扣价:" + discountPrice + "分");
如果需求是先抵扣优惠券再打折的话,只需要调整一下装饰器的包装顺序就可以了,如果还有其他的活动折扣,满减之类的对价格计算的调整,再跟进具体的需求新增装饰器并包装起来即可。
3.总结
装饰器的作用是在不改变目标类的情况下,动态的添加功能或职责,上述的功能使用继承也可以实现,但是使用继承之后,一旦修改了父类的方法所有子类都会受到影响,而使用装饰器的方式,目标对象与装饰器对象并不会互相影响。
另外,需要注意到的是装饰模式和代理模式非常相似,从功能上来讲,一个可以实现的功能使用另一个也可以实现,我们在使用的过程中只需要注意一点,即:代理模式用于实现非业务的功能增强,装饰器模式往往用于对业务功能的增强。
最后,再谈一下装饰器的缺点,包装的层数过多的时候会给我们的问题排查带来一定的困难,需要一层一层的往里面剥才有可能找到是哪个装饰器出现的问题,所以我们在使用的时候尽可能的减少装饰器包装的层数,两到三层即可。