• 【23种设计模式】装饰器模式


    个人主页金鳞踏雨

    个人简介:大家好,我是金鳞,一个初出茅庐的Java小白

    目前状况:22届普通本科毕业生,几经波折了,现在任职于一家国内大型知名日化公司,从事Java开发工作

    我的博客:这里是CSDN,是我学习技术,总结知识的地方。希望和各位大佬交流,共同进步 ~

    使用组合替代继承,对原生对象的方法做增强,增加新的行为和能力。

    一、实现原理

    装饰器设计模式(Decorator)是一种结构型设计模式,它允许动态地为对象添加新的行为。它通过创建一个包装器来实现,即将对象放入一个装饰器类中,再将装饰器类放入另一个装饰器类中,以此类推,形成一条包装链

    不改变原有对象的情况下,动态地添加新的行为或修改原有行为

    代码实现

    1、定义一个接口或抽象类,作为被装饰对象的基类。

    1. public interface Component {
    2. void operation();
    3. }

    在这个示例中,我们定义了一个名为 Component 的接口,它包含一个名为 operation 的抽象方法,用于定义被装饰对象的基本行为。

    2、定义一个具体的被装饰对象,实现基类中的方法。

    1. public class ConcreteComponent implements Component {
    2. @Override
    3. public void operation() {
    4. System.out.println("ConcreteComponent is doing something...");
    5. }
    6. }

    在这个示例中,我们定义了一个名为 ConcreteComponent 的具体实现类,实现了 Component 接口中的 operation 方法。

    3、定义一个抽象装饰器类,继承基类,并将被装饰对象作为属性

    1. public abstract class Decorator implements Component {
    2. // 装饰器设计模式 使用组合的形式进行装饰
    3. protected Component component;
    4. public Decorator(Component component) {
    5. this.component = component;
    6. }
    7. @Override
    8. public void operation() {
    9. component.operation();
    10. }
    11. }

    在这个示例中,我们定义了一个名为 Decorator 的抽象类,继承了 Component 接口,并将被装饰对象作为属性。在 operation 方法中,我们调用被装饰对象的同名方法。

    4、定义具体的装饰器类,继承抽象装饰器类,并实现增强逻辑

    1. public class ConcreteDecoratorA extends Decorator {
    2. public ConcreteDecoratorA(Component component) {
    3. System.out.println("这是第一次包装...");
    4. super(component);
    5. }
    6. @Override
    7. public void operation() {
    8. super.operation();
    9. System.out.println("ConcreteDecoratorA is adding new behavior...");
    10. }
    11. }
    12. public class ConcreteDecoratorB extends Decorator {
    13. public ConcreteDecoratorB(Component component) {
    14. System.out.println("这是第二次包装...");
    15. super(component);
    16. }
    17. @Override
    18. public void operation() {
    19. super.operation();
    20. System.out.println("ConcreteDecoratorB is adding new behavior...");
    21. }
    22. }

    在这个示例中,我们定义了一个名为 ConcreteDecoratorA 的具体装饰器类,继承了 Decorator 抽象类,并实现了 operation 方法的增强逻辑。在 operation 方法中,我们先调用被装饰对象的同名方法,然后添加新的行为。

    5、使用装饰器增强被装饰对象。

    1. public class Main {
    2. public static void main(String[] args) {
    3. Component component = new ConcreteComponent();
    4. // 进行第一次包装
    5. component = new ConcreteDecoratorA(component);
    6. // 进行第二次包装
    7. component = new ConcreteDecoratorB(component);
    8. component.operation();
    9. }
    10. }

    在这个示例中,我们先创建了一个被装饰对象 ConcreteComponent,然后通过 ConcreteDecoratorA 类创建了一个装饰器,并将被装饰对象作为参数传入。最后,调用装饰器的 operation 方法,这样就可以实现对被装饰对象的增强。

    装饰器设计模式,包装后可以继续包装...类似于套娃。一层一层,逐步增强!!!

    装饰器与静态代理的区别

    装饰器模式静态代理模式最大的区别——目的不同!

    代理模式的目的是为了控制对对象的访问,它在对象的外部提供一个代理对象来控制对原始对象的访问。代理对象和原始对象通常实现同一个接口或继承同一个类,以保证二者可以互相替代。

    装饰器模式的目的是为了动态地增强对象的功能,它在对象的内部通过一种包装器的方式来实现。装饰器模式中,装饰器类和被装饰对象通常实现同一个接口或继承同一个类,以保证二者可以互相替代。装饰器模式也被称为包装器模式。

    需要注意的是,装饰器模式虽然可以实现动态地为对象增加行为,但是会增加系统的复杂性,因此在使用时需要仔细权衡利弊。

    二、使用场景

    在 Java 中,装饰器模式的应用非常广泛,特别是在 I/O 操作中。Java 中的 I/O 类库就是使用装饰器模式来实现不同的数据流之间的转换和增强的。

    1.从IO库的设计理解装饰器

    打开文件 test.txt,从中读取数据。

    其中,InputStream 是一个抽象类,FileInputStream 是专门用来读取文件流的子类。BufferedInputStream 是一个支持带缓存功能的数据读取类,可以提高数据读取的效率

    1. InputStream in = new FileInputStream("D:/test.txt");
    2. InputStream bin = new BufferedInputStream(in);
    3. byte[] data = new byte[128];
    4. while (bin.read(data) != -1) {
    5. //...
    6. }

    初看上面的代码,我们会觉得 Java IO 的用法比较麻烦,需要先创建一个 FileInputStream 对象,然后再传递给 BufferedInputStream 对象来使用。

    为什么Java IO 为什么不设计一个继承 FileInputStream 并且支持缓存的 BufferedFileInputStream 类呢?

    这样直接创建一个 BufferedFileInputStream 类对象,打开文件读取数据,用起来岂不是更加简单?

    1. InputStream bin = new BufferedFileInputStream("/user/wangzheng/test.txt");
    2. byte[] data = new byte[128];
    3. while (bin.read(data) != -1) {
    4. //...
    5. }

    (1)基于继承的设计方案(能实现,不靠谱)

    如果 InputStream 只有一个子类 FileInputStream 的话,那我们在 FileInputStream 基础之上,再设计一个孙子类 BufferedFileInputStream,这样子是完全可以的,毕竟继承结构还算简单!

    但实际上,继承 InputStream 的子类有很多。我们需要给每一个 InputStream 的子类,再继续派生支持缓存读取的子类。

    除了支持缓存读取之外,如果我们还需要对功能进行其他方面的增强,比如下面的 DataInputStream 类,支持按照基本数据类型(int、boolean、long 等)来读取数据。

    1. FileInputStream in = new FileInputStream("/user/wangzheng/test.txt");
    2. DataInputStream din = new DataInputStream(in);
    3. int data = din.readInt();

    在这种情况下,如果我们继续按照继承的方式来实现的话,就需要再继续派生出 DataFileInputStream、DataPipedInputStream 等类。如果我们还需要既支持缓存、又支持按照基本类型读取数据的类,那就要再继续派生出 BufferedDataFileInputStream、BufferedDataPipedInputStream 等 n 多类。

    这还只是附加了两个增强功能,如果我们需要附加更多的增强功能,那就会导致组合爆炸,类继承结构变得无比复杂,代码既不好扩展,也不好维护

    (2)基于装饰器模式的设计方案

    组合优于继承,建议使用组合来替代继承!!!

    针对刚刚的继承结构过于复杂的问题,我们可以通过将继承关系改为组合关系来解决。

    1. public abstract class InputStream {
    2. //...
    3. public int read(byte b[]) throws IOException {
    4. return read(b, 0, b.length);
    5. }
    6. public int read(byte b[], int off, int len) throws IOException {
    7. //...
    8. }
    9. public long skip(long n) throws IOException {
    10. //...
    11. }
    12. public int available() throws IOException {
    13. return 0;
    14. }
    15. public void close() throws IOException {}
    16. public synchronized void mark(int readlimit) {}
    17. public synchronized void reset() throws IOException {
    18. throw new IOException("mark/reset not supported");
    19. }
    20. public boolean markSupported() {
    21. return false;
    22. }
    23. }
    24. public class BufferedInputStream extends InputStream {
    25. protected volatile InputStream in;
    26. protected BufferedInputStream(InputStream in) {
    27. this.in = in;
    28. }
    29. //...实现基于缓存的读数据接口...
    30. }
    31. public class DataInputStream extends InputStream {
    32. protected volatile InputStream in;
    33. protected DataInputStream(InputStream in) {
    34. this.in = in;
    35. }
    36. //...实现读取基本类型数据的接口
    37. }
    那装饰器模式就是简单的“用组合替代继承”吗?

    当然不是!从 Java IO 的设计来看,装饰器模式相对于简单的组合关系,还有两个比较特殊的地方!

    1、装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类。

    比如,下面这样一段代码,我们对 FileInputStream 嵌套了两个装饰器类:BufferedInputStream 和 DataInputStream,让它既支持缓存读取,又支持按照基本数据类型来读取数据。

    1. // 逐次增强!
    2. InputStream in = new FileInputStream("/user/wangzheng/test.txt");
    3. InputStream bin = new BufferedInputStream(in);
    4. DataInputStream din = new DataInputStream(bin);
    5. int data = din.readInt();

    2、装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。

    实际上,符合“组合关系”这种代码结构的设计模式有很多,比如之前讲过的代理模式,还有现在的装饰器模式。尽管它们的代码结构很相似,但是每种设计模式的意图是不同的!!!

    例如,代理模式中,代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。

    1. // 代理模式的代码结构(下面的接口也可以替换成抽象类)
    2. public interface IA {
    3. void f();
    4. }
    5. public class A impelements IA {
    6. public void f() {
    7. //...
    8. }
    9. }
    10. public class AProxy impements IA {
    11. private IA a;
    12. public AProxy(IA a) {
    13. this.a = a;
    14. }
    15. public void f() {
    16. // 新添加的代理逻辑
    17. a.f();
    18. // 新添加的代理逻辑
    19. }
    20. }
    21. // 装饰器模式的代码结构(下面的接口也可以替换成抽象类)
    22. public interface IA {
    23. void f();
    24. }
    25. public class A impelements IA {
    26. public void f() {
    27. // ...
    28. }
    29. }
    30. public class ADecorator impements IA {
    31. private IA a;
    32. public ADecorator(IA a) {
    33. this.a = a;
    34. }
    35. public void f() {
    36. // 功能增强代码
    37. a.f();
    38. // 功能增强代码
    39. }
    40. }
    BufferedInputStream为什么不直接继承InputStream?

    实际上,如果去查看 JDK 的源码,你会发现,BufferedInputStream、DataInputStream 并非继承自 InputStream,而是另外一个叫 FilterInputStream 的类。那这又是出于什么样的设计意图,才引入这样一个类呢?

    InputStream 是一个抽象类而非接口,而且它的大部分函数(比如 read()、available())都有默认实现,按理来说,我们只需要在 BufferedInputStream 类中重新实现那些需要增加缓存功能的函数就可以了,其他函数继承 InputStream 的默认实现。但实际上,这样做是行不通的!

    对于即便是不需要增加缓存功能的函数来说,BufferedInputStream 还是必须把它重新实现一遍,简单包裹对 InputStream 对象的函数调用;如果不重新实现,那 BufferedInputStream 类就无法将最终读取数据的任务,委托给传递进来的 InputStream 对象来完成。

    1. public class BufferedInputStream extends InputStream {
    2. protected volatile InputStream in;
    3. protected BufferedInputStream(InputStream in) {
    4. this.in = in;
    5. }
    6. // f()函数不需要增强,只是重新调用一下InputStream in对象的f()
    7. public void f() {
    8. in.f();
    9. }
    10. }

    实际上,DataInputStream 也存在跟 BufferedInputStream 同样的问题。为了避免代码重复,Java IO 抽象出了一个装饰器父类 FilterInputStream。

    InputStream 的所有的装饰器类(BufferedInputStream、DataInputStream)都继承自这个装饰器父类。这样,装饰器类只需要实现它需要增强的方法就可以了,其他方法继承装饰器父类的默认实现。

    1. public class FilterInputStream extends InputStream {
    2. protected volatile InputStream in;
    3. protected FilterInputStream(InputStream in) {
    4. this.in = in;
    5. }
    6. public int read() throws IOException {
    7. return in.read();
    8. }
    9. public int read(byte b[]) throws IOException {
    10. return read(b, 0, b.length);
    11. }
    12. public int read(byte b[], int off, int len) throws IOException {
    13. return in.read(b, off, len);
    14. }
    15. public long skip(long n) throws IOException {
    16. return in.skip(n);
    17. }
    18. public int available() throws IOException {
    19. return in.available();
    20. }
    21. public void close() throws IOException {
    22. in.close();
    23. }
    24. public synchronized void mark(int readlimit) {
    25. in.mark(readlimit);
    26. }
    27. public synchronized void reset() throws IOException {
    28. in.reset();
    29. }
    30. public boolean markSupported() {
    31. return in.markSupported();
    32. }
    33. }

    总的来说,当 BufferedInputStream 继承自 FilterInputStream 时,它可以非常轻松地扩展 FilterInputStream 的行为,从而实现了输入流的缓冲功能。如果 BufferedInputStream 直接继承自 InputStream,那么它就需要重新实现所有 InputStream 的方法,包括一些可能不需要修改的方法,这会使代码变得复杂且容易出错。

    2.MyBatis的缓存设计

    创建缓存的过程

    useNewCache方法用于配置Mapper是否使用一个全新的缓存实例,而不共享缓存实例

    1. public Cache useNewCache(Class typeClass,
    2. Class evictionClass,
    3. Long flushInterval,
    4. Integer size,
    5. boolean readWrite,
    6. boolean blocking,
    7. Properties props) {
    8. Cache cache = new CacheBuilder(currentNamespace)
    9. // 根据类型生成实例,并进行配置
    10. .implementation(valueOrDefault(typeClass, PerpetualCache.class))
    11. .addDecorator(valueOrDefault(evictionClass, LruCache.class)) // 添加装饰器
    12. .clearInterval(flushInterval)
    13. .size(size)
    14. .readWrite(readWrite)
    15. .blocking(blocking)
    16. .properties(props)
    17. .build();
    18. configuration.addCache(cache);
    19. currentCache = cache;
    20. return cache;
    21. }

    默认的缓存如下,本质就是维护了一个简单的HashMap

    1. public class PerpetualCache implements Cache {
    2. private final String id;
    3. private final Map cache = new HashMap<>();
    4. public PerpetualCache(String id) {
    5. this.id = id;
    6. }
    7. @Override
    8. public void putObject(Object key, Object value) {
    9. cache.put(key, value);
    10. }
    11. @Override
    12. public Object getObject(Object key) {
    13. return cache.get(key);
    14. }
    15. // ...省略其他的简单的方法
    16. }

    缓存的构建过程

    1. public Cache build() {
    2. // 设置默认的cache实现,并绑定默认的淘汰策略
    3. setDefaultImplementations();
    4. // 利用反射创建实例
    5. Cache cache = newBaseCacheInstance(implementation, id);
    6. // 设置properties属性
    7. setCacheProperties(cache);
    8. // 不应用装饰自定义缓存,自定义缓存需要自己实现对应的特性,如淘汰策略等
    9. // 通常情况自定义缓存有自己的独立配置,如redis、ehcache
    10. if (PerpetualCache.class.equals(cache.getClass())) {
    11. for (Classextends Cache> decorator : decorators) {
    12. cache = newCacheDecoratorInstance(decorator, cache);
    13. setCacheProperties(cache);
    14. }
    15. // 这是标准的装饰器,这里使用了装饰器设计模式
    16. cache = setStandardDecorators(cache);
    17. } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
    18. cache = new LoggingCache(cache);
    19. }
    20. return cache;
    21. }

    MyBatis会使用装饰者设计模式,对默认cache进行装饰,使其具有LRU的能力

    1. private void setDefaultImplementations() {
    2. if (implementation == null) {
    3. implementation = PerpetualCache.class;
    4. if (decorators.isEmpty()) {
    5. // decorators是成员变量,装饰器,饰器具备LRU的能力
    6. decorators.add(LruCache.class);
    7. }
    8. }
    9. }

    LruCache实现如下,默认情况下的LRU算法实现是基于LinkedHashMap实现的

    1. public class LruCache implements Cache {
    2. // 代理目标缓存
    3. private final Cache delegate;
    4. private Map keyMap;
    5. private Object eldestKey;
    6. // LruCache用来装饰默认的缓存,这里实现了缓存的高级特性
    7. public LruCache(Cache delegate) {
    8. this.delegate = delegate;
    9. setSize(1024);
    10. }
    11. @Override
    12. public String getId() {
    13. return delegate.getId();
    14. }
    15. @Override
    16. public int getSize() {
    17. return delegate.getSize();
    18. }
    19. // 设置长度,构建一个LinkedHashMap,重写removeEldestEntry
    20. public void setSize(final int size) {
    21. // 第三个参数accessOrder为true,可以使LinkedHashMap维护一个【访问顺序】
    22. // 最近被访问的数据会被放在链表的尾部,天然实现lru
    23. keyMap = new LinkedHashMap(size, .75F, true) {
    24. private static final long serialVersionUID = 4267176411845948333L;
    25. // 重写该方法,父类直接返回false
    26. // 只要实际容量size() 大于 初始化容量 size 认定当前的缓存已经满了
    27. // 该方法会在LinkedHashMap的afterNodeInsertion方法中被主动调用
    28. // 会将头节点当作eldest删除
    29. @Override
    30. protected boolean removeEldestEntry(Map.Entry eldest) {
    31. boolean tooBig = size() > size;
    32. if (tooBig) {
    33. // 同时将这个这个key复制给成员变量eldestKey
    34. eldestKey = eldest.getKey();
    35. }
    36. return tooBig;
    37. }
    38. };
    39. }
    40. // put一个缓存的过程
    41. // 放入当前的缓存值,淘汰eldestKey
    42. @Override
    43. public void putObject(Object key, Object value) {
    44. delegate.putObject(key, value);
    45. cycleKeyList(key);
    46. }
    47. // get一个缓存的过程
    48. // 获得该值,同时提升key热度,主动访问一下keyMap.get(key)
    49. @Override
    50. public Object getObject(Object key) {
    51. keyMap.get(key);
    52. return delegate.getObject(key);
    53. }
    54. @Override
    55. public Object removeObject(Object key) {
    56. return delegate.removeObject(key);
    57. }
    58. @Override
    59. public void clear() {
    60. delegate.clear();
    61. keyMap.clear();
    62. }
    63. // 循环key的集合
    64. private void cycleKeyList(Object key) {
    65. keyMap.put(key, key);
    66. if (eldestKey != null) {
    67. delegate.removeObject(eldestKey);
    68. eldestKey = null;
    69. }
    70. }
    71. }

    最后使用其他的装饰器对cache进行装饰,使其就有更多的能力

    1. private Cache setStandardDecorators(Cache cache) {
    2. try {
    3. MetaObject metaCache = SystemMetaObject.forObject(cache);
    4. // 设置大小,默认1024
    5. if (size != null && metaCache.hasSetter("size")) {
    6. metaCache.setValue("size", size);
    7. }
    8. if (clearInterval != null) {
    9. cache = new ScheduledCache(cache);
    10. ((ScheduledCache) cache).setClearInterval(clearInterval);
    11. }
    12. if (readWrite) {
    13. cache = new SerializedCache(cache);
    14. }
    15. cache = new LoggingCache(cache);
    16. cache = new SynchronizedCache(cache);
    17. if (blocking) {
    18. cache = new BlockingCache(cache);
    19. }
    20. return cache;
    21. } catch (Exception e) {
    22. throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
    23. }
    24. }

    三、总结

    装饰器模式主要解决继承关系过于复杂的问题,通常是通过组合来替代继承。

    它主要的作用是给原始类添加功能。这也是判断是否该用装饰器模式的一个重要的依据。除此之外,装饰器模式还有一个特点,那就是可以对原始类嵌套使用多个装饰器。为了满足这个应用场景,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接口。

    Q: 在学习代理设计模式的时候,我们通过装饰者模式给 InputStream 添加缓存读取数据功能。那对于“添加缓存”这个应用场景来说,我们到底是该用代理模式还是装饰器模式呢?你怎么看待这个问题?

    事实上对于绝大多数的"添加缓存"的业务场景,核心目的主要就是想增强对象的功能(即增加缓存功能),而并不是控制对对象的访问,所以装饰器模式可能会更合适。但是假如,我们想强制对持久层增加一个本地缓存,代理设计模式也是很好的选择。

    文章到这里就结束了,如果有什么疑问的地方,可以在评论区指出~

    希望能和大佬们一起努力,诸君顶峰相见

    再次感谢各位小伙伴儿们的支持!!!

  • 相关阅读:
    Java Web DTO 以及 VO 等实际意义以及作用
    微宏科技基于 KubeSphere 的微服务架构实践
    DevOps | 产研协同效能提升之评审、审批流、质量卡点
    Unity 最近经验分享
    Session会话追踪的实现机制
    UWB高精度定位系统:引领精准定位技术的新纪元
    nginx优化--压缩
    java计算机毕业设计课堂互动应答系统mp4源码+mysql数据库+系统+lw文档+部署
    c语言练习92:链表的中间结点
    数据库——模式分解和关系范式的分解
  • 原文地址:https://blog.csdn.net/weixin_43715214/article/details/133896753