Writing high quality software is a hard task, period. Here’s how five simple principles can improve your code quality drastically.
As the project deadline draws near, the temptation of violating well-known principles and self-created rules arise. It’s even worse if you’re not even aware of these violations and you’re happily creating your classes with confidence, not noticing the code smell generated.
As we all know by experience however, we’re paying a huge price for these violations on the long run as the maintainability of our product decreases heavily every time we integrate a suboptimal design or solution. If only there was a set of rules that’s easy to remember and if kept in mind, we can be assured we’re on the right way to writing high quality software…
Luckily, there is!
SOLID is an acronym of best-practice object-oriented design principles that you can apply to your design to accomplish various desirable goals like loose-coupling, higher maintainability, intuitive location of interesting code, and so on. Every letter stands for a principle — let’s get to know them!
Single responsibility principle
Single responsibility principle states that there should never be more than one reason for a class to change, meaning a class should concentrate on doing exactly one thing and have exactly one responsibility. This doesn’t mean it should have only one method, but its methods should relate to a single purpose and therefore should be cohesive.
For example, an Invoice class might have the responsibility of calculating various amounts based on it’s data. In that case it probably shouldn’t know about how to retrieve this data from a database, or how to format an invoice for print or display.
Violations of the SRP are pretty easy to notice: the class seems to be doing too much, is too big and too complicated. The easiest way to fix this is to split the class. If you can think of at least two different change requests that require a class to be changed, you class violates this rule.
Open-closed principle states that software entities should be open for extension and closed for modification. The original idea behind this principle is that we can use OO techniques like inheritance and composition to change (or extend) the behaviour of a class, instead of modifying the class itself. Following the OCP should make behaviour easier to change, and also help us avoid breaking existing behaviour while making changes. The OCP also gets us to think about the likely areas of change in a class, which helps us choose the right abstractions required for our design. The OCP does not say we cannot modify a class directly — it only says we shouldn’t be forced to do that often.
Say we have an OrderValidation class with one big Validate(Order order) method that contains all rules required to validate an order. If the rules change, we need to change our OrderValidation class, so we are violating the OCP. If the OrderValidation contained a collection of ValidationRule interface objects that contained the rules, then we could write Validate(Order order) to iterate through those rules to validate the order. Now if the rules change then we can just create a new object implementing the ValidationRule interface and add it to an OrderValidation instance at run time (rather than to the class definition itself).
If you find yourself in the need of modifying a similar area of code all the time (for example, validation rules) then it’s probably time to apply the OCP and abstract away the changing part of the code. Another sign of a potential OCP violation is switching on a type — if another type is created then we’ll have to alter the switch statement.
Liskov substitution principle
Liskov substitution principle states that function classes that use references to base classes must be able to use derived classes without knowing it.
Subclasses should behave nicely when used in place of their parent class. The LSP sounds deceptively straightforward — we should be able to substitute an instance of a subclass for its parent class and everything should continue to work. This might sound easy, but actually it’s not — which is probably why we are often advised to favour composition over inheritance. Ensuring a subclass works in any situation the parent does is really hard work, and whenever you use inheritance its a good idea to keep the LSP firmly in mind.
An example of an LSP violation is the Square IS-A Rectangle relationship (Square being inherited from Rectangle). Mathematically a square is a special case of a rectangle with all sides of equal length, but this breaks the LSP when modelled in code. What should setWidth(int width) do when called on a Square? Should it set the height as well? If you have code that expects one behaviour but gets another depending on which subtype it has, you can wind up with some very hard-to-find bugs.
LSP violations are very easy to miss until you actually hit the condition where your inheritance hierarchy breaks down. The best way to reduce violations is to keep very aware of the LSP whenever using inheritance, and always considering favouring composition over inheritance.
Interface segregation principle
Interface segregation principle states that clients should not be forced to depend upon interfaces that they do not use.
Keep interfaces small and cohesive. The ISP is all about keeping interfaces (both interface, and abstract classes!) small and limited only to a very specific need (single responsibility).
If you find yourself leaving interface methods empty on purpose too often, you’re violating ISP — your interface is not specialised enough. The way to fix violations like this is to break down interfaces along the lines of responsibilities and apply the SRP.
Dependency inversion principle
Dependency inversion principle states that high-level modules should not depend upon low-level modules, instead both should depend upon abstractions. Abstractions should not depend upon details — details should depend upon abstractions.
As this might seem very hard to understand, here’s a much simpler definition: “Use lots of interfaces and abstractions”. The DIP says that if a class has dependencies on other classes, it should rely on the dependencies’ interfaces rather than their concrete types. The idea is that we isolate our class behind a boundary formed by the abstractions it depends upon. If all the details behind those abstractions change then our class is still safe. This helps keep coupling low and makes our design easier to change as good interfaces stop chains of dependencies. Where the DIP starts to become really useful and a bit more profound is in a related concept, Dependency Injection.
A UserDataDisplay class that displays the user’s name and picture should use a DataProvider interface and only depend on its provided methods. A concrete UserDataProvider can implement this interface and dig the database for the required data when required. The client class only cares that the UserDataProvider meets the DataProvider contract and therefore is guaranteed to be able to provide required data. Better yet, our displaying class isn’t tied to the data provider in any ways. It can use NetworkDataProvider, XMLDataProvider or MockDataProvider without any difficulties.
Antoine de Saint-Exupery said
“Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away”
I couldn’t agree more and this is especially true to the SOLID principles — trivial they might seem, once kept they improve code quality drastically. Keep them in mind!
If you liked our post, we’d greatly appreciate it if you recommended us on Medium or shared our article on social media. You can also visit us at http://www.innoid.hu