Design Patterns: Abstract Factory

The Abstract Factory pattern is defined as "A pattern which provides an interface for creating families of related or dependant objects without specifying their concrete classes." This is a pattern I'm currently making use of in one of the games I'm developing, Galactica 2180, which is based on the old Battlestar Galactica TV show from the 1970's. Part of the game involves creating spaceships for each of the different factions in the game. It is in this regard that I'm making use of the Abstract Factory pattern.

The game makes use of several engine objects to handle game dynamics: combat, diplomacy, production, etc. The production engine is responsible for queueing object requests by the different factions, such as spaceships, ground vehicles, and so on. But because each faction has their own types of these objects, a generic approach was needed. In this example, we will focus strictly on spaceship creation.

For starters, I created an interface to be implemented by all factory classes which create spaceships -- specifically, objects of type SpaceCraft or anything that derives from it (FighterSquadron, Tanker, BaseShip, etc). The interface is as follows:

interface IShipFactory
{
Spacecraft CreateShip( string ShipType );
}

Simple enough, right? The interface defines a single method, CreateShip(), which takes a single parameter, a string named ShipType. This tells the method what type of ship to make.

In turn, each faction has its own factory class which implements this interface. For example, the Cylon Empire faction has a class which looks something like this:

using System;
using System.Collections.Generic;
using System.Text;
using mylifeandcode.Games;

namespace Galactica2180
{
class CylonEmpireShipFactory : IShipFactory
{
#region IShipFactory Members

public Spacecraft CreateShip(string ShipType)
{

switch (ShipType.Trim().ToUpper())
{
case "TANKER":
Spacecraft ship = new Spacecraft();
//Set object properties
...
return ship;

case "BASESTAR":
BaseShip baseship = new BaseShip();
&baseship.MaxSquadrons = 4;
//Set other object properites
...
return baseship;

//More ship types...

default:
throw new Exception("An unknown ship type, \"" + ShipType +
"\", was encountered while trying to create a new ship.");
}

}

#endregion
}
}

As you can see, it implements the IShipFactory interface, and it's method, CreateShip(). Inside the implementation of CreateShip() is a switch() block which creates the correct ship type and sets the ship properties accordingly depending on the value passed to the method (also, it's worth noting that the game will use enums to specify ship types, and that these enum values will be converted to strings to be passed to this method to eliminate the chance of a typo causing an exception).

The production engine has its own CreateShip() method, which looks like this:

public Spacecraft CreateShip(IShipFactory pFactory, string psShipType)
{
return pFactory.CreateShip(psShipType);
}

It takes two parameters: an IShipFactory instance, and a string specifying the type of ship to create. Even though each faction has its own ship factory class, because they all implement IShipFactory, they can all be passed to the production engine's CreateShip() method. The production engine's CreateShip() method just calls the CreateShip() method of the IShipFactory instance passed to it, supplying it with the psShipType parameter, and returns whatever this method returns. Here's some sample code that tests all this out:

ColonialShipFactory colonialfactory = null;
CylonEmpireShipFactory cylonempirefactory = null;
List<Spacecraft> lstShips = null;
ProductionEngine engine = null;

try
{
colonialfactory = new ColonialShipFactory();
cylonempirefactory = new CylonEmpireShipFactory();
lstShips = new List<Spacecraft>();
engine = new ProductionEngine();

lstShips.Add(engine.CreateShip(colonialfactory, "TANKER"));
lstShips.Add(engine.CreateShip(colonialfactory, "BATTLESTAR"));
lstShips.Add(engine.CreateShip(cylonempirefactory, "TANKER"));
lstShips.Add(engine.CreateShip(cylonempirefactory, "BASESTAR"));

foreach (Spacecraft ship in lstShips)
{
if (ship is BaseShip)
MessageBox.Show("Baseship!");
else
MessageBox.Show("Not BaseShip!");
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "ERROR", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
colonialfactory = null;
cylonempirefactory = null;
lstShips = null;
engine = null;
}

In the example above, two ship factory classes are created, one of type ColonialShipFactory, the other of type CylonEmpireShipFactory. A List of type SpaceCraft is created which will hold all of the ships created by either factory. Finally, a ProductionEngine object is created. The Production Engine's CreateShip() method is called four times, two times each with of the class factory classes as parameters, and the return value is added to the list of SpaceCrafts. We then loop through the list and display a MessageBox telling us whether or not each ship is a BaseShip object -- this demonstrates the use of Generics in .NET and shows that because the list was typed as type SpaceCraft, all types that derive from it, such as BaseShip, are correctly stored as such in the list.

The flexibility of the Abstract Factory pattern is evident. The actual creation of the SpaceCraft objects is done in the concrete class factory objects (CylonEmpireShipFactory, ColonialShipFactory, etc), so the production engine doesn't need to know anything about it -- it just returns what that concrete classes return based on the IShipFactory interface. This approach allows for greater flexibility -- for example, part of the design of the game is that new factions can be added simply by adding a new DLL to the game's directory which contains objects which implement the required interfaces, including IShipFactory. This way, new factions can be created and "plugged in" to the game.

Comments

Popular Posts

Resolving the "n timer(s) still in the queue" Error In Angular Unit Tests

How to Get Norton Security Suite Firewall to Allow Remote Desktop Connections in Windows

How to Determine if a Column Exists in a DataReader

Silent Renew and the "login_required" Error When Using oidc-client

Fixing the "Please add a @Pipe/@Directive/@Component annotation" Error In An Angular App After Upgrading to webpack 4