5: Factories: encapsulating object 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.
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:
//: c05:shapefact1:ShapeFactory1.java
// A simple static factory method.
package c05.shapefact1;
import java.util.*;
import com.bruceeckel.test.*;
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() {} // Friendly constructor
public void draw() {
System.out.println("Circle.draw");
}
public void erase() {
System.out.println("Circle.erase");
}
}
class Square extends Shape {
Square() {} // Friendly constructor
public void draw() {
System.out.println("Square.draw");
}
public void erase() {
System.out.println("Square.erase");
}
}
public class ShapeFactory1 extends UnitTest {
String shlist[] = { "Circle", "Square",
"Square", "Circle", "Circle", "Square" };
List shapes = new ArrayList();
public void test() {
for(int i = 0; i < shlist.length; i++)
shapes.add(Shape.factory(shlist[i]));
Iterator i = shapes.iterator();
while(i.hasNext()) {
Shape s = (Shape)i.next();
s.draw();
s.erase();
}
}
public static void main(String args[]) {
new ShapeFactory1().test();
}
} ///:~
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 made “friendly,” 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:
//: c05:shapefact2:ShapeFactory2.java
// Polymorphic factory methods.
package c05.shapefact2;
import java.util.*;
import com.bruceeckel.test.*;
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("c05.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 UnitTest {
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.
for(int i = 0; i < shlist.length; i++)
shapes.add(
ShapeFactory.createShape(shlist[i]));
Iterator i = shapes.iterator();
while(i.hasNext()) {
Shape s = (Shape)i.next();
s.draw();
s.erase();
}
}
public static void main(String args[]) {
new ShapeFactory2().test();
}
} ///:~
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:
//: c05:Games.java
// An example of the Abstract Factory pattern.
import com.bruceeckel.test.*;
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 UnitTest {
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[]) {
Games g = new Games();
g.test1();
g.test2();
}
} ///:~
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
- Add a class Triangle to
ShapeFactory1.java
- Add
a class Triangle to
ShapeFactory2.java
- Add
a new type of GameEnvironment called GnomesAndFairies to
GameEnvironment.java
- 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.).