封装创建(Encapsulating creation)

Encapsulating creation
When you discover that you need to add new types to a system, the most sensible first step is to use polymorphism to create a common interface to those new types. This separates the rest of the code in your system from the knowledge of the specific types that you are adding. New types may be added without disturbing existing code … or so it seems. At first it would appear that the only place you need to change the code in such a design is the place where you inherit a new type, but this is not quite true. You must still create an object of your new type, and at the point of creation you must specify the exact constructor to use. Thus, if the code that creates objects is distributed throughout your application, you have the same problem when adding new types—you must still chase down all the points of your code where type matters. It happens to be the creation of the type that matters in this case rather than the use of the type (which is taken care of by polymorphism), but the effect is the same: adding a new type can cause problems.

The solution is to force the creation of objects to occur through a common factory rather than to allow the creational code to be spread throughout your system. If all the code in your program must go through this factory whenever it needs to create one of your objects, then all you must do when you add a new object is to modify the factory.

Since every object-oriented program creates objects, and since it’s very likely you will extend your program by adding new types, I suspect that factories may be the most universally useful kinds of design patterns.

Although only the Simple Factory Method is a true singleton, you’ll find that each specify factory class in the more general types of factories will only have a single instance.

Simple Factory method
As an example, let’s revisit the Shape system. 

One approach is to make the factory a static method of the base class:

//: factory:shapefact1:ShapeFactory1.java

// A simple static factory method.

package factory.shapefact1;

import java.util.*;

import junit.framework.*;

 

abstract class Shape {

  public abstract void draw();

  public abstract void erase();

  public static Shape factory(String type) {

    if(type.equals("Circle")) return new Circle();

    if(type.equals("Square")) return new Square();

    throw new RuntimeException(

      "Bad shape creation: " + type);

  }

}

 

class Circle extends Shape {

  Circle() {} // Package-access constructor

  public void draw() {

    System.out.println("Circle.draw");

  }

  public void erase() {

    System.out.println("Circle.erase");

  }

}

 

class Square extends Shape {

  Square() {} // Package-access constructor

  public void draw() {

    System.out.println("Square.draw");

  }

  public void erase() {

    System.out.println("Square.erase");

  }

}

 

public class ShapeFactory1 extends TestCase  {

  String shlist[] = { "Circle", "Square",

    "Square", "Circle", "Circle", "Square" };

  List shapes = new ArrayList();

  public void test() {

    Iterator it = Arrays.asList(shlist).iterator();

    while(it.hasNext())

      shapes.add(Shape.factory((String)it.next()));

    it = shapes.iterator();

    while(it.hasNext()) {

      Shape s = (Shape)it.next();

      s.draw();

      s.erase();

    }

  }

  public static void main(String args[]) {

    junit.textui.TestRunner.run(ShapeFactory1.class);

  }

} ///:~

 

 

The factory( ) takes an argument that allows it to determine what type of  Shape to create; it happens to be a String in this case but it could be any set of data. The factory( ) is now the only other code in the system that needs to be changed when a new type of Shape is added (the initialization data for the objects will presumably come from somewhere outside the system, and not be a hard-coded array as in the above example).

To encourage creation to only happen in the factory( ), the constructors for the specific types of Shape are give package access, so factory( ) has access to the constructors but they are not available outside the package.

Polymorphic factories
The static factory( ) method in the previous example forces all the creation operations to be focused in one spot, so that’s the only place you need to change the code. This is certainly a reasonable solution, as it throws a box around the process of creating objects. However, the Design Patterns book emphasizes that the reason for the Factory Method pattern is so that different types of factories can be subclassed from the basic factory (the above design is mentioned as a special case). However, the book does not provide an example, but instead just repeats the example used for the Abstract Factory (you’ll see an example of this in the next section). Here is ShapeFactory1.java modified so the factory methods are in a separate class as virtual functions. Notice also that the specific Shape classes are dynamically loaded on demand:

