(Coursenotes for CSC 203 Project-based Object-oriented Programming and Design)

A class design process

In this lesson we’ll implement a simple game using an object-oriented (OO) design process. The goal is to get a tiny taste of the thought process that goes into designing an OO system with multiple interacting classes.

Game requirements

We’re going to implement the Game of Nim. Lets start by first understanding the game we’re building. The rules are simple:

We’re going to build this as a text-based game. Here is how an example game might go:

Player 1's name? Yaa
Player 2's name? Michael
How many sticks? 5
Yaa, how many sticks do you want to take? 2
Yaa takes 2 stick(s).
There are 3 left in the pile.
Michael, how many sticks do you want to take? 1
Michael takes 1 stick(s).
There are 2 left in the pile.
Yaa, how many sticks do you want to take? 1
Yaa takes 1 stick(s).
There are 1 left in the pile.
Michael, how many sticks do you want to take? 1
Michael takes 1 stick(s).
There are 0 left in the pile.
Yaa is the winner

Why follow a principled design process?

Before we proceed, I want to acknowledge that the requirements, as described above, are fairly simple. You could probably write the whole game in a couple hundred lines in a single file.

That is fine: we’re still going to use an OO process to design and implement this game. Remember, our goal is to learn to follow a principled design process when we engineer software. OO design is one such process to help us write maintainable code.

But it’s worth questioning: what do we actually mean by that? When I say we should prioritise maintainability, I’m thinking of the following:

All that to say: it pays to follow a principled software design process. That is, we want to write loosely coupled, tightly cohesive modules (where “modules” can mean functions, classes, packages, etc.; I use “module” as a general term to emphasise that these principles are not unique to OOP).

Designing the classes needed for the game

I find it useful to ask a series of questions to help me understand what it is I’m building.

What data do we need to keep track of to run this game? To what entities do those pieces of data belong? For what behaviours (functionality) is each entity responsible?

In the game rules, a player must remove 1–3 sticks from the pile. This means we must not allow player turns in which the player tries to remove < 1 stick or > 3 sticks. Should this check be done by the Pile, the Player, or the Game? Why?

In this text based game, the Player might enter text that cannot be parsed into an int while choosing the number of sticks they want to pick up. How should this be handled? Whose job is it to handle it?

We will discuss the above together as a class. That discussion should ideally give us an idea of what classes should exist in our system, and roughly what each of them should be responsible for.

Here is one possible implementation of the game — we may come up with something different in class, or you may come up with something different on your own. That is fine.

The Player class

View in new tab

The Pile class

View in new tab

The Game class

The Game class is our “controller” — it sits above and in-between the Player and Pile classes, managing and mediating those classes’ communications with each other.

That is, the Game prompts the Player to see how many sticks they want to pick up, and sends that input to the Pile. The Pile acts on this information, updating its number of sticks accordingly. The Game then inspects the Pile to see if the game is over or not (by using the Pile’s getter method).

View in new tab

Supporting additional features

As we’ve said before, a central theme in software engineering is that your requirements can change. For example, as you think about designing the game as described above, think about how easy to difficult it would be to support features like the following. How much of our code would need to change to allow these features?

You will find that there is a balance to be achieved between two extremes. On one end, you can put in huge amounts of design effort upfront, and try to prepare your code to easily handle any updates to the requirements, i.e., by strictly adhering to principles like information hiding and creating new abstractions (e.g., classes) to support most key features. The downside is that you often don’t need all of those abstractions—that design and preparatory implementation work can go to waste1, and worse, it can make your code harder to read and understand for someone who is not already familiar with the codebase.

On the other extreme, you can eschew this upfront design work, and when changes to requirements inevitably do crop up, you can pay the piper then. Though by that time you may have accrued a fair amount of technical debt.2

  1. Many pithy acronyms for engineering principles hint at this, like YAGNI (you aren’t gonna need it) and KISS (keep it simple, stupid!) 

  2. Technical debt is a metaphor describing the situation in which developers sacrifice some software maintenance tasks (like software design, testing, documentation, refactoring) in favour of speedy implementations and deployments. Sometimes this is fine, as long as that “debt” is repaid soon. A little debt speeds development so long as it is paid back promptly with refactoring.