设计模式--装饰者模式思考

装饰者模式实际上是一直提倡的组合代替继承的实践方式,个人认为要理解装饰者模式首先需要理解为什么需要组合代替继承,继承又是为什么让人深恶痛绝.

为什么建议使用组合代替继承?

面向对象的特性有继承与封装,但两者却又有一点矛盾,继承意味子类依赖了父类中的实现,一旦父类中改变实现则会对子类造成影响,这是打破了封装性的一种表现.
而组合就是巧用封装性来实现继承功能的代码复用.
举一个Effective Java中的案例,当前需求是为HashSet提供一个计数,要求统计它创建以来曾经添加了多少个元素,那么可以写出下面的代码.

public class InstrumentedHashSet <E> extends HashSet<E> {

  private int addCount = 0;

  @Override
  public boolean add(E e) {
    this.addCount++;
    return super.add(e);
  }

  @Override
  public boolean addAll(Collection<? extends E> c) {
    this.addCount += c.size();
    return super.addAll(c);
  }
  public int getAddCount() {
    return this.addCount;
  }
}

下面测试代码会抛出异常,正确结果是6,是不是匪夷所思,这种匪夷所思需要你去看HashSet的具体实现,其addAll实际上是调用了add方法.

  InstrumentedHashSet<String> hashSet = new InstrumentedHashSet<>();
    hashSet.addAll(Arrays.asList("张三", "李四", "王二"));
    Assert.assertEquals(hashSet.getAddCount(), 3);

这个案例说明了继承导致子类变得很脆弱,其不知道父类的细节,但是却实实在在的依赖了父类的实现.出现了问题也很难找出bug.
那么换成组合模式,让InstrumentedHashSet持有HashSet的私有实例,add以及addAll方法由HashSet的私有实例代理执行.这就是组合所带来的优势,充分利用其它类的特点,降低耦合度,我只需要你已完成的功能,相比继承而并不受到你内部实现的制约.

public class InstrumentedHashSet <E>{

  private int addCount = 0;

  private HashSet<E> hashSet = new HashSet<>();

  public boolean add(E e) {
    this.addCount++;
    return hashSet.add(e);
  }

  public boolean addAll(Collection<? extends E> c) {
    this.addCount += c.size();
    return hashSet.addAll(c);
  }

  public int getAddCount() {
    return this.addCount;
  }
}

装饰者模式

装饰者模式定义为:动态的给一对象添加一些额外的职责,对该对象进行功能性的增强.(只是增强,并没有改变使用原对象的意图)
装饰器模式类图:

image

以上是标准的装饰器模式,其中AbstractDecorator为一个装饰器模板,目的是为了提高代码复用,简化具体装饰器子类的实现成本,当然不需要的话也是可以省略的,其最主要的功能是持有了ComponentInterface这个被装饰者对象,然后子类可以利用类似AOP环绕通知形式来在被装饰类执行sayHello()前后执行自己的逻辑.这是装饰者模式的本质.

比如ContreteDecoratorA增强了sayHello()

public class ContreteDecoratorA extends AbstractDecorator {

  public ContreteDecoratorA(ComponentInterface componentInterface) {
    super(componentInterface);
  }

  @Override
  public void sayHello() {
    System.out.println("A start");
    super.sayHello();
    System.out.println("A end");
  }
}

具体使用方式

 public static void main(String[] args) {
    final ContreteDecoratorA decoratorA = new ContreteDecoratorA(new ComponentInterfaceImpl());
    decoratorA.sayHello();
  }

输出

A start
hello world
A end

其中默认实现ComponentInterfaceImpl的sayHello()功能被装饰后增强.

Java I/O与装饰者

字节流

Java I/O框架就是一个很好的装饰者模式的实例.如下InputStream关系图

image

其中FileInputStream,ObjectInputStream等直接实现类提供了最基本字节流读取功能.
FilterInputStream作为装饰者,其内部引用了另一个InputStream(实际被装饰的对象),然后以AOP环绕通知的形式来进行功能增强,笔者认为这里应该把该类定义为abstract更为合适.其承担的角色只是代码复用,帮助具体的装饰者类更加容易的实现功能增强.
image

具体的装饰者BufferedInputStream为其他字节流提供了缓冲输入的支持.DataInputStream则提供了直接解析Java原始数据流的功能.

由于装饰者模式的存在,原本一个字节一个字节读的FileInputStream只需要嵌套一层BufferedInputStream即可支持缓冲输入,

    BufferedInputStream br = new BufferedInputStream(new FileInputStream(new File("path")));

字符流

相比较字节流,字符流这边的关系则有点混乱,主要集中在BufferedReaderFilterReader,其两个角色都是装饰者,而FilterReader是更加基本的装饰者其相对于字节流中的FilterInputStream已经升级为abstract了,目的就是便于具体装饰者实现类更加容易的编写.那么为什么BufferedReader不继承FilterReader呢?这个问题暂时不知道答案,有兴趣的可以关注下知乎,等大牛回答.
为什么BufferedReader 不是 FilterReader的子类,而直接是Reader的子类?

不过从另一个角度来说,设计模式并不是套用模板,其最主要的是思想,对于装饰者模式最重要的是利用组合代替了继承,原有逻辑交给内部引用的类来实现,而自己只做增强功能,只要符合这一思想都可以称之为装饰者模式.


image

Mybatis与装饰者

Mybatis中有不少利用到装饰者模式,比如二级缓存Cache,另外其Executor也正在朝着装饰者模式改变.这里以Cache接口为主,类图如下:

image

从类图来看和装饰者模式似乎无半毛钱关系,实际上其省略了AbstractDecorator这一公共的装饰者基类.那么要实现装饰者其实现类中必须有一个Cache的被装饰对象,以LruCache为例.

public class LruCache implements Cache {

  private final Cache delegate;
  private Map<Object, Object> keyMap;
  private Object eldestKey;
  
  @Override
  public String getId() {
    return delegate.getId();
  }
  ....
}

其内部拥有Cache delegate这一被装饰者,也就是无论什么Cache,只要套上了LruCache那么就有了LRU这一特性.
org.apache.ibatis.mapping.CacheBuilder#setStandardDecorators构造时则根据配置参数来决定增强哪些功能,下面代码则很好的体现了装饰者模式的优势,还望好好体会.

  private Cache setStandardDecorators(Cache cache) {
    try {
      MetaObject metaCache = SystemMetaObject.forObject(cache);
      if (size != null && metaCache.hasSetter("size")) {
        metaCache.setValue("size", size);
      }
      if (clearInterval != null) {
        cache = new ScheduledCache(cache);
        ((ScheduledCache) cache).setClearInterval(clearInterval);
      }
      if (readWrite) {
        cache = new SerializedCache(cache);
      }
      cache = new LoggingCache(cache);
      cache = new SynchronizedCache(cache);
      if (blocking) {
        cache = new BlockingCache(cache);
      }
      return cache;
    } catch (Exception e) {
      throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
  }

总结

装饰者模式实际上是继承的一种另类替代方式,以持有同类对象来达到继承的目的,同时由于多态的存在,使其比继承更加灵活多变.
对象包裹代理的过程可以理解为递归调用,其增强行为则类似AOP的环绕通知,理解了这些装饰者模式就很容易掌握了.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349

推荐阅读更多精彩内容