《设计模式就该这样学》之趣谈装饰器模式,让你一辈子不会忘

本文节选自《设计模式就该这样学》

1 使用装饰器模式解决煎饼加码问题


来看这样一个场景,上班族大多有睡懒觉的习惯,每天早上上班都时间很紧张,于是很多人为了多睡一会儿,就用更方便的方式解决早餐问题,有些人早餐可能会吃煎饼。煎饼中可以加鸡蛋,也可以加香肠,但是不管怎么加码,都还是一个煎饼。再比如,给蛋糕加上一些水果,给房子装修,都是装饰器模式。

下面用代码来模拟给煎饼加码的业务场景,先来看不用装饰器模式的情况。首先创建一个煎饼Battercake类。

public class Battercake {

protected String getMsg(){

return “煎饼”;

}

public int getPrice(){

return 5;

}

}

然后创建一个加鸡蛋的煎饼BattercakeWithEgg类。

public class BattercakeWithEgg extends Battercake{

@Override

protected String getMsg() {

return super.getMsg() + “+1个鸡蛋”;

}

@Override

//加1个鸡蛋加1元钱

public int getPrice() {

return super.getPrice() + 1;

}

}

再创建一个既加鸡蛋又加香肠的BattercakeWithEggAndSausage类。

public class BattercakeWithEggAndSausage extends BattercakeWithEgg{

@Override

protected String getMsg() {

return super.getMsg() + “+1根香肠”;

}

@Override

//加1根香肠加2元钱

public int getPrice() {

return super.getPrice() + 2;

}

}

最后编写客户端测试代码。

public static void main(String[] args) {

Battercake battercake = new Battercake();

System.out.println(battercake.getMsg() + “,总价格:” + battercake.getPrice());

Battercake battercakeWithEgg = new BattercakeWithEgg();

System.out.println(battercakeWithEgg.getMsg() + “,总价格:” +

battercakeWithEgg.getPrice());

Battercake battercakeWithEggAndSausage = new BattercakeWithEggAndSausage();

System.out.println(battercakeWithEggAndSausage.getMsg() + “,总价格:” +

battercakeWithEggAndSausage.getPrice());

}

运行结果如下图所示。

图片

运行结果没有问题。但是,如果用户需要一个加2个鸡蛋和1根香肠的煎饼,则用现在的类结构是创建不出来的,也无法自动计算出价格,
除非再创建一个类做定制。如果需求再变,那么一直加定制显然是不科学的。下面用装饰器模式来解决上面的问题。首先创建一个煎饼的抽象Battercake类。

public abstract class Battercake {

protected abstract String getMsg();

protected abstract int getPrice();

}

创建一个基本的煎饼(或者叫基础套餐)BaseBattercake。

public class BaseBattercake extends Battercake {

protected String getMsg(){

return “煎饼”;

}

public int getPrice(){ return 5; }

}

然后创建一个扩展套餐的抽象装饰器BattercakeDecotator类。

public abstract class BattercakeDecorator extends Battercake {

//静态代理,委派

private Battercake battercake;

public BattercakeDecorator(Battercake battercake) {

this.battercake = battercake;

}

protected abstract void doSomething();

@Override

protected String getMsg() {

return this.battercake.getMsg();

}

@Override

protected int getPrice() {

return this.battercake.getPrice();

}

}

接着创建鸡蛋装饰器EggDecorator类。

public class EggDecorator extends BattercakeDecorator {

public EggDecorator(Battercake battercake) {

super(battercake);

}

protected void doSomething() {}

@Override

protected String getMsg() {

return super.getMsg() + “+1个鸡蛋”;

}

@Override

protected int getPrice() {

return super.getPrice() + 1;

}

}

创建香肠装饰器SausageDecorator类。

public class SausageDecorator extends BattercakeDecorator {

public SausageDecorator(Battercake battercake) {

super(battercake);

}

protected void doSomething() {}

@Override

protected String getMsg() {

return super.getMsg() + “+1根香肠”;

}

@Override

protected int getPrice() {

return super.getPrice() + 2;

}

}

再编写客户端测试代码。

