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:

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

Pets, Cats, Goats and Magic Dragons

Figure 9.1: Pets, Cats, Goats and Magic Dragons

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 class CuddlyToy 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 are ADULT_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

Possible subclasses of Pet

Figure 9.2: Possible subclasses of Pet

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

A badly designed hierarchy

Figure 9.3: A badly designed hierarchy

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.

Your codebase should look something like this at the end of this migration task

Figure 9.4: Your codebase should look something like this 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:

  1. Identify hunger states and their dependant behaviours.
  2. Implement an abstract HungerState that provides method signatures for dependant behaviours.
  3. Implement a concrete implementations for each of the hunger states identified previously.
  4. 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

Subclasses of Habitat

Figure 9.5: Subclasses of Habitat

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:

  1. Add new Habitat classes, Cave, Field, and MuddyPuddle
  2. Modify Habitat such that it can (optionally) contain a number of child Habitat objects.
  3. Modify the describe() and getOccupants() 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 the MuddyPuddle.
  • A call to aField.describe() shows the description for the Field and the MuddyPuddle.
  • A call to theFarm.describe() shows the description for the Farm, the Field and the MuddyPuddle.

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.

Your codebase should look something like this at the end of this migration task

Figure 9.6: Your codebase should look something like this 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:

  1. Create a new Java stub FieryMountainsAdapter that extends Habitat and stores a new FieryMountains instance as one of its attributes.
  2. Write a new implementation for FieryMountainsAdapter.describe(), that complies with the signature provided for this method in Habitat and calls relevant functionality from FieryMountains.
  3. Modify HabitatDriver to demonstrate that fiery mountains can be added to the ArrayList of Habitats, and that Pet instances (maybe a Dragon?) can be added as an occupant of FieryMountains.

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:

  1. Add a new PetCreator class that creates Pet objects in response to a String parameter.
  2. Add a new HabitatCreator class that creates Habitat objects in response to a String parameter.
  3. Modify the Tamagotchi class to use the new classes, passing user input in as the String parameters.

You should now be able to carry out these changes without the more detailed instructions of previous exercises.

9.4.2 Exercise 3b - The Singleton Pattern

In this final part of the exercise you should consider if there is a sensible application of the Singleton pattern in any of the application code you have worked with in today’s exercises.