For ages I treated objects as glorified data structures to be operated on by procedural code. It took me a long time to get out of the procedural mindset and into an object-oriented mindset.
Well-written object-oriented code helps you deal with complexity by hiding internal details so that you can operate at ever higher levels of abstraction. Object-oriented code increases flexibility – objects can be plugged together in different ways like Lego™ pieces – and it keeps duplication and churn to a minimum by keeping data and operations in the same place instead of spreading them throughout the code-base.
The recent discussion on the GOOS mailing list has made me realise that others are also struggling with the transition. Nat Pryce has posted a couple of blog entries today, and I'd like to pick up the same theme with an example in the hope it might help.
Let's start with a simple PopGroup class that implements the role of SongPerformer (in Java this would be an interface):
To construct a valid PopGroup you need a Singer, a Drummer and a Keyboardist. These are the PopGroup's dependencies.
The responsibility for coordinating the various activities to perform songs is the responsibility of the PopGroup object, but the actual acts of singing, drumming and keyboard playing are separated out. This is the single responsibility principle combined with dependency injection.
This approach makes the design highly flexible because we can plug together all kinds of variations. As long as the dependencies we inject implement the right interfaces we can make the pop group perform a song.
If we don't have a DrumMachine, we can substitute a PhysicalDrummer and the PopGroup will still function the same.
Our PhysicalDrummer object depends on interfaces not implementations. If we're desperate we could grab a passer by, a dustbin and a broom handle and create a drummer:
Unfortunately the BroomHandle class does not implement the Drumstick interface. We could make it implement Drumstick, but the BroomHandle is in a different domain. It's in the domain of sweeping, rather than musical performances. We don't really want to couple its class to an interface from the musical performances domain, so we create an adapter to map between the two. This is known as a ports and adapters architecture.
To make things easier to understand we can wrap the objects into a class that has a name that better expresses the intent.
Now we've hidden all the complexity and we're operating at a higher-level of abstraction.
In reality, a ports and adapters architecture is not so much mapping between entirely unconnected domains as mapping lower-level domains to higher-level domains, so that the technical detail is hidden within simpler more abstract concepts.