public class BattercakeTest {

public static void main(String[] args) {

Battercake battercake;

//买一个煎饼

battercake = new BaseBattercake();

//煎饼有点小,想再加1个鸡蛋

battercake = new EggDecorator(battercake);

//再加1个鸡蛋

battercake = new EggDecorator(battercake);

//很饿,再加1根香肠

battercake = new SausageDecorator(battercake);

//与静态代理的最大区别就是职责不同

//静态代理不一定要满足is-a的关系

//静态代理会做功能增强,同一个职责变得不一样

//装饰器更多考虑的是扩展

System.out.println(battercake.getMsg() + “,总价:” + battercake.getPrice());

}

}

运行结果如下图所示。

图片

最后来看类图,如下图所示。

图片

2 使用装饰器模式扩展日志格式输出


为了加深印象,我们再来看一个应用场景。需求大致是这样的,系统采用的是SLS服务监控项目日志,以JSON格式解析,因此需要将项目中的日志封装成JSON格式再打印。现有的日志体系采用Log4j + Slf4j框架搭建而成。客户端调用如下。

private static final Logger logger = LoggerFactory.getLogger(Component.class);

logger.error(string);

这样打印出来的是毫无规则的一行行字符串。当考虑将其转换成JSON格式时,笔者采用装饰器模式。目前有的是统一接口Logger和其具体实现类,笔者要加的就是一个装饰类和真正封装成JSON格式的装饰产品类。创建装饰器类DecoratorLogger。

public class DecoratorLogger implements Logger {

public Logger logger;

public DecoratorLogger(Logger logger) {

this.logger = logger;

}

public void error(String str) {}

public void error(String s, Object o) {

}

//省略其他默认实现

}

创建具体组件JsonLogger类。

public class JsonLogger extends DecoratorLogger {

public JsonLogger(Logger logger) {

super(logger);

}

@Override

public void info(String msg) {

JSONObject result = composeBasicJsonResult();

result.put(“MESSAGE”, msg);

logger.info(result.toString());

}

@Override

public void error(String msg) {

JSONObject result = composeBasicJsonResult();

result.put(“MESSAGE”, msg);

logger.error(result.toString());

}

public void error(Exception e) {

JSONObject result = composeBasicJsonResult();

result.put(“EXCEPTION”, e.getClass().getName());

String exceptionStackTrace = Arrays.toString(e.getStackTrace());

result.put(“STACKTRACE”, exceptionStackTrace);

logger.error(result.toString());

}

private JSONObject composeBasicJsonResult() {

//拼装了一些运行时的信息

return new JSONObject();

}

}

可以看到,在JsonLogger中,对于Logger的各种接口,我们都用JsonObject对象进行一层封装。在打印的时候,最终还是调用原生接口logger.error(string),只是这个String参数已经被装饰过了。如果有额外的需求,则可以再写一个函数去实现。比如error(Exception e),只传入一个异常对象,这样在调用时就非常方便。另外,为了在新老交替的过程中尽量不改变太多代码和使用方式,笔者又在JsonLogger中加入了一个内部的工厂类JsonLoggerFactory(这个类转移到DecoratorLogger中可能更好一些)。它包含一个静态方法,用于提供对应的JsonLogger实例。最终在新的日志体系中,使用方式如下。

private static final Logger logger = JsonLoggerFactory.getLogger(Client.class);

public static void main(String[] args) {

logger.error(“错误信息”);

}

对于客户端而言,唯一与原先不同的地方就是将LoggerFactory改为JsonLoggerFactory即可,这样的实现,也会更快更方便地被其他开发者接受和习惯。最后看如下图所示的类图。

图片

装饰器模式最本质的特征是将原有类的附加功能抽离出来,简化原有类的逻辑。通过这样两个案例,我们可以总结出来,其实抽象的装饰器是可有可无的,具体可以根据业务模型来选择。

面试系列爆文:

面试题:深克隆和浅克隆的实现方式

=========================================================================================================================================================================================================================================
看了这篇文章后,面试官再也不敢问你非结构化存储的原理了
吹爆阿里P8的SQL优化笔记,由于太全直接被GitHub下架
看了这篇文章后,面试官再也不敢问你非结构化存储的原理了

  • 16
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值