Professional Documents
Culture Documents
The emergence of parallel inheritance hierarchies can be a problem with the Factory Method pattern.
This is a kind of coupling that makes some programmers uncomfortable. Every time you add a product
family, you are forced to create an associated concrete creator (the BloggsCal encoders are matched
by
BloggsCommsManager, for example). In a system that grows fast enough to encompass many
products,
maintaining this kind of relationship can quickly become tiresome.
One way of avoiding this dependency is to use PHP’s clone keyword to duplicate existing concrete
products. The concrete product classes themselves then become the basis of their own generation.
This is
the Prototype pattern. It enables you to replace inheritance with composition. This in turn promotes
runtime
flexibility and reduces the number of classes you must create.
The Problem
Imagine a Civilization-style web game in which units operate on a grid of tiles. Each tile can represent
sea,
plains, or forests. The terrain type constrains the movement and combat abilities of units occupying
the tile.
You might have a TerrainFactory object that serves up Sea, Forest, and Plains objects. You decide that
you will allow the user to choose among radically different environments, so the Sea object is an
abstract
superclass implemented by MarsSea and EarthSea. Forest and Plains objects are similarly
implemented.
The forces here lend themselves to the Abstract Factory pattern. You have distinct product hierarchies
(Sea,
Plains, Forests), with strong family relationships cutting across inheritance (Earth, Mars). Figure 9-10
presents a class diagram that shows how you might deploy the Abstract Factory and Factory Method
patterns
to work with these products.
Figure 9-10. Handling terrains with the Abstract Factory method
Chapter 9 ■ Generating Objects
200
As you can see, I rely on inheritance to group the terrain family for the products that a factory will
generate. This is a workable solution, but it requires a large inheritance hierarchy, and it is relatively
inflexible. When you do not want parallel inheritance hierarchies, and when you need to maximize
runtime
flexibility, the Prototype pattern can be used in a powerful variation on the Abstract Factory pattern.
Implementation
When you work with the Abstract Factory/Factory Method patterns, you must decide, at some point,
which
concrete creator you wish to use, probably by checking some kind of preference flag. As you must do
this
anyway, why not simply create a factory class that stores concrete products, and then populate this
during
initialization? You can cut down on a couple of classes this way and, as you shall see, take advantage
of other
benefits. Here’s some simple code that uses the Prototype pattern in a factory:
// listing 09.30
class Sea
{
}
class EarthSea extends Sea
{
}
class MarsSea extends Sea
{
}
class Plains
{
}
class EarthPlains extends Plains
{
}
class MarsPlains extends Plains
{
}
class Forest
{
}
class EarthForest extends Forest
{
}
class MarsForest extends Forest
{
}
Chapter 9 ■ Generating Objects
201
// listing 09.31
class TerrainFactory
{
private $sea;
private $forest;
private $plains;
public function __construct(Sea $sea, Plains $plains, Forest $forest)
{
$this->sea = $sea;
$this->plains = $plains;
$this->forest = $forest;
}
public function getSea(): Sea
{
return clone $this->sea;
}
public function getPlains(): Plains
{
return clone $this->plains;
}
public function getForest(): Forest
{
return clone $this->forest;
}
}
// listing 09.32
$factory = new TerrainFactory(
new EarthSea(),
new EarthPlains(),
new EarthForest()
);
print_r($factory->getSea());
print_r($factory->getPlains());
print_r($factory->getForest());
popp\ch09\batch11\EarthSea Object
(
)
popp\ch09\batch11\EarthPlains Object
(
)
popp\ch09\batch11\EarthForest Object
(
)
Chapter 9 ■ Generating Objects
202
As you can see, I load up a concrete TerrainFactory with instances of product objects. When a client
calls getSea(), I return a clone of the Sea object that I cached during initialization. This structure buys
me additional flexibility. Want to play a game on a new planet with Earth-like seas and forests, but
Marslike
plains? No need to write a new creator class—you can simply change the mix of classes you add to
TerrainFactory:
$factory = new TerrainFactory(
new EarthSea(),
new MarsPlains(),
new EarthForest()
);
So the Prototype pattern allows you to take advantage of the flexibility afforded by composition. We
get
more than that, though. Because you are storing and cloning objects at runtime, you reproduce object
state
when you generate new products. Imagine that Sea objects have a $navigability property. The property
influences the amount of movement energy a sea tile saps from a vessel and can be set to adjust the
difficulty
level of a game:
// listing 09.33
class Sea
{
private $navigability = 0;
public function __construct(int $navigability)
{
$this->navigability = $navigability;
}
}
Now when I initialize the TerrainFactory object, I can add a Sea object with a navigability modifier.
This will then hold true for all Sea objects served by TerrainFactory:
// listing 09.34
$factory = new TerrainFactory(
new EarthSea(-1),
new EarthPlains(),
new EarthForest()
);
This flexibility is also apparent when the object you wish to generate is composed of other objects.
■■Note I covered object cloning in Chapter 4. The clone keyword generates a shallow copy
of any object to
which it is applied. This means that the product object will have the same properties as the
source. If any of the
source’s properties are objects, then these will not be copied into the product. Instead, the
product will reference
the same object properties. It is up to you to change this default and to customize object
copying in any other way,
by implementing a __clone() method. This is called automatically when the clone keyword is
used.
Chapter 9 ■ Generating Objects
203
Perhaps all Sea objects can contain Resource objects (FishResource, OilResource, etc.). According to
a preference flag, we might give all Sea objects a FishResource by default. Remember that if your
products
reference other objects, you should implement a __clone() method to ensure that you make a deep
copy:
class Contained
{
}
class Container
{
public $contained;
function __construct()
{
$this->contained = new Contained();
}
function __clone()
{
// Ensure that cloned object holds a
// clone of self::$contained and not
// a reference to it
$this->contained = clone $this->contained;
}
}