天天看點

翻譯TIPatterns--對象去耦(Object decoupling)

對象去耦(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。

目錄