What is a good class?

First let's set some basic terminology and make sure it's clear what it is to define a class.

Object basics

To bear in mind throughout: objects have state, behaviour and identity. (This characterisation is, I believe, due to Grady Booch.)

State

The state of an object is the data it contains. This includes the values of its attributes.

When you're working in an imperative object oriented language such as Java, most objects are mutable: their data can change, e.g. an object's state can change as a result of it being sent a message. Some are immutable: their data can't change. For example, objects of Java class String are immutable. Although you can send a String s a message - you can write s.toLowerCase(), for example - to ask it to calculate a new String, this does not change s's state. Instead, it returns a new String object with a different state from s's state. This is not inevitable, it's a design choice: Smalltalk, for example, has mutable String objects. Immutable objects tend to be easier to reason about, but harder to program with. (Much more could be said here.) I suggest as a rule of thumb that the more you tend to think about your class as something that is so small and basic it should be in some class library you didn't have to write, the more likely it is that you should make it immutable. A widely acknowledged mistake in the design of the Java standard library is that it has a mutable Date class. (An illustration of how hard it can be to correct design mistakes if you don't have control over other software that depends on yours is that even though it's been widely agreed to be a mistake for more than 10 years, it has not proved possible to correct this in later releases of the Java library!)

An interesting subtlety is whether, if object p has a reference to object q, we should count the state of object q as also being part of p's state. Normally we wouldn't: we'd include the fact that p is linked to this particular object q as part of p's state, but we'd stop there. You should bear in mind that if you take this definition, a consequence is that p's behaviour on receiving a certain message could change, even without p's state changing in the meantime.

Behaviour

The behaviour of an object is what it does. This includes what messages it understands, and what it does on receipt of a message. Here "what it does" includes everything: what messages it sends, what changes it makes to the outside world by e.g. printing things, what changes it makes to its own state and what it returns. The behaviour of an object can affect its state, and can also be affected by its state.

Behaviour is a more inclusive term than interface. The interface of an object is usually understood to be the messages an object can receive and some specification of what it will do in response. Behaviour includes all the details. You design the behaviour; you want your clients to rely on the interface. (And, as we'll discuss later when we get to Liskov substitutivity, one problem is that sometimes they actually rely on more than what you think of as the interface!)

Identity

We say that an object has identity to capture the idea that two objects could have identical state, and yet be different objects, and that we might need to know this. For example, if we send one of them a message that changes its state, now the two objects no longer have identical state.

(It wasn't an accident that that example relied on the object being mutable. If objects of the same class have identical state and are immutable, then you have (by definition) no way to tell the difference between them. So although we could argue about whether immutable objects still have identity or not, at least we won't have to worry about it; we could assume two immutable objects with the same state were the same object and it wouldn't get us into any trouble. This is why immutable objects are easier to reason about.)

Designing classes

So, designing a class involves deciding on its state and behaviour. (We don't decide whether its objects will have identity, that's an emergent property!) Deciding on an object's state is usually relatively straightforward; behaviour is harder.

The overall behaviour of the system in a given scenario comes from the requirements, and is recorded in the use case text. We can choose how to approach the task:

The anthropomorphic principle is sometimes the best guide: once you have an understanding of what an object is supposed to be, what is it reasonable to expect this object to know, and what should it be able to do?

The process of class design is not really algorithmic - practice usually includes elements of all these approaches, and requires human intelligence and practice.

When is a class good?

Strictly speaking it's always going to be a set of classes that is good: but in this note I want to focus on the stuff that you can mostly tell by looking at an individual class. You will get a feel for when an individual class is well designed and when it isn't. Sometimes a class you have to use always seems to have the method you want it to have, it's easy to find it, and you don't make mistakes when you're using it. That's what we're aiming for. What can we say about the properties of such a class?

It will have: low coupling (minimise unnecessary dependencies between classes; have only the dependencies people would naturally expect to exist) and high cohesion (make each class represent the whole of an easily describable abstraction), and present an interface to the world that has an appropriate balance of sufficiency (give clients enough operations so that they can do what they need to), completeness (give them all the operations they might reasonably want) and primitiveness (only provide the operations that really need access to the underlying data). (Grady Booch Object Oriented Design with Applications, p123ff).

These things are in tension. For example, if you are wondering whether to split a class into two because you think it's actually representing two different concepts, doing so may increase cohesion, but at the same time, it may introduce coupling (e.g. between the two new classes) that wasn't visible before. Increasing the completeness of an interface (e.g. by adding a utility method that several of your clients want, even though they could implement it themselves using already existing operations) tends to reduce its primitiveness, and vice versa. That's just how it is...


This page is maintained by Perdita Stevens (perdita@inf.ed.ac.uk)


Home : Teaching : Courses : Seoc : 2015_2016 : Schedule 

Informatics Forum, 10 Crichton Street, Edinburgh, EH8 9AB, Scotland, UK
Tel: +44 131 651 5661, Fax: +44 131 651 1426, E-mail: school-office@inf.ed.ac.uk
Please contact our webadmin with any comments or corrections. Logging and Cookies
Unless explicitly stated otherwise, all material is copyright © The University of Edinburgh