Over the past five weeks, I have been sharing my understanding of the SOLID principles with my team. Now being at the end of week five I wanted to share this understanding with everyone.
S is for Single Responsibility
Now, this seems straightforward and easy, but can you define what a responsibility is? No googling, no looking at Wikipedia, no grabbing your copy of the Clean Coder ;). What responsibility boils down to is "reason to change". It's easy to write code that has many reasons to change, really easy so don't get too stressed out about your code base. The trick is to extract responsibilities as things change, this is when they are easiest to identify in my experience. If you have built your code base Test First then this refactoring is straightforward, if you have no tests consider creating some "Characterisation Tests". My favourite advantage of conforming to this principle is you know where to go to make a change or where to start when debugging, making changes with confidence that you will not interfere with other responsibilities.
O is for Open Closed
In our second week, we had a bit of an oxymoron. How can something be open for change but closed for modification? The answer, like many things in software development, is polymorphism. Create a new derivative and override where you need to alter functionality, don't get too eager though and create a new derivative for a bug. In the case of a defect, most definitely crack open the source file and alter the code to address it, I don't see that as a violation. The main advantage here is that alteration of functionality will not affect any existing consumers, this is particularly important for public classes and even more so for public contracts such as a REST API, as there is no transparency into how they are being used.
L is for Liskov Substitution
The general plan here is to ensure that derivatives you created last week that conform to Open/Closed maintain the integrity of the existing logical pathways in your code. As an example, by swapping out an in-memory persistence derivative with one that saves to a MONGO database there should be no logical difference to the rest of the program. The fact that you have changed the technology that houses persistence should be inconsequential. "A good architect maximizes the number of decisions not made", conforming to this principle is this quote in action as you delay your decision to use MONGO as persistence until you absolutely need to make it, and hopefully, with no consequences to the rest of your program.
I is for Interface Segregation
I had never really formed a working understanding of this principle and just treated it as creating focused interfaces accepting it without question. It was not until I had to think about formalising it and explaining it to my team that I had to understand why, why bother conforming? The answer is when you create small client-focused interfaces you reduce coupling, clients don't depend on code that they don't use, when this is true you can develop them separately reducing developer collisions, increasing productivity and maintainability. Thinking from the other direction if you are implementing a FAT interface and you add a new method, then all classes that inherit from that interface will require an implementation of that method, even if it does not use it, violating the Open/Closed principle in the process, don't depend on what you don't need. It is worth noting that you want SLIM interfaces but that does not mean that FAT implementations are also to be avoided. Classes that implement many of the SLIM interfaces will become FAT but make sure they don't become OBESE.
D is for Dependency Inversion
In our final week, we arrived at my favourite of all the principles "Dependency Inversion". The premise here is quite easy to state, but not as easy to understand, but it boils down to this; when calling out to a dependency, the caller does not know what concrete implementation is actually being called at compile time, this is only resolved at run time. The off shot of this is you create logic that does not know or care about how something is achieved, it assumes that the concrete implementation conforms to a contract and therefore this lodgic can be reused with any number of concrete implementations. Imagine you are creating a catalogue import, it takes data from one system and loads it into another. Adhering to the Dependency Inversion principle you are able to create the core workflow create/update/delete in such a way that it does not know or care what the source or target systems are, these systems are just implementation details and the decision of what technology that is to be used can be deferred. Another benefit this principle provides us is enhancing the ability to test code in isolation because we are injecting contracts to perform other responsibilities. When creating tests it's trivial to inject dummies, fakes, stubs, mocks, or spies of these contracts.
Post Image licensed under Attribution-ShareAlike 2.0 Generic
- Expanded on some of the ideas in the Interface Segregation and Dependency Inversion descriptions. Shoutout to Seb for these suggestions.
Uncle Bob sometime in the past ↩︎
An interface is considered FAT when its consuming class does not use the majority of the members declared. ↩︎