//: factory:shapefact2:ShapeFactory2.java

// Polymorphic factory methods.

package factory.shapefact2;

import java.util.*;

import junit.framework.*;

 

interface Shape {

  void draw();

  void erase();

}

 

abstract class ShapeFactory {

  protected abstract Shape create();

  private static Map factories = new HashMap();

  public static void

  addFactory(String id, ShapeFactory f) {

    factories.put(id, f);

  }

  // A Template Method:

  public static final

  Shape createShape(String id) {

    if(!factories.containsKey(id)) {

      try {

        // Load dynamically

        Class.forName("factory.shapefact2." + id);

      } catch(ClassNotFoundException e) {

        throw new RuntimeException(

          "Bad shape creation: " + id);

      }

      // See if it was put in:

      if(!factories.containsKey(id))

        throw new RuntimeException(

          "Bad shape creation: " + id);

    }

    return

      ((ShapeFactory)factories.get(id)).create();

  }

}

 

class Circle implements Shape {

  private Circle() {}

  public void draw() {

    System.out.println("Circle.draw");

  }

  public void erase() {

    System.out.println("Circle.erase");

  }

  private static class Factory

  extends ShapeFactory {

    protected Shape create() {

      return new Circle();

    }

  }

  static {

    ShapeFactory.addFactory(

      "Circle", new Factory());

  }

}

 

class Square implements Shape {

  private Square() {}

  public void draw() {

    System.out.println("Square.draw");

  }

  public void erase() {

    System.out.println("Square.erase");

  }

  private static class Factory

  extends ShapeFactory {

    protected Shape create() {

      return new Square();

    }

  }

  static {

    ShapeFactory.addFactory(

      "Square", new Factory());

  }

}

 

public class ShapeFactory2 extends TestCase  {

  String shlist[] = { "Circle", "Square",

    "Square", "Circle", "Circle", "Square" };

  List shapes = new ArrayList();

  public void test() {

    // This just makes sure it will complete

    // without throwing an exception.

    Iterator it = Arrays.asList(shlist).iterator();

    while(it.hasNext())

      shapes.add(

        ShapeFactory.createShape((String)it.next()));

    it = shapes.iterator();

    while(it.hasNext()) {

      Shape s = (Shape)it.next();

      s.draw();

      s.erase();

    }

  }

  public static void main(String args[]) {

    junit.textui.TestRunner.run(ShapeFactory2.class);

  }

} ///:~

 

 

Now the factory method appears in its own class, ShapeFactory, as the create( ) method. This is a protected method which means it cannot be called directly, but it can be overridden. The subclasses of Shape must each create their own subclasses of ShapeFactory and override the create( ) method to create an object of their own type. The actual creation of shapes is performed by calling ShapeFactory.createShape( ), which is a static method that uses the Map in ShapeFactory to find the appropriate factory object based on an identifier that you pass it. The factory is immediately used to create the shape object, but you could imagine a more complex problem where the appropriate factory object is returned and then used by the caller to create an object in a more sophisticated way. However, it seems that much of the time you don’t need the intricacies of the polymorphic factory method, and a single static method in the base class (as shown in ShapeFactory1.java) will work fine.

Notice that the ShapeFactory must be initialized by loading its Map with factory objects, which takes place in the static initialization clause of each of the Shape implementations. So to add a new type to this design you must inherit the type, create a factory, and add the static initialization clause to load the Map. This extra complexity again suggests the use of a static factory method if you don’t need to create individual factory objects.

Abstract factories
The Abstract Factory pattern looks like the factory objects we’ve seen previously, with not one but several factory methods. Each of the factory methods creates a different kind of object. The idea is that at the point of creation of the factory object, you decide how all the objects created by that factory will be used. The example given in Design Patterns implements portability across various graphical user interfaces (GUIs): you create a factory object appropriate to the GUI that you’re working with, and from then on when you ask it for a menu, button, slider, etc. it will automatically create the appropriate version of that item for the GUI. Thus you’re able to isolate, in one place, the effect of changing from one GUI to another.

