返回介绍

1.6 装饰器模式

发布于 2025-01-04 00:44:54 字数 9657 浏览 0 评论 0 收藏 0

装饰器模式(Decorator Pattern) 可以在不改变一个对象的基本接口的情况装饰其行为。当需要原始对象(未被装饰对象)时,被装饰对象可以被替换。装饰行为通常并不会包含对原始对象源码的修改,装饰器应该能以各种灵活的手段进行组合,从而形成带有多种装饰行为的对象。

1.6.1 典型范例

假设现在有一个 Logger 类:

class Logger {
  def log(String message) {
    println message
  }
}

有时为日志记录添加时间戳是非常有用的,或者可能想改变消息的大小写格式。我们可能会试图将所有的功能都塞进 Logger 类中。这会使得 Logger 类变得十分复杂臃肿。另外,每一个人都将获得所有的功能,即使你不想要其中的某个小子集,也毫无办法。最后,功能交互就会变得极难控制。

为了克服这些缺点,定义两个装饰器类。 Logger 类的用途在于可自由地装饰它们的基本记录器,其中可按照任何预想顺序用到多个(或不用)装饰器类。这样的类如下所示:

class TimeStampingLogger extends Logger {
  private Logger logger
  TimeStampingLogger(logger) {
    this.logger = logger
  }
  def log(String message) {
    def now = Calendar.instance
    logger.log("$now.time: $message")
  }
}

class UpperLogger extends Logger {
  private Logger logger
  UpperLogger(logger) {
    this.logger = logger
  }
  def log(String message) {
    logger.log(message.toUpperCase())
  }
}

可以像下面这样使用装饰器:

def logger = new UpperLogger(new TimeStampingLogger(new Logger()))
logger.log("G'day Mate")
// => Tue May 22 07:13:50 EST 2007: G'DAY MATE

由上可见,我们用了两个装饰器来装饰 logger 的行为。根据我们选用的装饰器使用顺序,日志消息最终结果是全部大写字母,而时间戳则还是正常的格式。如果我们交换顺序,则结果如下:

logger = new TimeStampingLogger(new UpperLogger(new Logger()))
logger.log('Hi There')
// => TUE MAY 22 07:13:50 EST 2007: HI THERE

注意时间戳本身也被改成了大写。

1.6.2 探讨一下动态行为

之前的装饰器都是 Logger 类所特有的。我们可以利用 Groovy 的元编程特性创建一个装饰器,使其自然具有更通用的功用,比如像下面这个类:

class GenericLowerDecorator {
  private delegate
  GenericLowerDecorator(delegate) {
    this.delegate = delegate
  }
  def invokeMethod(String name, args) {
    def newargs = args.collect { arg ->
      if (arg instanceof String) {
        return arg.toLowerCase()
      } else {
        return arg
      }
    }
    delegate.invokeMethod(name, newargs)
  }
}

它将接受任何类并对其进行装饰,从而使任何 String 方法的参数都能自动变更为小写。

logger = new GenericLowerDecorator(new TimeStampingLogger(new Logger()))
logger.log('IMPORTANT Message')
// => Tue May 22 07:27:18 EST 2007: important message

只是要注意这里的顺序。原始的装饰器被局限于只能装饰 Logger 对象。这个装饰器适用于任何对象类型,所以不能将顺序调换,比如不能像下面这样:

// Can't mix and match Interface-Oriented and Generic decorators
// logger = new TimeStampingLogger(new GenericLowerDecorator(new Logger()))

在运行时生成一个正确的 Proxy 类型,就可以克服这个限制,但在这个例子中,我们不想搞得那么复杂。

1.6.3 运行时行为装饰

你现在可能还在认为,像从 Groovy 1.1 开始一直所采用的那样,利用 ExpandoMetaClass 来动态装饰类的行为。这虽然不是装饰器模式通常的风格(当然谈不上有多灵活),但却可能在有些场合帮你实现相似的结果,而无需创建新类。

下面是范例代码:

// ExpandoMetaClass 的当前使用机制
GroovySystem.metaClassRegistry.metaClassCreationHandle = new ExpandoMetaClassCreationHandle()

def logger = new Logger()
logger.metaClass.log = { String m -> println 'message: ' + m.toUpperCase() }
logger.log('x')
// => message: X

这样做虽然也和应用了一个单独的装饰器的效果相似,但却无法轻松地立刻使用并去除装饰。

1.6.4 更多动态装饰

假设有一个计算器类(实际上任何类都可以当做范例)。

class Calc {
  def add(a, b) { a + b }
}

我们可能会需要观察类在一段时间的使用情况。如果它深埋在代码基中,可能很难判断它是何时调用的以及所用的参数。另外,也很难知道它是否成功执行。我们可以轻松地创建一个通用的跟踪装饰器,只要 Calc 类上的任何方法一被调用,就让它打印出跟踪信息,以及提供方法执行的时间信息。下面就是这个跟踪装饰器的代码:

class TracingDecorator {
  private delegate
  TracingDecorator(delegate) {
    this.delegate = delegate
  }
  def invokeMethod(String name, args) {
    println "Calling $name$args"
    def before = System.currentTimeMillis()
    def result = delegate.invokeMethod(name, args)
    println "Got $result in ${System.currentTimeMillis()-before} ms"
    result
  }
}

下面是如何在脚本中使用这个类:

def tracedCalc = new TracingDecorator(new Calc())
assert 15 == tracedCalc.add(3, 12)

运行该脚本,所得结果如下:

Calling add{3, 12}
Got 15 in 31 ms

1.6.5 利用拦截器进行装饰

