對象去耦(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 第二版第九章 (可以從 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。
目錄