As another example suppose you are creating a general-purpose gaming environment and you want to be able to support different types of games. Here’s how it might look using an abstract factory:

//: factory:Games.java

// An example of the Abstract Factory pattern.

package factory;

import junit.framework.*;

 

interface Obstacle {

  void action();

}

 

interface Player {

  void interactWith(Obstacle o);

}

 

class Kitty implements Player {

  public void interactWith(Obstacle ob) {

    System.out.print("Kitty has encountered a ");

    ob.action();

  }

}

 

class KungFuGuy implements Player {

  public void interactWith(Obstacle ob) {

    System.out.print("KungFuGuy now battles a ");

    ob.action();

  }

}

 

class Puzzle implements Obstacle {

  public void action() {

    System.out.println("Puzzle");

  }

}

 

class NastyWeapon implements Obstacle {

  public void action() {

    System.out.println("NastyWeapon");

  }

}

 

// The Abstract Factory:

interface GameElementFactory {

  Player makePlayer();

  Obstacle makeObstacle();

}

 

// Concrete factories:

class KittiesAndPuzzles

implements GameElementFactory {

  public Player makePlayer() {

    return new Kitty();

  }

  public Obstacle makeObstacle() {

    return new Puzzle();

  }

}

 

class KillAndDismember

implements GameElementFactory {

  public Player makePlayer() {

    return new KungFuGuy();

  }

  public Obstacle makeObstacle() {

    return new NastyWeapon();

  }

}

 

class GameEnvironment {

  private GameElementFactory gef;

  private Player p;

  private Obstacle ob;

  public GameEnvironment(

    GameElementFactory factory) {

    gef = factory;

    p = factory.makePlayer();

    ob = factory.makeObstacle();

  }

  public void play() { p.interactWith(ob); }

}

 

public class Games extends TestCase  {

  GameElementFactory

    kp = new KittiesAndPuzzles(),

    kd = new KillAndDismember();

  GameEnvironment

    g1 = new GameEnvironment(kp),

    g2 = new GameEnvironment(kd);

  // These just ensure no exceptions are thrown:

  public void test1() { g1.play(); }

  public void test2() { g2.play(); }

  public static void main(String args[]) {

    junit.textui.TestRunner.run(Games.class);

  }

} ///:~

 

In this environment, Player objects interact with Obstacle objects, but there are different types of players and obstacles depending on what kind of game you’re playing. You determine the kind of game by choosing a particular GameElementFactory, and then the GameEnvironment controls the setup and play of the game. In this example, the setup and play is very simple, but those activities (the initial conditions and the state change) can determine much of the game’s outcome. Here, GameEnvironment is not designed to be inherited, although it could very possibly make sense to do that.

This also contains examples of Double Dispatching and the Factory Method, both of which will be explained later.

