I l@ve RuBoard Previous Section Next Section

9.1 The Architectural Role of Abstract Factory

Let's say you are in the enviable position of designing a "find 'em and kill 'em" game, like Doom or Quake.

You want to entice regular taxpayers to enjoy your game, so you provide an Easy level. On the Easy level, the enemy soldiers are rather dull, the monsters move like molasses, and the super-monsters are quite friendly.

You also want to entice hardcore gamers to play your game, so you provide a Diehard level. On this level, enemy soldiers fire three times a second and are karate pros, monsters are cunning and deadly, and really bad super-monsters appear once in a while.

A possible modeling of this scary world would include defining a base class Enemy and deriving the refined interfaces Soldier, Monster, and SuperMonster from it. Then, you derive SillySoldier, SillyMonster, and SillySuperMonster from these interfaces for the Easy level. Finally, you implement BadSoldier, BadMonster, and BadSuperMonster for the Diehard level. The resulting inheritance hierarchy is shown in Figure 9.1.

Figure 9.1. A hierarchy for a game with two levels of difficulty

graphics/09fig01.gif

It is worth noting that in your game, an instantiation of BadSoldier and an instantiation of SillyMonster never "live" at the same time. It wouldn't make sense; the player plays either the easy game with SillySoldiers, SillyMonsters, and SillySuperMonsters, or the tough game in the company of BadSoldiers, BadMonsters, and Bad SuperMonsters.

The two categories of types form two families; during the game, you always use objects in one of the two families, but you never combine them.

It would be nice if you could enforce this consistency. Otherwise, if you're not careful enough throughout the application, the beginner happily punching SillySoldiers could suddenly meet a BadMonster around the corner, get whacked, and exercise that money-back guarantee.

Because it's better to be careful once than a hundred times, you gather the creation functions for all the game objects into a single interface, as follows:



class AbstractEnemyFactory


{


public:


   virtual Soldier* MakeSoldier() = 0;


   virtual Monster* MakeMonster() = 0;


   virtual SuperMonster* MakeSuperMonster() = 0;


};


Then, for each play level, you implement a concrete enemy factory that creates enemies as prescribed by the game strategy.



class EasyLevelEnemyFactory : public AbstractEnemyFactory


{


public:


   Soldier* MakeSoldier()


   { return new SillySoldier; }


   Monster* MakeMonster()


   { return new SillyMonster; }


   SuperMonster* MakeSuperMonster()


   { return new SillySuperMonster; }


};





class DieHardLevelEnemyFactory : public AbstractEnemyFactory


{


public:


   Soldier* MakeSoldier()


   { return new BadSoldier; }


   Monster* MakeMonster()


   { return new BadMonster; }


   SuperMonster* MakeSuperMonster()


   { return new BadSuperMonster; }


};


Finally, you initialize a pointer to AbstractEnemyFactory with the appropriate concrete class:



class GameApp


{


   ...


   void SelectLevel()


   {


      if (user chooses the Easy level)


      {


         pFactory_ = new EasyLevelEnemyFactory;


      }


      else


      {


         pFactory_ = new DieHardLevelEnemyFactory;


      }


   }


private:


   // Use pFactory_ to create enemies


   AbstractEnemyFactory* pFactory_;


};


The advantage of this design is that it keeps all the details of creating and properly matching enemies inside the two implementations of AbstractEnemyFactory. Because the application uses pFactory_ as the only object creator, consistency is enforced by design. This is a typical usage of the Abstract Factory design pattern.

The Abstract Factory design pattern prescribes collecting creation functions for families of objects in a unique interface. Then you must provide an implementation of that interface for each family of objects you want to create.

The product types advertised by the abstract factory interface (Soldier, Monster, and SuperMonster) are called abstract products. The product types that the implementation actually creates (SillySoldier, BadSoldier, SillyMonster, and so on) are called concrete products. These terms should be familiar to you from Chapter 8.

The main disadvantage of Abstract Factory is that it is type intensive: The abstract factory base class (AbstractEnemyFactory in the example) must know about every abstract product that's to be created. In addition, at least in the implementation just provided, each concrete factory class depends on the concrete products it creates.

You can reduce dependencies by applying the techniques described in Chapter 8. There, you created a concrete object not by knowing its type but by knowing its type identifier (such as an int or a string). Such a dependency is much weaker.

However, the more you reduce dependencies, the more you also reduce type knowledge, and consequently the more you undermine the type safety of your design. This is yet another instance of the classic dilemma of better type safety versus lesser dependencies that often appears in C++.

As often happens, getting the right solution involves a trade-off between competing benefits. You should choose the setting that best suits your needs. As a rule of thumb, try to go with a static model when you can, and rely on a dynamic model when you must.

The generic implementation of Abstract Factory presented in the following sections sports an interesting feature that reduces static dependencies without compromising type safety.

    I l@ve RuBoard Previous Section Next Section