9 Software design patterns
9.1 Introduction
In this workshop, we will look at design patterns, and their application in refactoring. A software design pattern describes a general, reusable solution to a commonly occurring problem in a specific design context. They’re often useful when designing new pieces of software as they both allow us to reuse best practice from prior experience, and provide a means to discuss the design with others (a shared vocabulary). However, design patterns can be equally useful when refactoring existing codebases.
The workshop builds on techniques given in previous workshops for working with large codebases, in particular extending those in Workshop 8 for refactoring existing code. In this workshop you will:
- Be introduced to 6 of the 23 Gang of Four (GoF) Design Patterns (Gamma et al. 1994)
- Refactor a small existing code base to apply Behavioural, Structural and Creational patterns
We’ll be assuming that, after this workshop, you are capable of carrying out the following tasks for yourself, without needing much guidance:
- Identify portions of existing codebases that could be improved through the application of Design Patterns
- Describe a refactoring using a design pattern vocabulary and, where appropriate, supporting UML
- Apply design patterns to an existing codebase
As in prior workshops, there will be scope to work through the tasks at your own pace – in particular, each of the three workshop exercises is divided into multiple stages that address first one design pattern, and then a second. You should (at a minimum) aim to have completed all tasks related to the first design pattern of each exercise.
9.2 Workshop Exercise 1 - Behavioural Patterns
This first section of the workshop focuses on applying the two Behavioural patterns introduced: Strategy, and State.
For this exercise, you’ll be working with a small-scale Java codebase that’s loosely inspired by some classes in the Stendhal codebase. For this first exercise you’ll be focussing on a set of classes that represent pets. The main classes and their members can be represented in a UML class diagram shown in figure 9.1
In this workshop you’ll be extending and then refactoring the codebase to explore how behavioural patterns can simplify the process of adding new functionality, and can remove the need for duplicate code.
9.2.1 Exercise 1a - The Strategy Pattern
In this part of the exercise we’ll be focusing on the Strategy pattern. You will modify the code in five stages:
- Add a new
Pet
classCuddlyToy
that requires new algorithms for the growth, feeding, hunger, and crying. - Consider how one might use sub-class/super-class relationships to avoid duplicate code.
- Implement an abstract
GrowthStrategy
that provides method signatures for growth-related algorithms. - Implement the three concrete implementations of
GrowthStrategy
encountered so far. - Modify the existing
Pet
classes to use the newly created strategy classes.
9.2.1.1 Stage 1 - Add a new Pet class
You’ve been asked to add a new Pet, CuddlyToy
, for players that (for example) have allergies or just don’t want the effort of looking after a real-life creature. The requirements for CuddlyToy
are as follows:
- A
CuddlyToy
should not grow, they areADULT_SIZE
at instantiation. - A
CuddlyToy
does not eat, and should not get hungry. - A
CuddlyToy
squeaks, its cry is generated by a plastic squeaker
[ACTION]
Implement a new Pet
subclass that complies with the above requirements, and modify PetDriver.java
to demonstrate your new Pet subtype.
9.2.1.2 Stage 2 - Design sub-class/super-class relationships to avoid duplicated code
It’s clear that many of our Pets have quite different algorithms for growth. Some, like Goats
and Cats
grow steadily, increasing by a fixed amount over a constant time interval. Others, like MagicDragons
, increase by a fixed amount but at irregular intervals – their growth stagnates for a while and then they undergo a growth spurt. Some Pets
, like CuddlyToys
, don’t grow at all.
If we wanted to introduce more Pet
types, we could quickly end up having to duplicate the code for steady, irregular or no growth across multiple Pet
subclases. Alternatively, we could add layers of subclassing shown in figure 9.2
However, this could quickly become difficult to manage, and doesn’t always avoid duplicate. For example, suppose you’re now asked to add a new Bird
subtype. Birds can fly (like Dragons
) but grow steadily (like Cats
and Goats
). The resulting class structure might look something like that shown in figure 9.3
So, we’ve now potentially duplicated our steady growth code in two superclasses SteadilyGrowingGroundPet
and SteadilyGrowingFlyingPet
, and some new code for flying behaviours in two superclasses SteadilyGrowingFlyingPet
and RandomlyGrowingFlyingPet
. This definitely isn’t great design.
9.2.1.3 Stage 3 - Introduce a GrowthStrategy
A Strategy pattern defines a encapsulates a family of interchangeable algorithms – here, our interchangeable algorithms describe different patterns of growth.
The first step in refactoring to a Strategy will be to create an abstract class GrowthStrategy
.
[ACTION]
Create a new GrowthStrategy
class, with abstract method signatures for canGrow()
and Grow()
.
9.2.1.4 Stage 4 - Implement concrete growth strategies
You now need to create concrete implementations of your GrowthStrategy
class, each representing a different growth algorithm. So far, we’ve encountered three growth algorithms:
- Steady growth – Grows by a fixed amount every time the grow method is called.
- Random growth – Grows by a fixed amount some random subset of times that the grow method is called.
- No growth – Does not grow, even when the grow method is called.
[ACTION]
Create three subclass implementations of GrowthStrategy
, one for each of the growth algorithms encountered so far.
9.2.1.5 Stage 5 - Modify the codebase to use our GrowthStrategy
Now we have a selection of implemented GrowthStrategy
classes, we need to modify the Pet
subclass to utilise these new classes. To do this, we’ll add an attribute growthStrategy
of type GrowthStrategy
to the Pet
class. We’ll also need to add a set method for the new attribute, and modify the existing canGrow()
and grow()
method in Pet
and it’s subclasses to make calls to the new strategies.
[ACTION]
Make the remaining code changes needed to have Pet
and its subclasses use GrowthStrategy
. This should now mean that there is no special-case grow()
implementation in MagicDragon
and CuddlyToy
. Verify that PetDriver.java
still behaves as expected.
The UML diagram in figure 9.4 should be a good representation of your codebase at the end of this migration task.
9.2.2 Exercise 1b - The State Pattern
In this part of the exercise we’ll be focusing on the State pattern. You will continue to modify the Pet
codebase.
In this task, you should look to apply the State pattern to store attributes related to hunger, and algorithms that depend on those attribute values.
Note that this is the extension/secondary task for “Exercise 1 - Behavioural Patterns”. Detailed instructions are therefore not provided, but a suggested approach might break the modification down into the following four further stages:
- Identify hunger states and their dependant behaviours.
- Implement an abstract
HungerState
that provides method signatures for dependant behaviours. - Implement a concrete implementations for each of the hunger states identified previously.
- Modify the existing
Pet
classes to use the newly created state classes
You may find it helpful to make brief UML sketches as needed as you refactor the code towards the State pattern.
9.3 Workshop Exercise 2 - Structural Patterns
This first section of the workshop focuses on applying the two Structural patterns introduced: Composite, and Adapter.
For this exercise, you’ll be working with a set of classes that represent habitats – places that pets might want to live. The main classes and their members can be represented in a UML class diagram in figure 9.5
In this workshop you’ll be extending and then refactoring the codebase to explore how structural patterns can simplify the process of adding new functionality, and can remove the need for duplicate code.
9.3.1 Exercise 2a - The Composite Pattern
In this part of the exercise we’ll be focusing on the Composite pattern. You will modify the code in 3 stages:
- Add new
Habitat
classes,Cave
,Field
, andMuddyPuddle
- Modify
Habitat
such that it can (optionally) contain a number of childHabitat
objects. - Modify the
describe()
andgetOccupants()
methods to include the values of child objects.
9.3.1.1 Stage 1 - Add new Habitat classes
You’ve been asked to add some new Habitat
classes to represent more specific places that Pets might choose to spend time.
The current description for MythicalCaveSystem
already indicates that the cave system is actually composed of three separate Caves
.
Likewise, the Farm
is described as containing multiple fields and a barn.
You’ve been asked to add three specific new Habitat classes:
-
Cave
- A single cave for dragons to hide in. -
Field
- A field with grass that goats might eat. -
MuddyPuddle
- A patch of muddy water – goats love splashing in puddles.
[ACTION]
Implement three new Habitat
subclass as above, and modify HabitatDriver.java
to demonstrate your new Habitat
subtypes.
9.3.1.2 Stage 2 - Modify Habitat to contain child Habitat objects
We already know that MythicalCaveSystem
contains three Caves
, and that Farm
contains a Field
. We’re going to use the Composite pattern to make this relationship an integral part of our class structure.
To start this refactoring, you’ll need to modify Habitat
to have a list of children; children should be of type Habitat
.
[ACTION]
Modify the Habitat
class to add the new element.
[ACTION]
Create new methods to add, remove and get children to a Habitat
.
[ACTION]
Modify HabitatDriver
to demonstrate that multiple Caves
objects can be added as a child of a MythicalCaveSystem
, and that a Field
can be added as the child of a Farm
.
[ACTION]
Modify HabitatDriver
to demonstrate that an instance of MuddyPuddle
can be added as a child of the Field
(which is itself a child of Farm
).
9.3.1.3 Stage 3 - Modify Habitat to call child methods
The final stage of our refactoring is to make sure that the descriptions of each Habitat
are as complete as possible, and that the occupancy counts are correct (i.e. they include occupants in any part of the Habitat
). To do this, we need to make sure that the describe()
and getOccupants()
of Habitat
recursively call the same methods on any children.
[ACTION]
Modify describe()
to recursively call childHabitat.describe()
for every childHabitat
in the list of children for this habitat. You will need to store the result and build a new formatted description string in the parent.
[ACTION]
Modify getOccupants()
to recursively call childHabitat.getOccupants()
for every childHabitat
in the list of children for this habitat. You will need to store the result to build one complete list of every Pet
in parts of the top-level Habitat
.
[ACTION]
Modify HabitatDriver
to demonstrate that your new describe()
and getOccupants()
methods work as expected. In particular you should confirm that:
- A call to
aMuddyPuddle.describe()
shows only the description for theMuddyPuddle
. - A call to
aField.describe()
shows the description for theField
and theMuddyPuddle
. - A call to
theFarm.describe()
shows the description for theFarm
, theField
and theMuddyPuddle
.
Likewise, you should check calls to getOccupants()
for each of the above, and check both describe()
and getOccupants()
for theCaves
and aCave
.
[OPTIONAL EXTRA]
Modify removeOccupant()
to remove an Occupant from this child Habitats
if they aren’t found in the parent.
The UML diagram in figure 9.6 should be a good representation of your codebase at the end of this migration task.
9.3.2 Exercise 2b - The Adapter Pattern
In this part of the exercise we’ll be focusing on the Adapter pattern. You will continue to modify the Habitat
codebase.
In this task, you should look to apply the Adapter pattern to make a legacy class FieryMountains.java
available as a possible Habitat
.
FieryMountains
was implemented many years ago for a previous game but has lots of neat graphics that the team want to reuse. You should use the Adapter pattern to FieryMountains
to be used as is, as a new Habitat
. You absolutely must not modify FieryMountains.java
, and it must be used in your final solution (i.e. you can’t just copy and paste a few values out and then just ignore it).
Note that this is the extension/secondary task for “Exercise 2 - Structural Patterns”. Detailed instructions are therefore not provided, but a suggested approach might break the modification down into the following four further stages:
- Create a new Java stub
FieryMountainsAdapter
that extends Habitat and stores a newFieryMountains
instance as one of its attributes. - Write a new implementation for
FieryMountainsAdapter.describe()
, that complies with the signature provided for this method inHabitat
and calls relevant functionality fromFieryMountains
. - Modify
HabitatDriver
to demonstrate that fiery mountains can be added to theArrayList
of Habitats, and thatPet
instances (maybe aDragon
?) can be added as an occupant ofFieryMountains
.
You may find it helpful to make brief UML sketches as needed as you refactor the code towards the Adapter pattern.
9.4 Workshop Exercise 3 - Creational Patterns
This first section of the workshop focuses on applying the two Creational patterns introduced: Factory Method, and Singleton.
These two patterns should be more familiar to you, from your experiences in this and other courses. For example, you’ve previously looked at Stendhal’s own Singleton
class RPWorld
in one of the early workshops.
For this exercise, you’ll be working with the Pet
and Habitat
classes you’ve already seen. This time we’re using these classes together as part of a Tamagotchi
application – a simple text based application that lets users look after a virtual pet for a while.
In this workshop you’ll be extending and then refactoring the codebase to explore how creational patterns can allow users to control instantiation of Pets
and Habitats
.
9.4.1 Exercise 3a - The Factory Method
In this part of the exercise we’ll be refactoring the Factory Method to instantiate different Pet
and Habitat
classes at runtime1
This is a much simpler change than previous changes, and can most likely be achieved in 3 stages:
- Add a new
PetCreator
class that createsPet
objects in response to aString
parameter. - Add a new
HabitatCreator
class that createsHabitat
objects in response to aString
parameter. - Modify the
Tamagotchi
class to use the new classes, passing user input in as theString
parameters.
You should now be able to carry out these changes without the more detailed instructions of previous exercises.