Exercises
             1.             Add a class Triangle to ShapeFactory1.java

             2.             Add a class Triangle to ShapeFactory2.java

             3.             Add a new type of GameEnvironment called GnomesAndFairies to Games.java

             4.             Modify ShapeFactory2.java so that it uses an Abstract Factory to create different sets of shapes (for example, one particular type of factory object creates “thick shapes,” another creates “thin shapes,” but each factory object can create all the shapes: circles, squares, triangles etc.).

 


    当你发现需要向某个系统添加一些新类型的时候,最明智的做法就是先利用多态(polymorphism)为这些新类型创建一个公共接口。这可以使系统其余部分的代码与新加入的那些特定类型相分离。加入新的类型可能并不需要更改现有的代码。。。或者至少看上去不需要。初看起来,如果你用上面说的这种设计方法修改代码,那么唯一需要改动的就是继承新类型的地方,但事实上并非完全如此。你仍然需要创建新类型的对象,而且在创建的地方你必须指定用哪个构造函数。因此,如果用于创建对象的那部分代码分散在整个程序里(添加新类型的时候你会面临这个问题),你仍然需要找出所有与类型相关的代码。这种情况下,主要的问题是新类型(对象)的创建而不是新类型的使用(这个问题可以用多态来解决),但实际效果是一样的:添加一个新类型总会带来问题。
    解决办法是强制使用一个common factory来创建对象,而不允许创建对象的代码分散在整个系统里。如果所有需要创建对象的那些代码都借助于这个工厂(factory)来完成创建,那么添加新类型的时候你需要做的只是修改这个工厂就可以了。
    因为所有面向对象的代码都会用到创建对象,而且大多数情况下你会通过添加新的类型来扩展程序,所以我猜想factory模式(系列)可能是最广泛使用的设计模式。
    尽管只有简单工厂方法(Simple Factory Method)才是真正意义上的单件(Singleton),但你会发现每一个特定的工厂类(这些类属于更为通用的factory模式)实际上都只有一个实例。

 


简单工厂方法(Simple Factory method)

    我们用下面的例子,重温一下Shape系统。
    (实现factory模式)常用的方法是把factory声明为基类的静态方法(static method)。

//: factory:shapefact1:ShapeFactory1.java

// A simple static factory method.

package factory.shapefact1;

import java.util.*;

import junit.framework.*;

 

abstract class Shape {

 public abstract void draw();

 public abstract void erase();

 public static Shape factory(String type) {

  if(type.equals("Circle")) return new Circle();

  if(type.equals("Square")) return new Square();

  throw new RuntimeException(

   "Bad shape creation: " + type);

 }

}

 

class Circle extends Shape {

 Circle() {} // Package-access constructor

 public void draw() {

  System.out.println("Circle.draw");

 }

 public void erase() {

  System.out.println("Circle.erase");

 }

}

 

class Square extends Shape {

 Square() {} // Package-access constructor

 public void draw() {

  System.out.println("Square.draw");

 }

 public void erase() {

  System.out.println("Square.erase");

 }

}

 

public class ShapeFactory1 extends TestCase  {

 String shlist[] = { "Circle", "Square",

  "Square", "Circle", "Circle", "Square" };

 List shapes = new ArrayList();

 public void test() {

  Iterator it = Arrays.asList(shlist).iterator();

  while(it.hasNext())

   shapes.add(Shape.factory((String)it.next()));

  it = shapes.iterator();

  while(it.hasNext()) {

   Shape s = (Shape)it.next();

   s.draw();

   s.erase();

  }

 }

 public static void main(String args[]) {

  junit.textui.TestRunner.run(ShapeFactory1.class);

 }

} ///:~

 


    factory() 方法需要传入一个参数来决定要创建的Shape的具体类型; 在上面的例子里(参数)碰巧是一个字符串(String),它也可以是其它任意类型。当加入新的Shape类型的时候(我们假定被创建对象的初始化代码是来自系统以外的,而不是像上面那个例子使用一个硬编码(hard-coded)的数组),系统唯一需要改动的代码就是factory()方法。为了促使创建对象的代码只包含在factory()方法里,特定类型的Shape类的构造函数都被声明为package权限,这样一来只有factory()方法可以调用这些构造函数,而位于包(package)以外的那部分代码则没有足够的权限(调用这些构造函数)。

 

 

多态工厂(Polymorphic factories)
   上例中,静态的factory()方法使得所有创建对象的操作都集中在一个地方完成,这也就是唯一需要你修改代码的地方。这当然是一个还算不错的的解决办法,它封装了创建对象的过程。然而,《设计模式》强调使用Factory Method是为了使不同类型的工厂可以由基本类型的工厂派生(subclass)出来(上面例子是一个特例)。但是,那本书没有给出具体例子,只是重复了用于说明抽象工厂(Abstract Factory)的那个例子(你会在本书下一节看到一个Abstract Factory的例子)。下面的例子,我们修改了ShapeFactory1.java使得工厂方法成为一个单独的类的虚函数。请注意,特定类型的Shape类是根据需要动态加载的。

