CPSC 329 Software Development Fall 2026

CPSC 329 Lab 6: Patterns

This lab gives you practice applying design patterns to improve the design of existing code. You'll refactor a maze solver to use Strategy and a simple factory, eliminating near-duplicate classes, and refactor a Breakout game to use Decorator so that new kinds of special bricks can be added without changing the existing classes. Along the way, you'll use characterization tests as a safety net to make sure the refactoring doesn't change the programs' behavior and let Eclipse's refactoring tools do much of the mechanical work.

Objectives

Successful completion of this lab means that you:

Collaboration and AI

Labs are for learning and practice — you may help and be helped by others when it comes to figuring out what to do, but in the end you need to understand and be able to do the things for yourself.

This lab is individual — everyone works with their own repository and does their own refactoring.

You may not use AI to figure out what to do or to do the task for you.

Due Date, Deliverables, and Handin

due Fri 10/9 at the start of class

To hand in your work:


Preliminaries

Provided Code

You have been provided with two complete programs: a maze solver and a game of breakout.

Maze solver: The solver package contains implementations of three maze solving algorithms (depth-first search, breadth-first search, and best-first search); the main package contains the corresponding main programs to run them. The remaining packages provide support code — a parser to read maze description files and a maze data structure — that you shouldn't need to work with directly. The details and tradeoffs of the different maze solving techniques are not the focus of this assignment (so don't worry if you aren't familiar with them), but if you are curious, you'll run the programs to establish a baseline before refactoring — observe how their strategies differ.

