1.6 装饰器模式
装饰器模式(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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论