趣谈装饰器模式,让你一辈子不会忘

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 来看这样一个场景,上班族大多有睡懒觉的习惯,每天早上上班都时间很紧张,于是很多人为了多睡一会儿,就用更方便的方式解决早餐问题,有些人早餐可能会吃煎饼。煎饼中可以加鸡蛋,也可以加香肠,但是不管怎么加码,都还是一个煎饼。再比如,给蛋糕加上一些水果,给房子装修,都是装饰器模式。

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

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());
    }

运行结果如下图所示。

20211101163802899.png

运行结果没有问题。但是,如果用户需要一个加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());
    }
}

运行结果如下图所示。

20211101163803134.png

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

20211101163803329.png

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即可,这样的实现,也会更快更方便地被其他开发者接受和习惯。最后看如下图所示的类图。


20211101163803484.png

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


【推荐】Tom弹架构:30个设计模式真实案例(附源码),挑战年薪60W不是梦


本文为“Tom弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐!

如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
6月前
|
安全 算法 前端开发
作为程序员变强了也变秃了遇到令人膛目结舌的代码技巧
作为程序员变强了也变秃了遇到令人膛目结舌的代码技巧
56 1
|
存储 编译器 C语言
重生之我要学C++第三天(类和对象)
重生之我要学C++第三天(类和对象)
69 0
|
设计模式 分布式计算 Java
你敢信?清华毕业大佬用了一个坦克大战项目就讲完了23种设计模式
坦克大战 一、需求分析 坦克大战中有我方坦克和敌方坦克,我方坦克有一个,敌方坦克有多个。坦克可以移动,也可以发射子弹。我方坦克可以通过上下左右键来控制方向,敌方坦克自动改变方向。在游戏窗体中有障碍物,包括砖墙、钢墙、水墙和草地。坦克可以穿过草地,在遇到其他障碍物时,我方坦克停止移动,并通过操纵改变方向再移动,敌方坦克遇到其他障碍物时可以自动的改变方向。我方坦克有三次生命值,当与敌方坦克或敌方子弹相撞时,生命值减- -, 当生命值是零时,敌方胜利,游戏结束。敌方坦克的生命值为-一,当敌方坦克与我方坦克或者我方子弹相遇就消失,同时我方子弹也消失,并产生爆炸。当我方坦克把敌方坦克消灭完后,我方胜利
74 0
|
设计模式 算法
趣解设计模式之《怀念小时候玩的红白机嘛?》
趣解设计模式之《怀念小时候玩的红白机嘛?》
104 0
|
设计模式 缓存 算法
花了30天才肝出来,史上最全面Java设计模式总结,看完再也不会忘
Design Patterns: Elements of Reusable Object-Oriented Software(以下简称《设计模式》),一书由Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides合著(Addison-Wesley,1995)。这四位作者常被称为“四人组(Gang of Four)”,而这本书也就被称为“四人组(或 GoF)”书。他们首次给我们总结出一套软件开发可以反复使用的经验,帮助我们提高代码的可重用性、系统的可维护性等,解决软件开发中的复杂问题。
165 0
|
人工智能 IDE 算法
【周末闲谈】新的编程方式,程序员的未来何在?
【周末闲谈】新的编程方式,程序员的未来何在?
123 0
|
Windows
写文章狗屁不通,怎么办?跪求高人指点!
写文章狗屁不通,怎么办?跪求高人指点!
189 0
写文章狗屁不通,怎么办?跪求高人指点!
|
小程序 数据安全/隐私保护 C语言
爱上c++的第九天-聊一聊类和对象的特性
总结如下:1.基于过程的程序设计:由一个个函数组成,每个函数都是独立的,一个函数对应一个操作,适合小程序开发。讲究每一步都要详细的写出,写出来,问题也就解决了。
61 0
爱上c++的第九天-聊一聊类和对象的特性
|
Java 程序员
一个程序员的中秋节碎碎念
2022 年中秋节非常特殊,和教师节同一天。 在这个特殊的日子里,谈谈我的中秋仪式感,中秋计划怎么过,并谈谈自己的一些收获和感悟。
263 0
一个程序员的中秋节碎碎念
|
设计模式 前端开发 JavaScript
好记性不如烂笔头——23种设计模式(上)
好记性不如烂笔头——23种设计模式(上)
好记性不如烂笔头——23种设计模式(上)

相关实验场景

更多