[轉貼]翻譯TIPatterns--對象去耦(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。

發佈了24 篇原創文章 · 獲贊 2 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章