趣谈装饰器模式,让你一辈子不会忘
本文节选自《设计模式就该这样学》
1 使用装饰器模式解决煎饼加码问题
来看这样一个场景,上班族大多有睡懒觉的习惯,每天早上上班都时间很紧张,于是很多人为了多睡一会儿,就用更方便的方式解决早餐问题,有些人早餐可能会吃煎饼。煎饼中可以加鸡蛋,也可以加香肠,但是不管怎么加码,都还是一个煎饼。再比如,给蛋糕加上一些水果,给房子装修,都是装饰器模式。
下面用代码来模拟给煎饼加码的业务场景,先来看不用装饰器模式的情况。首先创建一个煎饼Battercake类。
```java
public class Battercake {
protected String getMsg(){
return "煎饼";
}
public int getPrice(){
return 5;
}
}
```
然后创建一个加鸡蛋的煎饼BattercakeWithEgg类。
```java
public class BattercakeWithEgg extends Battercake{ @Override protected String getMsg() { return super.getMsg() + "+1个鸡蛋"; }
@Override
//加1个鸡蛋加1元钱
public int getPrice() {
return super.getPrice() + 1;
}
}
```
再创建一个既加鸡蛋又加香肠的BattercakeWithEggAndSausage类。
```java
public class BattercakeWithEggAndSausage extends BattercakeWithEgg{ @Override protected String getMsg() { return super.getMsg() + "+1根香肠"; }
@Override
//加1根香肠加2元钱
public int getPrice() {
return super.getPrice() + 2;
}
}
```
最后编写客户端测试代码。
```java
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类。
```java
public abstract class Battercake { protected abstract String getMsg(); protected abstract int getPrice(); }
```
创建一个基本的煎饼(或者叫基础套餐)BaseBattercake。
```java
public class BaseBattercake extends Battercake { protected String getMsg(){ return "煎饼"; }
public int getPrice(){ return 5; }
}
```
然后创建一个扩展套餐的抽象装饰器BattercakeDecotator类。
```java
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类。
```java
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类。
```java
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;
}
}
```
再编写客户端测试代码。
```java
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框架搭建而成。客户端调用如下。
```java
private static final Logger logger = LoggerFactory.getLogger(Component.class); logger.error(string);
```
这样打印出来的是毫无规则的一行行字符串。当考虑将其转换成JSON格式时,笔者采用装饰器模式。目前有的是统一接口Logger和其具体实现类,笔者要加的就是一个装饰类和真正封装成JSON格式的装饰产品类。创建装饰器类DecoratorLogger。
```java
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类。
```java
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实例。最终在新的日志体系中,使用方式如下。
```java
private static final Logger logger = JsonLoggerFactory.getLogger(Client.class);
public static void main(String[] args) {
logger.error("错误信息");
}
```
对于客户端而言,唯一与原先不同的地方就是将LoggerFactory改为JsonLoggerFactory即可,这样的实现,也会更快更方便地被其他开发者接受和习惯。最后看如下图所示的类图。
装饰器模式最本质的特征是将原有类的附加功能抽离出来,简化原有类的逻辑。通过这样两个案例,我们可以总结出来,其实抽象的装饰器是可有可无的,具体可以根据业务模型来选择。
关注『 Tom弹架构 』回复“设计模式”可获取完整源码。
【推荐】Tom弹架构:30个设计模式真实案例(附源码),挑战年薪60W不是梦
本文为“Tom弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐!如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。关注『 Tom弹架构 』可获取更多技术干货!
- 使用桥接模式设计复杂的消息系统
- 为什么MySQL索引结构采用B 树?
- 为什么Netty线程池默认大小为CPU核数的2倍
- 谈谈你对深克隆和浅克隆的理解
- 什么是代理,为什么要用动态代理?
- 什么是零拷贝,Netty是如何实现的?
- 3分钟轻松理解单线程下的HashMap工作原理
- 被面试官问烂的Spring AOP原理,你是怎么答的?
- Spring为何需要三级缓存解决循环依赖,而不是二级缓存?
- 为什么Spring中每个Bean都要定义作用域?
- 谈谈你对Spring Bean的理解
- 趣谈装饰器模式,让你一辈子不会忘
- 掌握这些招数,你也能写出HR眼中的高分简历
- MongoDB高级应用之数据转存与恢复(5)
- 图解MongoDB集群部署原理(3)
- 爆肝30天,肝出来史上最透彻Spring原理和27道高频面试题总结
- Spring核心原理之IoC容器初体验(2)
- Spring核心原理分析之MVC九大组件(1)
- 30个类手写Spring核心原理之动态数据源切换(8)
- 【紧急】Log4j又发新版2.17.0,只有彻底搞懂漏洞原因,才能以不变应万变,小白也能看懂