第三篇 构建复杂结构

    我们知道,一个类不能完成一个系统的全部功能,为了完成某些复杂的功能,我们往往需要多个对象组成更大更复杂的结构来协作完成。使用面向对象来合成更大单元的方式,主要有合成、继承、还有像Ruby语言的Mixin 11[1]等,我们这里主要讨论合成和继承两种。

    通常情况下,为了使用现有类的功能,避免代码重复,我们可以继承该类,从而具有父类的性质;或者使用合成,执行它的功能。到底那种方式更好呢?或者是否需要结合二者呢?本部分内容会给你提供建议。

    第7章 装饰器(Decorator)模式

    提到装饰器模式,使用过Java IO编程的人应该非常熟悉,Java IO包就是使用装饰器模式设计的。譬如,为了快速地从InputStream流读取数据,我们使用BufferedInputStream装饰该流,被它装饰过的流便增加了缓冲数据的功能。装饰器模式可以让我们在运行时动态地增强对象的功能,而不影响客户程序的使用。

    本章将介绍一个记录操作日志的案例,它包括如下内容:

    描述一个记录修改日志的真实案例。

    开放—封闭原则(Open-Close Principle)。

    灵活地使用装饰器模式解决这个案例。

    装饰器模式的优缺点。

    7.1 记录历史修改

    几年前,我曾接到过一个任务,需要开发一个数据访问层(Data Access Layer),提供对持久化数据的访问。我们当时使用的是关系数据库(Relational Database),所以选用了Hibernate这款非常著名的ORM [2](Object-Relational Mapping)工具来实现持久化。

    刚开始,我编写了一个非常简单的接口GenericRepository用于完成CRUD操作[3],代码如下所示。

    figure_0110_0099

    幸亏有Hibernate工具,让我们的实现变得异常简单了:使用Hibernate Session对象的save(To)方法可以根据ORM映射关系把对象持久化到数据库,它的update(To)方法可以更新持久化的数据,其他方法就不再一一叙述,Hibernate官方文档非常全面,感兴趣的读者可以登录官方网站下载相关文档进行学习。

    我们的代码大致如下所示。

    figure_0111_0100

    figure_0112_0101

    我们的方法getCurrentSession()用于得到Hibernate的Session对象,然后调用Session对象的相应方法就可以创建和更新数据了。

    在4.1.2节,我们讲述了如何在Web应用中使用ThreadLocal类给当前线程绑定一个Connection对象,我们这里的getCurrentSession()方法就是使用ThreadLocal给当前线程绑定一个Session对象,关于此类的使用,请参见ThreadLocal类的介绍。

    随着开发的深入,几个星期之后,用户希望对某些业务操作进行回滚,为了实现回滚,我们要对修改前的数据记录日志,这样才能完成回滚。比如,他们的这些操作会影响用户账户状态,我们就为用户账户信息记录日志,把数据存储在user这张表中,把日志记录在另外一张日志表user_log中。

    在刚开始的讨论中,很多人想到了直接修改GenericRepositoryHibernate这个类,这样,update(T o)方法的代码便会被修改为如下所示。

    figure_0112_0102

    这种方式对现有类进行了修改,但对之前健壮的代码进行修改,很可能导致这些代码不再健壮。为了保证以前的代码能够正常执行,我们必须对该类重新做单元测试,更麻烦的是,由于此方法非常基础,为了保证不影响到其他功能,我们还要为所有受影响的其他类和方法以及它们的单元测试代码进行修改,这一工作量相当大。能否不修改这些健壮的代码又能扩展它的功能呢?我们首先看看一条设计原则——OCP原则(OpenClose Principle)。

    [1] Mixin是一种复用代码的方式,可以把不同模块的代码通过include关键字引入。

    [2]对象关系映射(ObjectRelational Mapping):由于关系数据库和面向对象的数据之间存在不匹配现象,为了像使用面向对象数据库那样使用关系数据库,我们使用这方面技术为面向对象编程虚拟出对象数据库以供数据访问。

    [3]是指四个最基础的持久化操作:Create、Read、Update 和Delete操作,缩写为CRUD。