Introduction
Decorator pattern is one of the essential software development patterns. It works nicely with Dependency Injection and Composition Root patterns. This small project is a showcase of its usage.
Background
I'm no game developer, I write enterprise applications. It's expected that I deliver code that is extensible and reusable. I struggled for a while but the situation has vastly improved since I write code with Dependency Injection and Decorator patterns in mind.
There are many introductory articles on Dependency Injection, Composition Root, and Decorator patterns so if you are unfamiliar with the concepts, I would recommend reading those first. I'm really a "show me the code" person so I will dive straight to the code and explain the concepts as I go.
Using the Code
Why a Pacman game clone? I wrote a CodeProject article about unit testing for a Tic Tac Toe game, and I read somewhere that Pacman should be the second novice game development project.
The project goals I had set were:
- keep the code as simple as possible
- implement an extendable game base using Dependency Injection and code to abstractions
- compose everything in one place (composition root)
- decorate game in composition root
To keep the game as simple as possible, no third party tools were used. All game base logic is placed in a class library project MazeLib
, and the GUI implemented in a Windows Forms project Maze.
When Dependency Injection concepts are in use, dependencies can be decorated before they are sent to the class that needs them. With that concept, the base game GUI form can be implemented and functionality can be added without modifying the form later on.
The base game looks like this:
By extending the player object via Decorator, it can change the appearance based on the direction it is moving.
Same goes for ghosts.
All options to add game functionality are accessible through options at the top of the main game form:
Remaining options enable pickups to make ghosts vulnerable, temporarily freeze them, or eating them can add to player score. So can eating a ghost.
So how can this be achieved? In a more traditional approach, the main form could contain all the different code variations based on user options preference. And there is class decorating.
The high level concept of Decorator pattern works like this:
The Decorator pattern has four parts. The first part, the component defines the abstract
class with all the basic methods and properties, without any implementation. There are four classes in the game that can be decorated - AbstractBorder
, AbstractGhost
, AbstractPickup
, AbstractPlayer
. AbstractShape
is the base class for other classes. It is a generic class, and it's most important property is Display
. It returns a generic object that is used to display the shape on the form. For a Windows Forms GUI project, I used PictureBox
as a generic parameter to display the shapes on the screen. The class hierarchy looks like this:
The second pattern part is the concrete component. That is a non abstract
class with basic implementation only. It must be inherited from the component class. So the concrete component classes are:
The Decorator pattern part is where things start to become interesting. As the pattern says, decorator classes are classes that are inherited from the component
class. They also get an instance of the component
class through the constructor that they save as a protected
variable. Last, but not the least, they are abstract
. First, the class hierarchy:
And part of the code for the GhostDecorator
class:
public abstract class GhostDecorator<T> : AbstractGhost<T>
{
protected readonly AbstractGhost<T> _ghostToDecorate;
public GhostDecorator(AbstractGhost<T> ghostToDecorate)
{
if (ghostToDecorate == null)
{
throw new ArgumentNullException("ghostToDecorate");
}
_ghostToDecorate = ghostToDecorate;
}
public override void ResetToStartLocation()
{
_ghostToDecorate.ResetToStartLocation();
}
public override Point Location
{
get
{
return _ghostToDecorate.Location;
}
}
Important parts of Decorator
class to notice are:
- It is an
abstract
class - It is inherited from the
Component
class - It receives a
Component
class instance through the constructor and saves it in a protected
member variable - All override methods and properties call the methods of
protected
member class instance that is received as constructor parameter
The Decorator class' sole purpose is to call all methods of the class it receives through the constructor. When we come to concrete decorators, it will make sense.
One more important class in the reusable MazeLib
library is AbstractLevel
. It receives a player object, a collection of borders, pickups, and ghost objects, and it handles collision detection between objects. It also fires an LevelCompleted
event when the game is finished.
public abstract class AbstractLevel<T>
{
protected int _stepSize;
protected AbstractPlayer<T> _player;
protected List<AbstractBorder<T>> _borders;
protected List<AbstractPickup<T>> _pickups;
protected List<AbstractGhost<T>> _ghosts;
public event EventHandler<AbstractLevel<T>> LevelCompleted;
public AbstractLevel(int stepSize, AbstractPlayer<T> player,
List<AbstractBorder<T>> borders,
List<AbstractPickup<T>> pickups,
List<AbstractGhost<T>> ghosts)
{
}
That's all the logic needed for the game, so it is separated from the GUI in the reusable MazeLib
library.
Another project in the solution is the GUI project, Maze
. It calls out the library methods for the base game functions and decorates the classes as needed. The high level game logic is, the first part occurs in the library, the rest is GUI project responsibility.
In code, this is achieved by composing the class structure at one place - composition root. In the Windows Forms project, it is the Main()
method of the Program
class. Here, we create a class dependency graph for the game. The main form needs a level in order to operate - a level is the dependency of the main form:
static void Main()
{
FormMain mainForm = new FormMain(level);
mainForm.GameRestarted += (sender, e) =>
{
level = ComposeLevel(e.Value);
mainForm.Level = level;
};
Application.Run(mainForm);
}
public static AbstractLevel<PictureBox> ComposeLevel(ConfigurationOptions configurationOptions)
{
AbstractPlayer<PictureBox> player = null;
List<AbstractBorder<PictureBox>> borders = null;
List<AbstractPickup<PictureBox>> pickups = null;
List<AbstractGhost<PictureBox>> ghosts = null;
var levelDataCreator = new LevelDataCreator<PictureBox>(_stepSize);
levelDataCreator.LevelOneData(out player, out borders, out pickups, out ghosts);
player = new PlayerPictureBoxDecorator(player);
if (configurationOptions.PlayerFaces))
{
player = new PlayerPictureBoxFacesDecorator(player);
}
for (int i = 0; i < ghosts.Count; i++)
{
AbstractGhost<PictureBox> ghost = new GhostPictureBoxDecorator(ghosts[i]);
if (configurationOptions.GhostFaces))
{
ghost = new GhostPictureBoxFacesDecorator(ghost);
}
if (configurationOptions.GhostTrackScore))
{
ghost = new GhostTrackScoreDecorator(ghost);
}
ghosts[i] = ghost;
}
for (int i = 0; i < pickups.Count; i++)
{
AbstractPickup<PictureBox> pickup = new PickupPictureBoxDecorator(pickups[i]);
if (configurationOptions.PickupGhostVurneability))
{
pickup = new PickupGhostVulnerabilityDecorator(pickup, ghosts);
}
if (configurationOptions.PickupGhostTempFreeze))
{
pickup = new PickupGhostsTempFreezeDecorator(pickup, ghosts);
}
if (configurationOptions.PickupTrackScore))
{
pickup = new PickupTrackScoreDecorator(pickup);
}
pickups[i] = pickup;
}
return new LevelPictureBox(_stepSize, player, borders, pickups, ghosts);
}
}
I will break down the code into high level concept parts:
AbstractPlayer<PictureBox> player = null;
List<AbstractBorder<PictureBox>> borders = null;
List<AbstractPickup<PictureBox>> pickups = null;
List<AbstractGhost<PictureBox>> ghosts = null;
var levelDataCreator = new LevelDataCreator<PictureBox>(_stepSize);
levelDataCreator.LevelOneData(out player, out borders, out pickups, out ghosts);
Level borders, ghosts, and player objects are created first. Notice that all objects are abstract
classes. Since objects are abstract
, they can be decorated before sending them as dependencies to the level object.
The important things to notice here are the class types that are in the collections. Collections are originally defined as collections of abstract
classes. Every collection can contain objects that are inherited. So after the call:
levelDataCreator.LevelOneData(out player, out borders, out pickups, out ghosts);
We get the following results:
AbstractPlayer<PictureBox> player
is an instance of the Ghost<PictureBox>
class List<AbstractBorder<PictureBox> borders
is the list of Border<Ghost>
objects List<AbstractPickup<PictureBox>> pickups
is the list of Pickup<PictureBox>
objects List<AbstractGhost<PictureBox>> ghosts
is the list of Ghost<PictureBox>
objects
Although variables are defined as abstract
classes, they must contain instances of concrete objects. Declaring variables are abstraction enabled class decoration. In Decorator pattern words, we declare a variable to be a component type (abstraction) and we assign the concrete component instance to it.
If we just delete all the code in the ComposeLevel
method after creating dependencies for the level object and return it:
public static AbstractLevel<PictureBox> ComposeLevel(ConfigurationOptions configurationOptions)
{
AbstractPlayer<PictureBox> player = null;
List<AbstractBorder<PictureBox>> borders = null;
List<AbstractPickup<PictureBox>> pickups = null;
List<AbstractGhost<PictureBox>> ghosts = null;
var levelDataCreator = new LevelDataCreator<PictureBox>(_stepSize);
levelDataCreator.LevelOneData(out player, out borders, out pickups, out ghosts);
return new LevelPictureBox(_stepSize, player, borders, pickups, ghosts);
}
The code would compile, but we would get an empty form. What happened? All game logic is there and code compiles, but the form needs the information for how to paint the PictureBox
object that is used as the Display
property in the shape class. There are several ways to remedy the situation, one is directly setting the picture box properties to the object before sending them to the level class. If we just add PictureBox
properties before sending classes to level object:
public static AbstractLevel<PictureBox> ComposeLevel(ConfigurationOptions configurationOptions)
{
AbstractPlayer<PictureBox> player = null;
List<AbstractBorder<PictureBox>> borders = null;
List<AbstractPickup<PictureBox>> pickups = null;
List<AbstractGhost<PictureBox>> ghosts = null;
var levelDataCreator = new LevelDataCreator<PictureBox>(_stepSize);
levelDataCreator.LevelOneData(out player, out borders, out pickups, out ghosts);
var playerPictureBox = new PictureBox();
playerPictureBox.BackColor = Color.Blue;
playerPictureBox.Location = player.Location;
playerPictureBox.Size = player.OccupiedSpace.Size;
player.Display = playerPictureBox;
foreach (var border in borders)
{
var borderPictureBox = new PictureBox();
borderPictureBox.BackColor = Color.Green;
borderPictureBox.Location = border.Location;
borderPictureBox.Size = border.OccupiedSpace.Size;
border.Display = borderPictureBox;
}
foreach (var pickup in pickups)
{
var pickupPictureBox = new PictureBox();
pickupPictureBox.BackColor = Color.CornflowerBlue;
pickupPictureBox.Location = pickup.Location;
pickupPictureBox.Size = pickup.OccupiedSpace.Size;
pickup.Display = pickupPictureBox;
}
foreach (var ghost in ghosts)
{
var ghostPictureBox = new PictureBox();
ghostPictureBox.BackColor = Color.Red;
ghostPictureBox.Location = ghost.Location;
ghostPictureBox.Size = ghost.OccupiedSpace.Size;
ghost.Display = ghostPictureBox;
}
return new LevelPictureBox(_stepSize, player, borders, pickups, ghosts);
}
This would display the form and objects, as intended:
But when we start the game, neither ghosts nor the player appear to be moving. Yet they do in code, but we need to override the base class methods to include the PictureBox
moving on the screen when ghosts and the player move. So we can go two ways: inherit from the ghost
class and add all the functionality there, or add Decorator
classes.
player = new PlayerPictureBoxDecorator(player);
if (configurationOptions.PlayerFaces))
{
player = new PlayerPictureBoxFacesDecorator(player);
}
for (int i = 0; i < ghosts.Count; i++)
{
AbstractGhost<PictureBox> ghost = new GhostPictureBoxDecorator(ghosts[i]);
if (configurationOptions.GhostFaces))
{
ghost = new GhostPictureBoxFacesDecorator(ghost);
}
if (configurationOptions.GhostTrackScore))
{
ghost = new GhostTrackScoreDecorator(ghost);
}
ghosts[i] = ghost;
}
for (int i = 0; i < pickups.Count; i++)
{
AbstractPickup<PictureBox> pickup = new PickupPictureBoxDecorator(pickups[i]);
if (configurationOptions.PickupGhostVurneability))
{
pickup = new PickupGhostVulnerabilityDecorator(pickup, ghosts);
}
if (configurationOptions.PickupGhostTempFreeze))
{
pickup = new PickupGhostsTempFreezeDecorator(pickup, ghosts);
}
if (configurationOptions.PickupTrackScore))
{
pickup = new PickupTrackScoreDecorator(pickup);
}
pickups[i] = pickup;
}
This is where all the decorating occurs. Before the player object or the collection of ghosts, borders, and pickups are sent to the level instance, they are decorated. As we saw previously, for PictureBox
implementation, we at least need to extend the base game to include the logic for the PictureBox
moving. So we create the first concrete decorator component, GhostPictureBoxDecorator
.
internal class GhostPictureBoxDecorator : GhostDecorator<PictureBox>
{
private PictureBox _display;
public GhostPictureBoxDecorator(AbstractGhost<PictureBox> ghostToDecorate)
: base(ghostToDecorate)
{
ghostToDecorate.GhostMoved += (sender, e) =>
{
Display.Location = e.Location;
};
}
public override PictureBox Display
{
get
{
if (_display == null)
{
_display = new PictureBox()
{
Location = Location,
Size = OccupiedSpace.Size,
BackColor = Color.Red
};
}
return _display;
}
set
{
base.Display = value;
}
}
public override void MoveLeft()
{
_ghostToDecorate.MoveLeft();
OnGhostMoved();
}
}
To make PictureBox
move on the screen is a two part process - setting up PictureBox
instance in Display
property:
internal class GhostPictureBoxDecorator : GhostDecorator<PictureBox>
{
public override PictureBox Display
{
get
{
if (_display == null)
{
_display = new PictureBox()
{
Location = Location,
Size = OccupiedSpace.Size,
BackColor = Color.Red
};
}
}
}
and updating PictureBox.Location
property when ghost is moving by subscribing to GhostMoved
event.
public GhostPictureBoxDecorator(AbstractGhost<PictureBox> ghostToDecorate)
: base(ghostToDecorate)
{
ghostToDecorate.GhostMoved += (sender, e) =>
{
Display.Location = e.Location;
};
}
First concrete decorators classes for borders, player and pickups have the same responsibility -
set up properties for picture boxes so they can be displayed on the form.
The most basic composition for fully functional game with PictureBox
could be set in the following way:
public static AbstractLevel<PictureBox> ComposeLevel()
{
AbstractPlayer<PictureBox> player = null;
List<AbstractBorder<PictureBox>> borders = null;
List<AbstractPickup<PictureBox>> pickups = null;
List<AbstractGhost<PictureBox>> ghosts = null;
var levelDataCreator = new LevelDataCreator<PictureBox>(_stepSize);
levelDataCreator.LevelOneData(out player, out borders, out pickups, out ghosts);
player = new PlayerPictureBoxDecorator(player);
for (int i = 0; i < borders.Count; i++)
{
borders[i] = new BorderPictureBoxDecorator(borders[i]);
}
for (int i = 0; i < ghosts.Count; i++)
{
ghosts[i] = new GhostPictureBoxDecorator(ghosts[i]);
}
for (int i = 0; i < pickups.Count; i++)
{
pickups[i] = new PickupPictureBoxDecorator(pickups[i]);
}
return new LevelPictureBox(_stepSize, player, borders, pickups, ghosts);
}
The game would function - all logic implemented in library would work, players could eat pickups but not ghosts, score is not tracked and pickups have no extra properties. But this is where the decorator concept begins to shine - adding functionality to existing, fully functional system. Let's add a simple one, GhostPictureBoxFaces
decorator, which will change PictureBox.Image
of ghosts objects depending on their movement direction.
public class GhostPictureBoxFacesDecorator : GhostDecorator<PictureBox>
{
public GhostPictureBoxFacesDecorator(AbstractGhost<PictureBox> ghostToDecorate)
: base(ghostToDecorate)
{
}
public override DIRECTION MoveAutomatically(IEnumerable<DIRECTION> availableDirections)
{
DIRECTION result = _ghostToDecorate.MoveAutomatically(availableDirections);
if (result == DIRECTION.LEFT)
{
if (_ghostToDecorate.Display.BackColor == Color.Red)
{
_ghostToDecorate.Display.Image = Properties.Resources.IMG_GHOST_MOVE_LEFT;
}
OnGhostMoved();
}
else if (result == DIRECTION.UP)
{
if (_ghostToDecorate.Display.BackColor == Color.Red)
{
_ghostToDecorate.Display.Image = Properties.Resources.IMG_GHOST_MOVE_UP;
}
OnGhostMoved();
}
else if (result == DIRECTION.RIGHT)
{
if (_ghostToDecorate.Display.BackColor == Color.Red)
{
_ghostToDecorate.Display.Image = Properties.Resources.IMG_GHOST_MOVE_RIGHT;
}
OnGhostMoved();
}
else if (result == DIRECTION.DOWN)
{
if (_ghostToDecorate.Display.BackColor == Color.Red)
{
_ghostToDecorate.Display.Image = Properties.Resources.IMG_GHOST_MOVE_DOWN;
}
OnGhostMoved();
}
return result;
}
}
MoveAutomatically
is a method that generates a random ghost movement. After the base implementation call, we get the direction where the ghost had moved and we can update the Image accordingly.
To add this functionality to game, we need to wrap GhostPictureBoxDecorator
instance with GhostPictureBoxFaces
decorator in the composition root:
public static AbstractLevel<PictureBox> ComposeLevel()
{
AbstractPlayer<PictureBox> player = null;
List<AbstractBorder<PictureBox>> borders = null;
List<AbstractPickup<PictureBox>> pickups = null;
List<AbstractGhost<PictureBox>> ghosts = null;
var levelDataCreator = new LevelDataCreator<PictureBox>(_stepSize);
levelDataCreator.LevelOneData(out player, out borders, out pickups, out ghosts);
player = new PlayerPictureBoxDecorator(player);
for (int i = 0; i < borders.Count; i++)
{
borders[i] = new BorderPictureBoxDecorator(borders[i]);
}
for (int i = 0; i < ghosts.Count; i++)
{
ghosts[i] = new GhostPictureBoxFacesDecorator(new GhostPictureBoxDecorator(ghosts[i]));
}
for (int i = 0; i < pickups.Count; i++)
{
pickups[i] = new PickupPictureBoxDecorator(pickups[i]);
}
return new LevelPictureBox(_stepSize, player, borders, pickups, ghosts);
}
This is the most important part of the article to understand - the class hierarchy of decorated ghosts objects so far.
We will go step by step:
levelDataCreator.LevelOneData(out player, out borders, out pickups, out ghosts);
After this line, ghosts
objects are instances of Ghost
object from the class library and they contain all base functionality. After the call:
ghosts[i] = new GhostPictureBoxFacesDecorator(new GhostPictureBoxDecorator(ghosts[i]));
each ghost object first becomes GhostPictureBoxDecorator
instance, decorator
class that implemented Display
property and handled movement animations. In other words, Ghost
object instance is decorated with GhostPictureBoxDecorator
. Then, that object is further decorated with GhostPictureBoxFacesDecorator
.
ghosts[i] = new GhostPictureBoxFacesDecorator(new GhostPictureBoxDecorator(ghosts[i]));
The decorator added more functionality to an existing object, in this case, it changed display image based on ghost movement direction. All concrete decorators besides the necessary basic PictureBox
decorators are available through game menu. When an option "Different movement faces" is checked:
the player object will be decorated in this line:
if (configurationOptions.PlayerFaces))
{
player = new PlayerPictureBoxFacesDecorator(player);
}
After the AbstractPlayer
object instance player
is decorated, the decorated instance is sent as the dependency to the level object. The level object just uses the player
object, whether it is decorated or not.
The question I get a lot is - why don't you use simple object inheritance, why complicate things for future maintenance developers with your fancy decorators? And it's a fair question, all OO developers understand object inheritance so it is sometimes smarter to avoid decorators altogether. After all, decorator is just a variation of object inheritance. But take a look at the options menu. Why don't I just extend the ghost
class and implement Display
property and movement in it? No PictureBoxDecorator
class is necessary, right? Yes, that's correct. And after that, I can just implement score tracking in the same class? Of course. In that scenario, object graph would be much simpler:
But how about more complex options system? With no decorator pattern, I need a separate class for every option combination. This game still implements small number of options, but the problem becomes evident as we add options to the system. Since player
object has only one optional functionality added, using decorators seems like an overkill.
But things tie in decorator vs simple inheritance discussion when we have two options added on base functionality.
Notice here that we need a separate class to include ghost movement faces and score tracking functionality. I guess another approach could be class that inherits from GhostFaces
and implements score tracking and similar class that is inherited from GhostScore
and it implements faces functionality. That way, instead of one, we would get two additional classes. In any case, code would be duplicated.
The problem really becomes evident with 3 options, like for the pickup
class:
We have 3 options to cover, therefore 7 combinations in total:
Ghost
vulnerability only Ghost
temp freezing only Score
tracking only Ghost
vulnerability and temp freezing Ghost
vulnerability and score
tracking Ghost
temp freezing and score
tracking Ghost
vulnerability, ghost
temp freezing and score
tracking (all)
Bottom line is - the more options we add to the existing system, the more powerful decorator concept becomes. By using decorator, I have made a composition root with simple class instancing logic. Instead of comparing single options to decorate class with in composition root, I would have logic that compares option combinations and creates instance of concrete classes instead:
if (PickupGhostVulneability && !PickupGhostTempFreeze && !PickupTrackScore)
{
}
else if (!PickupGhostVulneability && PickupGhostTempFreeze && !PickupTrackScore)
{
}
else if (!PickupGhostVulneability && !PickupGhostTempFreeze && PickupTrackScore)
{
}
else if (PickupGhostVulneability && PickupGhostTempFreeze && !PickupTrackScore)
{
}
else if (PickupGhostVulneability && !PickupGhostTempFreeze && PickupTrackScore)
{
}
else if (!PickupGhostVulneability && PickupGhostTempFreeze && PickupTrackScore)
{
}
else if (PickupGhostVurneability && PickupGhostTempFreeze && PickupTrackScore)
{
}
Instead, with decorators, the same can be achieved in a simpler way.
if (PickupGhostVulneability)
{
pickup = new PickupGhostVulnerabilityDecorator(pickup, ghosts);
}
if (PickupGhostTempFreeze)
{
pickup = new PickupGhostsTempFreezeDecorator(pickup, ghosts);
}
if (PickupTrackScore)
{
pickup = new PickupTrackScoreDecorator(pickup);
}
Last but not least, system stays extendable for future expansion.
Back to the project code, after classes level dependencies are created, they are passed to level object via level constructor.
return new LevelPictureBox(_stepSize, player, borders, pickups, ghosts);
Then main form is displayed:
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var level = ComposeLevel();
FormMain mainForm = new FormMain(level);
mainForm.GameRestarted += (sender, e) =>
{
level = ComposeLevel(e.Value);
mainForm.Level = level;
};
Application.Run(mainForm);
}
And finally, when the game is restarted, game options are sent as an eventargs
so classes can be composed and decorated again:
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var level = ComposeLevel();
FormMain mainForm = new FormMain(level);
mainForm.GameRestarted += (sender, e) =>
{
level = ComposeLevel(e.Value);
mainForm.Level = level;
};
Application.Run(mainForm);
}
Points of Interest
Class decorating is a powerful concept, basically it is a variation of class inheritance, with one important difference - it keeps single responsibility per class, can reduce code size, and drastically improve the overall code quality.
Please rate and comment if I can make any article improvements. Thank you!
History
- 11-30-2013: Initial version
- 12-08-2013: More code examples, discussion about simple class inheritance vs class decorating