[转贴]翻译TIPatterns--对象去耦(Object decoupling)

来源:互联网 发布:逻辑思维 知乎 编辑:程序博客网 时间:2024/04/29 02:46

    代理(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。