Growing Object-Oriented Software, Guided By Tests (chapter 2)
Feb 05, 22Chapter 2
Object-oriented design focuses more on the communication (messages) between the objects, rather than the objects themselves.
An OO system is a web of collaborating objects that send messages to each other.
The behaviour of a system is determined by the composition of the objects (and how they are connected), and the messages they send to one another.
Values and objects
Values are immutable, that model fixed properties - and could be said to be fungible (lololol). The reason for this is that two values are fundamentally the same if they have the same state
Objects can hold state that can be mutated as part of a computational process. Two objects may begin with the same state, however, this can diverge as the system runs.
Practically this means we should split our program into these 2 worlds - Values and Objects.
Objects should be easily ‘pluggable’ i.e we should be able to compose more complex systems from small and simple interchangeable objects.
We can use tests and mock objects to check just how ‘pluggable’ our system is. We should be able to see how the objects in the system are communicating with one another.
Try to think about objects in terms of roles, responsibilities and collaborators. Use a low tech method (whiteboarding) to flesh out ideas and structure before committing the ideas to code.
Tell don’t ask
Follow the law of Demeter - avoid creating huge method chains in your code, when these can easily be abstracted into one method call. I.e the calling object should tell the other object what it wants, and the callee should do all of the work ‘behind closed doors’.
Example:
bad = FilterNumbersLessThanTwo.new(array).data.map(&:to_i).sort.reject {|x| x <
2 }
good = FilterNumbersLessThanTwo.call(array)
# move .map(&:to_i).sort.reject {|x| x < 2 } into the object itself
This also forces us to be explicit about what an object does and leads to some noice encapsulation - making the system easier to test.
However, because we are encapsulating all of the logic, how are we to test how objects interact with each other? We test them at their interface level, and we also use mocks and stubs to stand in for objects that are receiving messages for the SUT (subject under test).
Can also have OUT -> object under test.
This approach makes it easy for us to test in the ‘seams’ of the program,
hopefully keeping bugs to a minimum. The authors all call this approach
interface discovery
, as working this way should expose the way the system is
communicating between objects.
Care must be taken when mocking/stubbing related objects though - it can be too easy to not test the correct behaviour if we overutilize this approach.
The essentials of a test are thus:
- Create any required mock objects
- Create any real objects, including the OUT/SUT
- Specify how you expect the mock objects to be called by the OUT
- Call the triggering methods on the OUT
- Assert that any resulting values are valid and that all the expected calls have been made.
The most important part is that we make clear the intention of every test, and we distinguish between the functionality we are testing, the supporting infrastructure and object structure.