Breakout: The provided code is a complete implementation of a basic Breakout game. (There are some quirks with handling ball bounces; it's a basic game.) Move the mouse in and out of the game window to start and pause the game. Control the paddle by moving the mouse left or right — the paddle moves towards the mouse position. (No need to click any buttons.) The provided code is thin on comments but the organization should be reasonably straightforward. The main program is BreakoutMain.

Setup

Both programs in this lab make use of JavaFX, so you'll need to configure the Java settings in Eclipse:

New projects you create should now be set up for JavaFX.


Maze Solver

In this part you'll refactor a maze solver program to avoid redundant code and make it easier to extend.

Project and Repository Setup

Create an Eclipse project and share it to a local Git repository:

Characterization Tests

Much of this lab is about refactoring code to improve its design by following design patterns — there's no change in functionality. To ensure that refactoring doesn't break anything, we can write a set of JUnit characterization tests which capture the application's current behavior and then re-run them when the refactoring is complete.

A set of characterization tests have been provided in the test package. These check the stats produced by the maze solver during the solving process. You'll need to fill in the baseline values:

Strategy

If you look through the provided code, you'll notice several sets of parallel class names — there are DFS, BFS, and best-first versions of the application (in main), the maze solver itself (in solver), and the tests (in test).

Compare the solver classes:

Apart from the class and constructor names, all of the differences in the solver classes are in the solve() method and involve the discovered collection — the declaration, the construction, adding a room, taking the next room, and checking if the collection is empty.

Both Strategy and Template Method support encapsulating what varies where the variants are peers. (Different collections of rooms are independent of each other.) Strategy is the better fit here: what varies is only one piece of the behavior — which room is next — and is an aspect of something the solver uses — the collection of discovered rooms — rather than steps in an algorithm — solve() is the same in all three cases, except for the discovered rooms collection. (Template Method would be the better choice if future maze-solving strategies needed to change the algorithm itself and not just the order of rooms.)

Strategy calls for the code structure shown:

The next several sections go through refactoring the provided code to implement Strategy.

Define the Discovered Interface

Design notes:

Implement the Concrete Strategies

Create the (single) MazeSolver Context

Rather than creating this from scratch (and copying over a lot of code from the existing solvers), first turn MazeSolverDFS into a general MazeSolver through a short series of refactorings:

Update the Applications and Tests

MazeSolverDFS and MazeSolverDFSTest are already done; the refactorings in the previous step means they now correctly uses MazeSolver. For BFS and best-first:

Simple Factory

The Strategy refactoring eliminated the nearly-identical solvers but there are still three nearly-identical applications.

Compare the application main programs:

There are only two differences: the title of the window and which discovered collection is created. The same thing occurs in the tests — the only difference is the discovered collection.

Why simple factory? A simple factory centralizes object-creation logic that would otherwise be repeated in several places — here, the code in each application and test that chooses and builds a Discovered object. It fits well because the choice is made from a value: the client supplies a SolverType and the factory returns the matching Discovered object without the client needing to know which class that is or what it needs to be constructed.

The tradeoff is that a simple factory isn't closed for modification: adding a new solver algorithm means editing the factory as well as adding a new Discovered class and a new SolverType value. That is acceptable here because the edits are small and in known places, and the clients don't have to change at all. The factory also doesn't limit how the solver can be used — someone who couldn't modify the factory could still create their own Discovered object and pass it to MazeSolver directly.

Simple factory calls for the code structure shown:

The next several sections go through refactoring the provided code to implement a simple factory.

Create the SolverType enum

Clients need a way to tell the factory which type of solver they want without naming a specific Discovered class. Defining an enum instead of using a String or int value makes a bad solver type a compile-time problem rather than a runtime problem. (It also lists all of the valid values in one place.)

Create DiscoveredFactory

The factory is the one place that will know about the concrete discovered collection classes.

Use the Factory

Switch the tests first:

The rest of this section is optional — come back if you have time. The remaining step is to create a single application which provides the user with the choice of solvers. This is optional because it requires some UI updates so it is a little more involved than the updates to the tests and it doesn't add anything new in terms of use of the factory.

Turn MazeSolverDFSApp into the general MazeSolverApp:


Breakout (Optional)

This section is optional. You are encouraged to do it if you have time in order to see Decorator in action — there are some subtleties in how all the details come together, and it is important to see those to really understand how Decorator works — but it is not required for full credit on the lab.

The object of the game Breakout is to clear all the bricks from the top of the screen by bouncing a ball off of them. The player moves a paddle back and forth along the bottom of the screen to keep the ball in play; a brick is cleared once it has been hit enough times and the player scores points for each brick cleared. If the ball gets past the paddle, the player loses that ball. The game ends when all the bricks are cleared or the player runs out of balls.

So far the game has only basic bricks — it takes some number of hits to clear them, but all that happens when a brick is cleared is that the player scores some points. In this part you'll refactor the code to allow for special bricks that can make the ball or paddle bigger or smaller, speed the ball up or slow it down, add scoring multipliers, and so on. You'll also add functionality to support ball-resizing bricks.

Project and Repository Setup

Create an Eclipse project and share it to a local Git repository:

Characterization Tests

As with maze solver, this part starts off with some refactoring. A set of characterization tests have been provided to provide a safety net that the refactoring doesn't break anything.

Decorator

Decorator's intent is to add responsibilities to an individual object at runtime, without changing its class. A decorator wraps an object, has the same interface as that object, passes most calls through to it, and changes or adds to the behavior of a few. Because a decorator looks just like the thing it wraps, decorators can wrap other decorators, so effects stack. That matches what is needed to support special bricks need: effects are added to an existing object during play (e.g. when a resizer brick is cleared, the ball already in play must start behaving differently) and effects combine and accumulate (e.g. clearing two resizer bricks should resize the ball twice, and other extensions, such as a speed-up brick, should combine with resizing). The same reasoning applies to bricks: a special brick is an ordinary brick with its own position, color, points and hits, plus zero or more stackable effects.

Decorator calls for the code structure shown:

The next several sections go through refactoring the provided code to implement Decorator and to add the resizing effect.

Make the Ball Decorable

A decorator must be usable anywhere the original object is. So the game needs to depend on a Ball interface, which both the original ball and its decorators implement.

DecoratedBall is the base class for all ball decorators. It holds the wrapped ball and just passes every call through to it — the default is no new behavior. A concrete decorator then overrides only the methods whose behavior it changes. To create DecoratedBall:

Finally:

Create ResizedBall

ResizedBall is a concrete decorator that changes the ball's size by a fixed amount. The amount can be negative, to shrink the ball.

Why override draw as well as getRadius? The inherited draw passes the call to the wrapped ball and PlainBall.draw calls its own getRadius(), not the decorator's inherited one — the wrapped object doesn't know it's wrapped. As a result, a decorator must override every method whose result depends on the behavior it changes.

Make Bricks Decorable

Bricks follow the same pattern as the ball: an interface, the original class renamed as the plain version, an abstract base decorator, and a concrete decorator. Bricks also get one new operation, decorate, which applies a brick's effect to the ball.

Now create a decorated brick:

Finally:

Create ResizerBrick

ResizerBrick is a concrete brick decorator. When it is cleared, it resizes the ball. It also draws itself differently, so the player can tell which bricks are special.

It's worth taking a moment to unpack what decorate() is doing and why it wraps super.decorate(ball) rather than just ball. Remember that ResizerBrick extends DecoratedBrick, so super.decorate(ball) invokes DecoratedBrick's version, which passes the call on to the wrapped brick. The wrapped brick is then able to apply its effect to the ball first; ResizerBrick adds its own effect on top of the result. If decorate() instead returned new ResizedBall(ball,amount_), it would apply only its own effect and the effect of any wrapped brick decorator would be lost.

In general a decorator's methods should build on what the wrapped object does — delegate, then add — rather than replace it. That's the key that lets decorators stack! The exception is when a decorator deliberately replaces behavior rather than building on it, such as with ResizedBall's draw() — a resized ball should be drawn as a circle with the appropriate radius, not by adding on to the drawing of the original ball.

Use the Decorations

Place some resizer bricks:

Apply a brick's effect when it is cleared:

Add tests for the new behavior: