Growing Object-Oriented Software, Guided By Tests (chapter 6)
Feb 09, 22After yesterday’s mammoth note-taking session, I am going to try and keep today’s notes a bit more concise.
Value code that is easier to maintain than it is to write. If we implement a feature directly, this can make the code difficult to understand and maintain - see Clean Code - Bob Martin
2 things to keep in mind when designing systems:
- Separation of concerns (make things easy to test/change)
- Higher levels of abstraction (make complicated things easier to work with)
If we apply the two points consistently, we should end up with a system that roughly implements the ‘ports and adapters’ pattern (otherwise known as hexagonal architecture). Link.
Basically… Separate the technical implementation (DB’s, UI, cron jobs etc) from the core domain business logic. The only communication between should be via interfaces.
Program to interfaces/abstractions, NOT concretions!
We do have to make sure that each object sits at the right level of abstraction, and only exposes parts of itself that are necessary for its peer objects to function. We need clean interfaces, please.
A style pointer to think about -> When passing messages between objects, use standard OO coding practices. In the internals of objects, try using a more functional style (as this code will not be exposed to the wider system, and builds behaviour from methods and values that have no side effects) 🤔. Immutability within objects is the key takeaway here. It should lead to safer code.
To clarify the above point - use message passing style between objects, functional style inside objects. Use small, specialised methods within objects. The message passing between objects happens via their public interfaces.
Important note: Every object should have a clearly defined responsibility (Single Responsibility Principle)
This will help us to decide whether we need to create a new object/service to be called or extend an existing object.
We should be able to describe what an object does without using ‘and’/’or’
If we break this rule, the chances are we are breaking the SRP.
This also goes for when we are creating abstractions out of many objects - We should be able to clearly describe what the new abstraction will achieve without using ‘and’/’or’
How do we decide how objects interact with each other?
-
Dependencies These objects cannot function without each other.
-
Notifications Objects that send messages to listeners (without knowledge of which objects are listening). Helps to decouple the system.
-
Adjustments Peer objects will modify objects behaviour to fit in with the wider system. An example of this is the strategy pattern from the gang of 4
Use the patterns based on the system you are creating -> there are no hard and fast rules!
To make sure that we are always creating valid objects, we should strive to pass in the required objects via the constructer/initializer. A hint of dependency injection here… If the object we are passing in is a direct dependency, we should initialize it within the constructer so we can be sure it is valid during the life of the object. If the object we are passing in is a notification/adjustment they can be initialized as a sensible default, and perhaps overwritten later.
Defaults
- Notifications = null object/empty collection
- Adjustments = common values
Essentially we can add methods that will allow callers to change these default values, and add or remove listeners. Pretty smart.
When dealing with composite objects/abstractions, we should always follow the below mantra:
Composite objects should be simpler than the sum of their parts
The API of a composite object should not be more complicated than that of any of its components.
As an example, think of a vending machine - Its rough API is inserting coins, pressing buttons, and dispensing treats. But on the inside, there will be a multitude of internal processes required to dispense the items, all of which are abstracted away from the user of the vending machine.
We can also make composite objects out of smaller, simpler composite objects. However, we must always try to keep the complexity under control and make meaningful abstractions.
We should make all objects ‘context independent’ - the object should work in isolation with just the data that has been passed in when initialized or modified via the public methods. This goes back to the separation of concerns and making the object’s context-independent will ensure they are easier to test, which in turn will reinforce that our design is sufficient.
A class that uses terms from multiple domains might be violating context independence unless it’s part of a bridging layer.
The independence of objects also means they can be reused, perhaps with slightly different inputs (think of a loosely coupled interface that implements behaviour based on requirements).
Be careful not to hide data within classes. Encapsulation is amazing, but we should avoid hiding domain knowledge within a class (e.g file paths, knowledge of data structures)