-
Notifications
You must be signed in to change notification settings - Fork 9
/
09-patterning.Rmd
249 lines (144 loc) · 17.6 KB
/
09-patterning.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# Software design patterns {#patterning}
## 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](https://en.wikipedia.org/wiki/Design_Patterns) [@GoF]
* 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.
## Workshop Exercise 1 - Behavioural Patterns {#behave}
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 \@ref(fig:basicpets-fig)
```{r basicpets-fig, echo = FALSE, fig.align = "center", out.width = "100%", fig.cap = "Pets, Cats, Goats and Magic Dragons"}
knitr::include_graphics("images/BasicPets.png")
```
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.
### Exercise 1a - The Strategy Pattern {#strategy}
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.
#### Stage 1 - Add a new Pet class {#pet}
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.
#### 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 \@ref(fig:petsubclasses-fig)
```{r petsubclasses-fig, echo = FALSE, fig.align = "center", out.width = "100%", fig.cap = "Possible subclasses of Pet"}
knitr::include_graphics("images/GrowthSubclasses.png")
```
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 \@ref(fig:petsubclasses2-fig)
```{r petsubclasses2-fig, echo = FALSE, fig.align = "center", out.width = "100%", fig.cap = "A badly designed hierarchy"}
knitr::include_graphics("images/GrowthSubclasses2.png")
```
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.
#### Stage 3 - Introduce a GrowthStrategy {#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]`** \color{black} Create a new `GrowthStrategy` class, with abstract method signatures for `canGrow()` and `Grow()`.
#### Stage 4 - Implement concrete growth strategies {#growthstrategies}
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.
#### Stage 5 - Modify the codebase to use our GrowthStrategy {#modifys}
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 \@ref(fig:petstrategy-fig) should be a good representation of your codebase at the end of this migration task.
```{r petstrategy-fig, echo = FALSE, fig.align = "center", out.width = "100%", fig.cap = "Your codebase should look something like this at the end of this migration task"}
knitr::include_graphics("images/GrowthStrategy.png")
```
### Exercise 1b - The State Pattern {#state}
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.
1. Implement an abstract `HungerState` that provides method signatures for dependant behaviours.
1. Implement a concrete implementations for each of the hunger states identified previously.
1. Modify the existing `Pet` classes to use the newly created state classes
<!--%Note that this is the extension/secondary task for ``Exercise 1 - Behavioural Patterns'' and as such, the descriptions are briefer with limited UML support.-->
You may find it helpful to make brief UML sketches as needed as you refactor the code towards the State pattern.
<!--
%\subsubsection{Stage 6 - Identify hunger states and dependant behaviours}
%\subsubsection{Stage 7 - Introduce a HungerState}
%\subsubsection{Stage 8 - Implement concrete hunger states}
%\subsubsection{Stage 9 - Modify the codebase to use our HungerState}-->
## Workshop Exercise 2 - Structural Patterns {#structural}
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 \@ref(fig:habitat-fig)
```{r habitat-fig, echo = FALSE, fig.align = "center", out.width = "100%", fig.cap = "Subclasses of Habitat"}
knitr::include_graphics("images/Habitat.png")
```
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.
### Exercise 2a - The Composite Pattern {#composite}
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`
<!--%\item Consider how one might use sub-class/super-class relationships to avoid duplicate code.-->
1. Modify `Habitat` such that it can (optionally) contain a number of child `Habitat` objects.
1. Modify the `describe()` and `getOccupants()` methods to include the values of child objects.
#### Stage 1 - Add new Habitat classes {#habitats}
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.
#### Stage 2 - Modify Habitat to contain child Habitat objects {#children}
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`).
#### Stage 3 - Modify Habitat to call child methods {#childmethods}
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 \@ref(fig:composite-fig) should be a good representation of your codebase at the end of this migration task.
```{r composite-fig, echo = FALSE, fig.align = "center", out.width = "100%", fig.cap = "Your codebase should look something like this at the end of this migration task"}
knitr::include_graphics("images/HabitatComposite.png")
```
### Exercise 2b - The Adapter Pattern {#adapter}
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.
1. Write a new implementation for `FieryMountainsAdapter.describe()`, that complies with the signature provided for this method in `Habitat` and calls relevant functionality from `FieryMountains`.
1. 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.
<!--
%\subsubsection{Stage 4 - Add a new FieryMountainsAdapter class}
%\subsubsection{Stage 5 - Implement describe}
%\subsubsection{Stage 6 - Modify the driver}-->
## Workshop Exercise 3 - Creational Patterns {#creational}
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`.
### Exercise 3a - The Factory Method {#factory}
In this part of the exercise we'll be refactoring \textbf{towards} the Factory Method to instantiate different `Pet` and `Habitat` classes at runtime^[Note that it would also be possible to achieve this using Reflection, but in this case we'll be demonstrating how a Factory Method might be applied.]
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.
1. Add a new `HabitatCreator` class that creates `Habitat` objects in response to a `String` parameter.
1. 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.
### Exercise 3b - The Singleton Pattern {#singleton}
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.