对象去耦(Object decoupling)
代理(Proxy)模式和状态(State)模式分别提供了供你使用的代理类(surrogate class);正真干活的那个类被代理类隐藏了。当你调用代理类的一个方法的时候,代理类只是简单的调用实现类(implementing class)所对应的方法。这两种模式非常相似,实际上,代理(Proxy)模式只是状态(State)模式的一个特例。
有人试图将这两种模式合在一起统称为Surrogate模式,但是“代理(proxy)”这个术语已经用了很长时间了,而且它有自己特殊的含义,它的这些含义基本上体现了这两种模式的差别所在。
这两种模式的基本概念非常简单:代理类 (surrogate) 和 实现类都由同一个基类派生出来:
当创建一个代理对象 (surrogate object) 时,同时会创建一个实现(对象),代理对象会把所有的方法调用传递给实现对象。
从结构上看,代理(Proxy)模式和状态(State)模式之间的差别非常简单:一个代理(Proxy)只对应一个实现(implementation),而一个状态(State)却可以对应多个实现。《设计模式》一书认为,这两种两种模式的应用场合是截然不同的:代理(Proxy)模式用于控制对实现(类)的访问,而状态(State)模式可以动态地改变实现(类)。但是,如果把“控制对实现类的访问”这个概念扩展开来的话,这两种模式就可以优雅的结合在一起了。
代理:替另外一个对象打点一切(Proxy: fronting for another object)
我们按照上面的图示实现代理(Proxy)模式,下面是实现代码:
//: proxy:ProxyDemo.java
// Simple demonstration of the Proxy pattern.
package proxy;
import junit.framework.*;
interface ProxyBase {
void f();
void g();
void h();
}
class Proxy implements ProxyBase {
private ProxyBase implementation;
public Proxy() {
implementation = new Implementation();
}
// Pass method calls to the implementation:
public void f() { implementation.f(); }
public void g() { implementation.g(); }
public void h() { implementation.h(); }
}
class Implementation implements ProxyBase {
public void f() {
System.out.println("Implementation.f()");
}
public void g() {
System.out.println("Implementation.g()");
}
public void h() {
System.out.println("Implementation.h()");
}
}
public class ProxyDemo extends TestCase {
Proxy p = new Proxy();
public void test() {
// This just makes sure it will complete
// without throwing an exception.
p.f();
p.g();
p.h();
}
public static void main(String args[]) {
junit.textui.TestRunner.run(ProxyDemo.class);
}
} ///:~
当然,并不是说实现类和代理类必须实现完全相同的接口;既然代理类只是在一定程度上代表那个需要它提交(referring)方法的类,这就已经满足了proxy模式的基本要求(注意这里的陈述和GoF一书所给出的定义是有差别的)。尽管如此,定义一个公共的接口还是很方便的,这样就可以强制实现类(Implementation)实现(fulfill)代理类(Proxy)需要调用的所有方法。
用Proxy模式实现PoolManager
//: proxy:PoolManager.java
package proxy;
import java.util.*;
public class PoolManager {
private static class PoolItem {
boolean inUse = false;
Object item;
PoolItem(Object item) { this.item = item; }
}
public class ReleasableReference { // Used to build the proxy
private PoolItem reference;
private boolean released = false;
public ReleasableReference(PoolItem reference) {
this.reference = reference;
}
public Object getReference() {
if(released)
throw new RuntimeException(
"Tried to use reference after it was released");
return reference.item;
}
public void release() {
released = true;
reference.inUse = false;
}
}
private ArrayList items = new ArrayList();
public void add(Object item) {
items.add(new PoolItem(item));
}
// Different (better?) approach to running out of items:
public static class EmptyPoolItem {}
public ReleasableReference get() {
for(int i = 0; i < items.size(); i++) {
PoolItem pitem = (PoolItem)items.get(i);
if(pitem.inUse == false) {
pitem.inUse = true;
return new ReleasableReference(pitem);
}
}
// Fail as soon as you try to cast it:
// return new EmptyPoolItem();
return null; // temporary
}
} ///:~
//: proxy:ConnectionPoolProxyDemo.java
package proxy;
import junit.framework.*;
interface Connection {
Object get();
void set(Object x);
void release();
}
class ConnectionImplementation implements Connection {
public Object get() { return null; }
public void set(Object s) {}
public void release() {} // Never called directly
}
class ConnectionPool { // A singleton
private static PoolManager pool = new PoolManager();
private ConnectionPool() {} // Prevent synthesized constructor
public static void addConnections(int number) {
for(int i = 0; i < number; i++)
pool.add(new ConnectionImplementation());
}
public static Connection getConnection() {
PoolManager.ReleasableReference rr =
(PoolManager.ReleasableReference)pool.get();
if(rr == null) return null;
return new ConnectionProxy(rr);
}
// The proxy as a nested class:
private static
class ConnectionProxy implements Connection {
private PoolManager.ReleasableReference implementation;
public
ConnectionProxy(PoolManager.ReleasableReference rr) {
implementation = rr;
}
public Object get() {
return
((Connection)implementation.getReference()).get();
}
public void set(Object x) {
((Connection)implementation.getReference()).set(x);
}
public void release() { implementation.release(); }
}
}
public class ConnectionPoolProxyDemo extends TestCase {
static {
ConnectionPool.addConnections(5);
}
public void test() {
Connection c = ConnectionPool.getConnection();
c.set(new Object());
c.get();
c.release();
}
public void testDisable() {
Connection c = ConnectionPool.getConnection();
String s = null;
c.set(new Object());
c.get();
c.release();
try {
c.get();
} catch(Exception e) {
s = e.getMessage();
System.out.println(s);
}
assertEquals(s,
"Tried to use reference after it was released");
}
public static void main(String args[]) {
junit.textui.TestRunner.run(
ConnectionPoolProxyDemo.class);
}
} ///:~
动态代理(Dynamic Proxies)
JDK1.3引入了动态代理 (Dynamic Proxy). 尽管一开始有些复杂,但它确实是一个吸引人的工具。下面这个有趣的小例子证明了这一点, 当invocation handler被调用的时候,代理机制(proxying)开始工作。这是非常Cool的一个例子,它就在我的脑海里,但是我必须得想出一些合理的东西给invocation handler,这样才能举出一个有用的例子…(作者还没有写完)
// proxy:DynamicProxyDemo.java
// Broken in JDK 1.4.1_01
package proxy;
import java.lang.reflect.*;
interface Foo {
void f(String s);
void g(int i);
String h(int i, String s);
}
public class DynamicProxyDemo {
public static void main(String[] clargs) {
Foo prox = (Foo)Proxy.newProxyInstance(
Foo.class.getClassLoader(),
new Class[]{ Foo.class },
new InvocationHandler() {
public Object invoke(
Object proxy, Method method,
Object[] args) {
System.out.println(
"InvocationHandler called:" +
"\n\tMethod = " + method);
if (args != null) {
System.out.println("\targs = ");
for (int i = 0; i < args.length; i++)
System.out.println("\t\t" + args[i]);
}
return null;
}
});
prox.f("hello");
prox.g(47);
prox.h(47, "hello");
}
} ///:~
练习:用java的动态代理创建一个对象作为某个简单配置文件的前端。例如,在good_stuff.txt文件里有如下条目:
a=1
b=2
c="Hello World"
客户端程序员可以使用(你写的)NeatPropertyBundle类:
NeatPropertyBundle p =
new NeatPropertyBundle("good_stuff");
System.out.println(p.a);
System.out.println(p.b);
System.out.println(p.c);
配置文件可以包含任何内用,任意的变量名。动态代理要么返回对应属性的值要么告诉你它不存在(可能通过返回null)。如果你摇设置一个原本不存在的属性值,动态代理会创建一个新的条目。ToString()
方法应该显示当前的所有条目。
练习:和上一道练习类似,用Java的动态代理连接一个DOS的Autoexec.bat文件。
练习:接受一个可以返回数据的SQL查询语句,然后读取数据库的元数据(metadata)。为每一条记录(record)提供一个对象,这个对象拥有一下属性:列名(column names)和对应的数据类型(data types).
练习:用XML-RPC写一个简单的服务器和客户端.每一个客户端返回的对象都必须使用动态代理的概念(dynamic proxy concept)来实现(exercise)远端的方法。(瞎翻的,不知道啥意思)
读者Andrea写道:
除了最后一个练习,我觉得你给出的上面几个练习都不咋的。我更愿意把Invocation handler看成是能和被代理对象正交的 (orthogonal) 东东。
换句话说,invocation handler的实现应该是和动态创建的代理对象所提供的那些接口完全无关的。也就是说,一旦invocation handler写好之后,你就可以把它用于任何暴露接口的类,甚至是那些晚于invocation handler出现的类和接口。
这就是我为什么要说invocation handler所提供的服务是和被代理对象正交的(orthognal)。Rickard 在他的SmartWorld例子里给出了几个handler,其中我最喜欢的是那个调用-重试(call-retry)handler。它首先调用那个(被代理的)实际对象,如果调用产生异常或者等待超时,就重试三次。如果这三次都失败了,那就返回一个异常。这个Handler可以被用于任何一个类。
那个handler的实现相对于你这里讲的来说过于复杂了,我用这个例子仅仅是想说明我所指的正交(orthogonal)服务到底是什么意思。
您所给出的那几个练习,在我看来,唯一适合用动态代理实现的就是最后那个用XML-RPC与对象通信的那个练习。因为你所使用的用以分发消息的机制(指XML-RPC)是和你想要建立通信的那个对象完全正交的。
状态模式:改变对象的行为(State: changing object behavior)
一个用来改变类的(状态的)对象。
迹象:几乎所有方法里都出现(相同的)条件(表达式)代码。
为了使同一个方法调用可以产生不同的行为,状态(State)模式在代理(surrogate)的生命周期内切换它所对应的实现(implementation)。当你发现,在决定如何实现任何一个方法之前都必须作很多测试的情况下,这是一种优化实现代码的方法。例如,童话故事青蛙王子就包含一个对象(一个生物),这个对象的行为取决于它自己所处的状态。你可以用一个布尔(boolean)值来表示它的状态,测试程序如下:
//: state:KissingPrincess.java
package state;
import junit.framework.*;
class Creature {
private boolean isFrog = true;
public void greet() {
if(isFrog)
System.out.println("Ribbet!");
else
System.out.println("Darling!");
}
public void kiss() { isFrog = false; }
}
public class KissingPrincess extends TestCase {
Creature creature = new Creature();
public void test() {
creature.greet();
creature.kiss();
creature.greet();
}
public static void main(String args[]) {
junit.textui.TestRunner.run(KissingPrincess.class);
}
} ///:~
但是,greet() 方法(以及其它所有在完成操作之前必须测试isFrog值的那些方法)最终要产生一大堆难以处理的代码。如果把这些操作都委托给一个可以改变的状态对象(State object),那代码会简单很多。
//: state:KissingPrincess2.java
package state;
import junit.framework.*;
class Creature {
private interface State {
String response();
}
private class Frog implements State {
public String response() { return "Ribbet!"; }
}
private class Prince implements State {
public String response() { return "Darling!"; }
}
private State state = new Frog();
public void greet() {
System.out.println(state.response());
}
public void kiss() { state = new Prince(); }
}
public class KissingPrincess2 extends TestCase {
Creature creature = new Creature();
public void test() {
creature.greet();
creature.kiss();
creature.greet();
}
public static void main(String args[]) {
junit.textui.TestRunner.run(KissingPrincess2.class);
}
} ///:~
此外,状态(State)的改变会自动传递到所有用到它的地方,而不需要手工编辑类的方法以使改变生效。
下面的代码演示了状态(State)模式的基本结构。
//: state:StateDemo.java
// Simple demonstration of the State pattern.
package state;
import junit.framework.*;
interface State {
void operation1();
void operation2();
void operation3();
}
class ServiceProvider {
private State state;
public ServiceProvider(State state) {
this.state = state;
}
public void changeState(State newState) {
state = newState;
}
// Pass method calls to the implementation:
public void service1() {
// ...
state.operation1();
// ...
state.operation3();
}
public void service2() {
// ...
state.operation1();
// ...
state.operation2();
}
public void service3() {
// ...
state.operation3();
// ...
state.operation2();
}
}
class Implementation1 implements State {
public void operation1() {
System.out.println("Implementation1.operation1()");
}
public void operation2() {
System.out.println("Implementation1.operation2()");
}
public void operation3() {
System.out.println("Implementation1.operation3()");
}
}
class Implementation2 implements State {
public void operation1() {
System.out.println("Implementation2.operation1()");
}
public void operation2() {
System.out.println("Implementation2.operation2()");
}
public void operation3() {
System.out.println("Implementation2.operation3()");
}
}
public class StateDemo extends TestCase {
static void run(ServiceProvider sp) {
sp.service1();
sp.service2();
sp.service3();
}
ServiceProvider sp =
new ServiceProvider(new Implementation1());
public void test() {
run(sp);
sp.changeState(new Implementation2());
run(sp);
}
public static void main(String args[]) {
junit.textui.TestRunner.run(StateDemo.class);
}
} ///:~
在main()函数里,先用到的是第一个实现,然后转入第二个实现。
当你自己实现State模式的时候就会碰到很多细节的问题,你必须根据自己的需要选择合适的实现方法,比如用到的状态(State)是否要暴露给调用的客户,以及如何使状态发生变化。有些情况下(比如Swing的LayoutManager),,客户端可以直接传对象进来,但是在KissingPrincess2.java那个例子里,状态对于客户端来说是不可见的。此外,用于改变状态的机制可能很简单也可能很复杂-比如本书后面将要提到的状态机(State Machine),那里会讲到一系列的状态以及改变状态的不同机制。
上面提到Swing的LayoutManager那个例子非常有趣,它同时体现了Strategy模式和State模式的行为。
Proxy模式和State模式的区别在于它们所解决的问题不同。《设计模式》里是这么描述Proxy模式的一般应用的:
1. 远程代理(Remote Proxy)为一个对象在不同的地址空间提供局部代理。A remote proxy is created for you automatically by the RMI compiler rmic as it creates stubs and
2. 虚代理(Virtual proxy),根据需要,在创建复杂对象时使用“lazy initialization” .
3. 保护代理(protection proxy) 用于你不希望客户端程序员完全控制被代理对象(proxied object)的情况下。
4. 智能引用(smart reference). 当访问被代理对象时提供额外的动作。例如,它可以用来对特定对象的引用进行计数,从而实现copy-on-write,进而避免对象别名(object aliasing). 更简单的一个例子是用来记录一个特定方法被调用的次数。
你可以把java里的引用(reference)看作是一种保护代理,它控制对分配在堆(heap)上的实际对象的访问(而且可以保证你不会用到一个空引用(null reference))。
【重写:在《设计模式》一书里,Proxy模式和State模式被认为是互不相干的,因为那本书给出的用以实现这两种模式的结构是完全不同的(我认为这种实现有点武断)。尤其是State模式,它用了一个分离的实现层次结构,但我觉着完全没有必要,除非你认定实现代码不是由你来控制的(当然这也是一种可能的情况,但是如果代码是由你来控制的,那还是用一个单独的基类更简洁实用)。此外,Proxy模式的实现不需要用一个公共的基类,因为代理对象只是控制对被代理对象的访问。尽管有细节上的差异,Proxy模式和State模式都是用一个代理(surrogate)把方法调用传递给实现对象。】
State模式到处可见,因为它是最基本的一个想法,比如,在Builder模式里,“Director”就是用一个后端(backend)的Buider object来产生不同的行为。
迭代器:分离算法和容器(Iterators: decoupling algorithms
from containers)
Alexander Stepanov(和Dave Musser一起)写STL以前 ,已经用了好几年思考泛型编程(generic programming)的问题。最后他得出结论:所有的算法都是定义在代数结构(algebraic structures)之上的-我们把代数结构称作容器(container)。
在这个过程中,他意识到i迭代器对于算法的应用是至关重要的,因为迭代器将算法从它所使用的特定类型的容器中分离出来。这就意味着在描述算法的时候,可以不必考虑它所操作的特定序列。更为一般情况,用迭代器写的任何代码都与它所操作的数据结构相分离,这样一来这些代码就更为通用并且易于重用。
迭代器的另外一个应用领域就是functional programming,它的目标是描述程序的每一步是干什么的,而不是描述程序的每一步是怎么做的。也就是说,使用“sort”(来排序),而不是具体描述排序的算法实现。C++STL的目的就是为C++语言提供对这种泛型编程方法的支持(这种方法成功与否还需要时间来验证)。
如果你用过Java的容器类(写代码不用到它们是很难的),那你肯定用过迭代器-Java1.0/1.1把它叫作枚举器(Enumeration),Java2.0叫作迭代器-你肯定已经熟悉它们的一般用法。如果你还不熟悉的话,可以参考Thinking in Java 第二版第九章 (freely downloadable from www.BruceEckel.com).
因为Java2的容器非常依赖于迭代器,所以它们就成了泛型编程/功能性编程的最佳候选技术。这一章节通过把STL移植到Java来讲解这些技术,(移植的迭代器)会和Java2的容器类一起使用。
类型安全的迭代器(Type-safe iterators)
在Thinking in Java 第二版里,我实现了一个类型安全的容器类,它只接受某一特定类型的对象。读者Linda Pazzaglia想要我实现另外一个类型安全的组件,一个可以和java.util里定义的容器类兼容的迭代器,但要限制它所遍历的对象必须都是同一类型的。
如果Java有模板(template)机制,上面这种(类型安全的)迭代器很容易就可以返回某一特定类型的对象。但是没有模板机制,就必须得返回generic Objects,或者为每一种需要遍历的对象都手工添加代码。这里我会使用前一种方法。
另外一个需要在设计时决定的问题(design decision)是什么时候判定对象的类型。一种方法是以迭代器遍历的第一个对象的类型(作为迭代器的类型),但是这种方法当容器类根据它自己的内部算法(比如hash表)重新为对象排序时就会有问题,这样同一迭代器的两次遍历就可能得到不同的结果。安全的做法是在构造迭代器的时候让用户指定迭代器的类型。
最后的问题是如何构建迭代器。我们不可能重写现有的Java类库,它已经包含了枚举器和迭代器。但是,我们可以用Decorator模式简单的创建一个枚举器或者迭代器的外覆类,产生一个具有我们想要的迭代行为(本例中,指在类型不正确的时候抛出RuntimeException异常)的新对象,而这个新对象跟原来的枚举器或者迭代器有相同的接口,这样一来,它就可以用在相同的场合(或许你会争论说这实际上是Proxy模式,但是从它的目的(intent)来说它更像Decorator模式)。
实现代码如下:
//: com:bruceeckel:util:TypedIterator.java
package com.bruceeckel.util;
import java.util.*;
public class TypedIterator implements Iterator {
private Iterator imp;
private Class type;
public TypedIterator(Iterator it, Class type) {
imp = it;
this.type = type;
}
public boolean hasNext() {
return imp.hasNext();
}
public void remove() { imp.remove(); }
public Object next() {
Object obj = imp.next();
if(!type.isInstance(obj))
throw new ClassCastException(
"TypedIterator for type " + type +
" encountered type: " + obj.getClass());
return obj;
}
} ///:~
练习:
1.写一个“virtual proxy”。
2.写一个“Smartreference”代理,用这个代理记录某个特定对象的方法调用次数。
3.仿照某个DBMS系统,写一个程序限制最大连接数。用类似于singleton的方法控制连接对象的数量。当用户释放某个连接时,必须通知系统将释放的连接收回以便重用。为了保证这一点,写一个proxy对象代替对连接的引用计数,进一步设计这个proxy使它能够将连接释放回系统。
4.用State模式,写一个UnpredictablePerson类,它根据自己的情绪(Mood)改变对hello()方法的响应。再写一个额外的Mood类:Prozac。
5.写一个简单的copy-on write实现。
6.java.util.Map 没有提供直接从一个两维数组读入“key-value”对的方法。写一个adapter类实现这个功能。
7.Create an Adapter Factory that dynamically finds and produces the adapter that you need to connect a given object to a desired interface.
8.用java标准库的动态代理重做练习7。
9.改写本节的Object Pool,使得对象再一段时间以后自动回收到对象池。
10.改写练习9,用“租借(leasing)”的方法使得客户端可以刷新“租借对象”,从而阻止对象定时自动释放。
11.考虑线程因素,重写Object Pool。