//: factory:shapefact2:ShapeFactory2.java

// Polymorphic factory methods.

package factory.shapefact2;

import java.util.*;

import junit.framework.*;

 

interface Shape {

 void draw();

 void erase();

}

 

abstract class ShapeFactory {

 protected abstract Shape create();

 private static Map factories = new HashMap();

 public static void

  addFactory(String id, ShapeFactory f) {

   factories.put(id, f);

  }

  // A Template Method:

  public static final

   Shape createShape(String id) {

    if(!factories.containsKey(id)) {

     try {

      // Load dynamically

      Class.forName("factory.shapefact2." + id);

     } catch(ClassNotFoundException e) {

      throw new RuntimeException(

       "Bad shape creation: " + id);

     }

     // See if it was put in:

     if(!factories.containsKey(id))

      throw new RuntimeException(

      "Bad shape creation: " + id);

    }

    return

     ((ShapeFactory)factories.get(id)).create();

   }

}

 

class Circle implements Shape {

 private Circle() {}

 public void draw() {

  System.out.println("Circle.draw");

 }

 public void erase() {

  System.out.println("Circle.erase");

 }

 private static class Factory

  extends ShapeFactory {

   protected Shape create() {

    return new Circle();

   }

  }

  static {

   ShapeFactory.addFactory(

    "Circle", new Factory());

  }

}

 

class Square implements Shape {

 private Square() {}

 public void draw() {

  System.out.println("Square.draw");

 }

 public void erase() {

  System.out.println("Square.erase");

 }

 private static class Factory

  extends ShapeFactory {

   protected Shape create() {

    return new Square();

   }

  }

  static {

   ShapeFactory.addFactory(

    "Square", new Factory());

  }

}

 

public class ShapeFactory2 extends TestCase  {

 String shlist[] = { "Circle", "Square",

  "Square", "Circle", "Circle", "Square" };

 List shapes = new ArrayList();

 public void test() {

  // This just makes sure it will complete

  // without throwing an exception.

  Iterator it = Arrays.asList(shlist).iterator();

  while(it.hasNext())

   shapes.add(

   ShapeFactory.createShape((String)it.next()));

  it = shapes.iterator();

  while(it.hasNext()) {

   Shape s = (Shape)it.next();

   s.draw();

   s.erase();

  }

 }

 public static void main(String args[]) {

  junit.textui.TestRunner.run(ShapeFactory2.class);

 }

} ///:~

 


    现在工厂方法(factory method)出现在它自己的类ShapeFactory里,名字改成了create()方法。它是一个受保护的(protected)方法,也就是说它不能被直接调用,但可以被重载。Shape类的子类必须创建与之对应的ShapeFactory的子类,并且通过重载create()函数来创建它自己的实例。实际上一系列Shape对象的创建是通过调用ShapeFactory. createShape( ) 来完成的。 CreateShape()是个静态方法, 它根据传入的标示,通过查找ShapeFactory   的 Map 成员变量找到与之相应的工厂对象 (factory obejct)。 然后,找到的factory对象即被用来创建shape对象,但你可以想象一下更为棘手的问题:(与某种Shape类型相对应的)工厂对象被调用者用来以更为复杂的方式创建对象。但是,大多数情况下你似乎并不需要用到复杂的多态工厂方法(polymorphic factory method),在基类里加一个静态方法(像ShapeFactory1.java 那样)就足以解决问题了。
    注意到,ShapeFactory的初始化必须通过加载Map数据成员才能完成(Map的元素是factory对象),而这些初始化代码又位于Shape实现类的静态初始化语句里。这样一来,每加入一个新的类型你就必须得继承原来的类型(指Shape?),创建一个factory,然后添加静态初始化语句用以加载Map对象。这些额外的复杂性又一次暗示我们:如果不需要创建单独的factory对象,那最好还是使用静态工厂方法。

 

 

