设计之禅——装饰者模式详解(与代理模式的区别以及与其他模式的组合)
前言
相信很多初学者都对JavaAPI中的IO包感到头大,其中的类非常多,看着看着就晕了,笔者也是一样。不过,若是了解了装饰者模式那再看IO包的设计就很清晰明了了。
概述
装饰者模式动态的将责任附加到对象上。若要增加功能,装饰者提供了比继承更具有弹性的替代方案。
我们知道面向对象设计最基本的原则之一就是对扩展开放,对修改关闭,如果仅仅使用继承那么必然不会有很好的扩展性,尤其是我们想给对象增加功能时,装饰者模式也就因此而出现了,那它是如何做到的呢?先来看看它的类图:
装饰者模式包含了抽象组件、具体组件、抽象装饰者、具体装饰者四个角色。具体组件和抽象组件都实现自组件,以此保持类型的一致;具体装饰者持有具体组件的引用,也就是通过组合来实现动态的为对象附加责任。那么如何如何通过代码实现呢?
Coding
生活中到处可以看到装饰者的影子,这里以奶茶店购买奶茶为例,奶茶店提供了很多基本饮料,如奶茶、果汁、咖啡等等,这些就是等会儿我们看到的具体组件,而椰果、冰块、糖等等就是我们的装饰者了。接下来我们先实现一个最基础的装饰者,如果你看到这儿,先停下来思考几个问题:
- 为什么具体组件和装饰者需要实现自抽象组件来保持类型一致?
- 装饰者的优缺点?
- 可以通过其他的什么模式来避免装饰者的不足?
Common
首先是抽象组件Beverage接口(能使用接口的地方就别使用抽象类,尤其是Java8后接口增加了默认的实现方法),它有一个价格方法:
public interface Beverage {
double cost();
}
``
然后是具体组件MilkTea:
```java
public class MilkTea implements Beverage {
@Override
public double cost() {
return 5;
}
}
然后是装饰者一族:
// 使用抽象类是为了隐藏的无参构造,这需要根据具体的业务需求而定,没必要生搬硬套
public abstract class Decorator implements Beverage {
protected Beverage beverage;
public Decorator(Beverage beverage) {
this.beverage = beverage;
}
}
// 椰果
public class Coconut extends Decorator {
public Coconut(Beverage beverage) {
super(beverage);
}
@Override
public double cost() {
// 这里就是装饰者的核心要点,动态的附加责任
return beverage.cost() + 4.3;
}
}
// 珍珠
public class Pearl extends Decorator {
public Pearl(Beverage beverage) {
super(beverage);
}
@Override
public double cost() {
return beverage.cost() + 2.2;
}
}
测试:
public static void main(String[] args) {
Beverage beverage = new MilkTea();
Decorator decorator = new Pearl(beverage);
System.out.println(decorator.cost());
Decorator decorator1 = new Coconut(decorator);
System.out.println(decorator1.cost());
}
这样一个简单的装饰者就实现完成了,如果你有认真思考,相信对于刚刚提的问题中的前两个应该已经有了答案了。其中使用装饰者的最大优点就是可以减少大类(经过装饰者装饰的类,如果都硬编码写到程序里,程序会非常的臃肿)的数量以及重复的代码,但同时也是它的缺点,这会增加非常多的小类(装饰者以及组件,相较于大类,可以灵活的自由组合产生需要的类),也就会增加系统的复杂度,那该如何解决这个问题呢?如果你看过我之前的文章或是熟悉其他模式那么不难想到可以通过工厂模式和生成器模式在一定程度上降低客户端创建对象的复杂度,注意,是一定程度,没有完美的模式。下面我通过工厂模式来说明,至于与生成器或是其他模式的组合就需要你自己去思考了。
WithFactory
沿用上面的类,在此基础上实现一个工厂:
public interface Factory {
Beverage getBeverage();
}
public class CoConutFactory implements Factory {
@Override
public Beverage getBeverage() {
return new Coconut(new MilkTea());
}
}
可以看到客户端只需要创建椰果工厂,并掉用getBeverage()方法就能或取到加椰果的奶茶,对客户来讲也就不用繁复的去创建组件和装饰者,但是缺点也很明显,工厂只能提供热门的预设产品,而不可能提供给用户所有的组合,如果某个用户想要在奶茶中加入额外的牛奶或是糖,那么只能由用户自己再组合一下(其实细想一下MilkTea原本不就是奶茶店预设好的组合商品么)。但总的而言,装饰者模式提供了比继承更好的扩展性,熟悉其原理对于我们的项目扩展非常的有帮助。
IO包中的装饰者
下面是InputStream的体系类图,感兴趣的可以自行画出OutputStream的,与InputStream都是相对应的。
通过装饰者模式来看IO包的结构就很清晰明了了,即使Java内置的IO流不满足我们的需求,我们也能很轻易的扩展还不用破坏原本的结构。
与代理模式的区别
设计模式中对于很多初学者来说是很像的,如果不深入理解他们的设计理念就会感觉很疑惑。比如代理模式也可以增加对象的功能,那么它和装饰者模式的区别究竟是什么呢?这个在笔者之前的代理模式一文中也有提到过,他们之间最主要的区别就是装饰者模式是用来增强对象的行为和职责,而代理模式则是用于控制对象的访问,代理模式这里就不详细阐述了,感兴趣的可以去看笔者之前的文章《设计之禅——深入剖析代理模式》
总结
装饰者模式良好的遵循了对扩展开放,对修改关闭原则,使得我们的系统有更良好的扩展性,但同时也会产生比较多的小类,虽然可以通过和工厂模式以及生成器模式的组合来降低创建对象的复杂度,但这对于不了解该模式的人而言来说就会非常难以理解,平时应该多看多思考才能真正掌握。
- Spark详解01概览|Spark部署|执行原理概览Job 例子
- Spark详解05架构Architecture架构
- SQL Server常用命令(平时不用别忘了)
- Spark详解06容错机制Cache 和 Checkpoint Cache 和 Checkpoint
- SQL Server 学习笔记
- Collaborative Filtering(协同过滤)算法详解
- 【Hadoop】三句话告诉你 mapreduce 中MAP进程的数量怎么控制?
- Spark系列课程-00xxSpark RDD持久化
- RDD持久化
- P02_Hadoop CDH 5.3.6集群搭建
- P01_Spark开发测试运行环境安装Spark开发测试运行环境安装
- spark2.x依赖包POM
- P03_Hive 安装
- P06_flume-ng-1.5.0-cdh5.3.6安装
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Spring 整合 Mybatis
- 数据库PostrageSQL-关闭服务器
- 快速配置Azure DevOps代理服务器
- 数据库PostrageSQL-管理内核资源
- airtest操作夜神模拟器adb冲突解决办法
- 数据库PostrageSQL-启动数据库服务器
- 数据库PostrageSQL-PostgreSQL用户账户创建一个数据库集簇
- 轻松上手SpringBoot Security + JWT Hello World示例
- [Go] Golang发送http GET请求
- Windows系统快速安装Superset 0.37
- 商业数据分析从入门到入职(3)Excel进阶应用
- python列表练习
- python元组
- python字典、集合
- 秒懂JVM的三大参数类型,就靠这十个小实验了