Growing Object-Oriented Software, Guided By Tests (chapter 7)

Feb 10, 22

Using the design principles we have investigated in previous chapters, we should strive to find the boundaries for objects in our system.

Calling objects want to know what their neighbouring objects do, but not how they work.

Objects should make coherent units within the context of our system, and thus should be easier to reason about and test.

This should make our system flexible, and adaptable as requirements change (remember, the only constant in software engineering is change).

3 aspects of TDD that will help us to scope out our system

  1. When we start with a test, we have to describe what we want to achieve before we consider how we will implement it. This helps us to maintain the correct level of abstraction - if the unit tests are unclear, then it probably means we weren’t ready to start coding.
  2. We have to limit the scope of our tests. Be wary of huge unit tests that are doing too much. Ensure that each test is focused on the behaviour of the object, not pulling in the entire world just for the setup. In this scenario, we should try and break down the unit into composite pieces to allow more focused behaviour/testing.
  3. To be able to pass dependencies to an object under test, we have to know what they are. We should make a note of where it is painful to set up objects (passing in lots of dependencies). If an object is painful to set up, this should be an indication that it is not ‘context independent’ and we should try to refactor this.

The way classes are designed is important, but it is more important how the classes within a system interact with each other.

Interfaces describe whether two components will fit together, protocols describe whether they will work together.

We can use mock objects to expose the communication protocols within our system (to see which objects are peers). If a test is clumsy (pulling in a large number of other objects) or unclear, we should look to refactor and rebalance the responsibilities between the objects.

We shouldn’t mock the internals of objects! Mocking is a tool to find the seams (communication protocols) within the system.


When using value types, although they are immutable we should seek to give them a discrete type. This will help us to understand the purpose they are serving.

3 ways we can identify value types

Breaking out: code that has become too complex is broken down into smaller, more focused components.

Budding off: when creating a new domain concept in the code, make a ‘blank’ type with a descriptive name. Fill in the behaviour later.

Bundling up: when there is a group of values that always seem to be used together, can we roll these up into a construct? We can also add behaviour later potentially, and a clean interface.

3 ways we can identify object types

Breaking out: If we feel the objects are getting too complex, we can break them out into smaller objects. Favour using a spike here to get a feel for how the refactoring will look. Then roll back and implement the changes cleanly (with tests).

Break up an object if it becomes too large to test easily, or if the error messages are hard to interpret. Unit-test the smaller pieces separately.

Budding off: When introducing a new peer object, mock the peer object from the point of view of the calling object. This will help to confirm the required behaviours. Then we can flesh out the peer objects implementation by writing tests and creating them. In the process, we may identify that the new object also needs to interface with a new peer object of its own, so in this case, we repeat the process.

We should always strive to start writing tests from the point of view of the consumer, mocking the new object before it exists. This will ensure we are testing behaviour and will drive our system forward, leading us finally to the internal implementation details of the new object/interface/abstraction.

Bundling up: bundle up a cluster of related objects into a containing object. Giving the new object a meaningful name helps us to create bounded contexts within our domain. This should allow us to identify the scope of the dependencies more clearly, as we can see the boundaries of the concept. We should be able to be more precise with our unit testing, mocking objects in the places that were previously called one of its component objects (we should be using an interface to allow this to happen cleanly). When the tests become too complicated, consider creating abstractions out of composite objects.


Try to keep the focus of interfaces as narrow as possible. If we are following the earlier rule about ‘pulling’ objects into existence by creating them as mocks/interfaces first, this should be natural.

We should also refactor interfaces as the system grows (use the above 3 points as a guide!)


We should strive to write systems that are composed of small programs, and abstract away control flow and data manipulation.

A good example of this is the Unix philosophy, where the results of programs can be piped to one another cleanly.

Focus less on classes, and more on the communication between classes (via interfaces in Java, or duck typing in Ruby).