上面的计时范例与 Groovy 对象的生命周期(通过 invokeMethod )。这是一种实现元编程的非常重要的方式,以至于 Groovy 为这一使用 拦截器interceptors)的装饰方式提供了特殊支持。

Groovy 甚至还内建了 TracingInterceptor 。还可以像下面这样扩展内建类:

class TimingInterceptor extends TracingInterceptor {
  private beforeTime
  def beforeInvoke(object, String methodName, Object[] arguments) {
    super.beforeInvoke(object, methodName, arguments)
    beforeTime = System.currentTimeMillis()
  }
  Object afterInvoke(Object object, String methodName, Object[] arguments, Object result) {
    super.afterInvoke(object, methodName, arguments, result)
    def duration = System.currentTimeMillis() - beforeTime
    writer.write("Duration: $duration ms\\n")
    writer.flush()
    result
  }
}

有关于这个新类,使用范例如下所示:

def proxy = ProxyMetaClass.getInstance(Calc)
proxy.interceptor = new TimingInterceptor()
proxy.use {
  assert 7 == new Calc().add(1, 6)
}

输出结果如下:

before Calc.ctor()
after  Calc.ctor()
Duration: 0 ms
before Calc.add(java.lang.Integer, java.lang.Integer)
after  Calc.add(java.lang.Integer, java.lang.Integer)
Duration: 2 ms

1.6.6 利用 java.lang.reflect.Proxy 进行装饰

如果想装饰一个对象(比如只是某个类的一个特殊实例,而不是类本身),可以使用 Java 的 java.lang.reflect.Proxy 。Groovy 这样做起来要更简单一些。下面这个代码范例取自一个 grails 项目,它封装了 java.sql.Connection ,所以它的 close 方法是无参的。

protected Sql getGroovySql() {
  final Connection con = session.connection()
  def invoker = { object, method, args ->
    if (method.name == "close") {
      log.debug("ignoring call to Connection.close() for use by groovy.sql.Sql")
    } else {
      log.trace("delegating $method")
      return con.invokeMethod(method.name, args)
    }
  } as InvocationHandler;
  def proxy = Proxy.newProxyInstance( getClass().getClassLoader(), [Connection] as Class[], invoker )
  return new Sql(proxy)
}

如果有很多方法需要拦截,那么经过修改后,该方法可以按照方法名查找映射中的闭包,并加以调用。

1.6.7 利用 Spring 进行装饰

Spring 框架 允许利用拦截器来应用装饰器(你可能听过名词 advice 或 aspect)。也可以用 Groovy 来实现这种机制。

首先定义一个需要装饰的类(另外也会用到一个接口,因为这是 Spring 的惯例)。

假设该接口如下:

interface Calc {
  def add(a, b)
}

类如下:

class CalcImpl implements Calc {
  def add(a, b) { a + b }
}

下面,在一个 beans.xml 的文件中定义我们的连线:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:lang="http://www.springframework.org/schema/lang"
  xsi:schemaLocation="
    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang.xsd">

  <bean id="performanceInterceptor" autowire="no"
      class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor">
    <property name="loggerName" value="performance"/>
  </bean>
  <bean id="calc" class="util.CalcImpl"/>
  <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="beanNames" value="calc"/>
    <property name="interceptorNames" value="performanceInterceptor"/>
  </bean>
</beans>

脚本如下:

@Grab('org.springframework:spring-context:3.2.2.RELEASE')
import org.springframework.context.support.ClassPathXmlApplicationContext

def ctx = new ClassPathXmlApplicationContext('beans.xml')
def calc = ctx.getBean('calc')
println calc.add(3, 25)

运行脚本的结果如下:

21/05/2007 23:02:35 org.springframework.aop.interceptor.PerformanceMonitorInterceptor invokeUnderTrace
FINEST: StopWatch 'util.Calc.add': running time (millis) = 16

为了显示在日志级别 FINEST 的消息,我们必须调整 logging.properties 文件。

1.6.8 使用 GPars 的异步装饰者

Panini 上的代码范例有一定的启发性。避免使用 @AddedBehavior 标记。

@Grab('org.codehaus.gpars:gpars:0.10')
import static groovyx.gpars.GParsPool.withPool

interface Document {
  void print()
  String getText()
}

class DocumentImpl implements Document {
  def document
  void print() { println document }
  String getText() { document }
}

def words(String text) {
  text.replaceAll('[^a-zA-Z]', ' ').trim().split("\\\\s+")*.toLowerCase()
}

def avgWordLength = {
  def words = words(it.text)
  sprintf "Avg Word Length: %4.2f", words*.size().sum() / words.size()
}
def modeWord = {
  def wordGroups = words(it.text).groupBy {it}.collectEntries { k, v -> [k, v.size()] }
  def maxSize = wordGroups*.value.max()
  def maxWords = wordGroups.findAll { it.value == maxSize }
  "Mode Word(s): ${maxWords*.key.join(', ')} ($maxSize occurrences)"
}
def wordCount = { d -> "Word Count: " + words(d.text).size() }

def asyncDecorator(Document d, Closure c) {
  ProxyGenerator.INSTANCE.instantiateDelegate([print: {
    withPool {
      def result = c.callAsync(d)
      d.print()
      println result.get()
    }
  }], [Document], d)
}

Document d = asyncDecorator(asyncDecorator(asyncDecorator(
    new DocumentImpl(document:"This is the file with the words in it\\n\\t\\nDo you see the words?\\n"),
//    new DocumentImpl(document: new File('AsyncDecorator.groovy').text),
    wordCount), modeWord), avgWordLength)
d.print()

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文