抽象工厂(Abstract factories)
    抽象工厂(abstract factory)模式看起来很像前面我们看到的那些factory对象,只不过它有多个而不是一个factory方法。每一个factory 方法创建一个不同类型的对象。基本思想是:在创建工厂对象的地方,由你来决定如何使用该工厂对象创建的那些对象。《设计模式》里给出的例子实现了在不同用户图形界面(GUIs)之间的可移植性:你根据自己使用的GUI来创建一个与之对应的factory对象,在这以后,当你需要用到菜单,按钮,滚动条这些东西的时候,它会根据你使用的GUI自动创建合适的对象。这样,你就可以把实现不同GUI之间切换的代码分离出来,使它集中在一个地方。

    作为另外一个例子,假设你要创建一个通用的游戏环境,而且你还想要支持不同类型的游戏。下面的例子用抽象工厂给出了它的一种可能的实现。

//: factory:Games.java

// An example of the Abstract Factory pattern.

package factory;

import junit.framework.*;

 

interface Obstacle {

 void action();

}

 

interface Player {

 void interactWith(Obstacle o);

}

 

class Kitty implements Player {

 public void interactWith(Obstacle ob) {

  System.out.print("Kitty has encountered a ");

  ob.action();

 }

}

 

class KungFuGuy implements Player {

 public void interactWith(Obstacle ob) {

  System.out.print("KungFuGuy now battles a ");

  ob.action();

 }

}

 

class Puzzle implements Obstacle {

 public void action() {

  System.out.println("Puzzle");

 }

}

 

class NastyWeapon implements Obstacle {

 public void action() {

  System.out.println("NastyWeapon");

 }

}

 

// The Abstract Factory:

interface GameElementFactory {

 Player makePlayer();

 Obstacle makeObstacle();

}

 

// Concrete factories:

class KittiesAndPuzzles

 implements GameElementFactory {

  public Player makePlayer() {

   return new Kitty();

  }

  public Obstacle makeObstacle() {

   return new Puzzle();

  }

 }

 

 class KillAndDismember

  implements GameElementFactory {

   public Player makePlayer() {

    return new KungFuGuy();

   }

   public Obstacle makeObstacle() {

    return new NastyWeapon();

   }

  }

 

  class GameEnvironment {

   private GameElementFactory gef;

   private Player p;

   private Obstacle ob;

   public GameEnvironment(

    GameElementFactory factory) {

     gef = factory;

     p = factory.makePlayer();

     ob = factory.makeObstacle();

    }

    public void play() { p.interactWith(ob); }

  }

 

  public class Games extends TestCase  {

   GameElementFactory

    kp = new KittiesAndPuzzles(),

    kd = new KillAndDismember();

   GameEnvironment

    g1 = new GameEnvironment(kp),

    g2 = new GameEnvironment(kd);

   // These just ensure no exceptions are thrown:

   public void test1() { g1.play(); }

   public void test2() { g2.play(); }

   public static void main(String args[]) {

    junit.textui.TestRunner.run(Games.class);

   }

  } ///:~

 


    在上面的游戏环境里,Player对象与Obstale对象交互,根据你所选择的游戏类型,player和obstacle的各自类型也会不同。你通过选择某个特定的GameElementFactory来决定游戏的类型,然后GameElementFactory会控制初始化和游戏的进行。在上面的例子里,初始化和游戏的进行都非常简单,但那些活动(初始条件和状态变化)可以在很大程度上决定游戏的结局。这里,GameEnvironment不是用来给其它类继承的 ,尽管那么做很可能也说的通。
    上面的代码也包含了双向分发(Double Dispatching)和工厂方法(Factory Method)两种模式,我们会在后面讲解它们。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值