SOLID Principles
Last updated
Last updated
Introduced by Robert C Martin in his 2000 paper and formed by Michael Feathers, these principle changed the way software is written.
These principle help us to write better code, code which are maintainable, understandable and flexible.
The following five concepts make up SOLID principles:
Single Responsibility Principle (SRP)
Open/Closed Principle (OCP)
Liskov Substitution Principle (LSP)
Interface Segregation Principle (ISP)
Dependency Inversion Principle (DIP)
Lets dive in onto each of them,
Single Responsibility Principle
"There should never be more than one reason for a class to change." In other words, every class should have only one responsibility.
We don’t want objects that know too much and have unrelated behavior. These classes are harder to maintain.
For example, if we have a class that we change a lot, and for different reasons, then this class should be broken down into more classes, each handling a single concern. Surely, if an error occurs, it will be easier to find.
When writing a class according to the SRP principle, we have to think about the problem domain, business needs, and application architecture. It is very subjective, which makes implementing this principle harder then it seems.
How does this principle help us write better software? A few benefits are as follows,
Testing: Will have fewer test cases.
Lower coupling: Will have fewer dependencies.
Organization: Smaller, well-organized classes are easier to search than monolithic ones.
Classes will adhere to one functionality. Their methods and data will be concerned with one clear purpose. This means high cohesion, as well as robustness, which together reduce errors.
One of the tools that can help achieve high cohesion in methods is LCOM. Essentially, LCOM measures the connection between class components and their relation to one another.
A detailed explanation can be found here.
Open Closed Principle
Software entities should be open for extensions, closed for modifications.
Programs that conform to the open-closed principle are changed by adding new code, rather than changing existing code. They dont experience cascade of changes exhibited by non-conforming programs.
Exception to this principle is when fixing bugs in existing code.
Due to this principle, loose coupling is achieved which leads to easy maintenance of applications.
More about it can be found here.
Liskov Substitution Principle
If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
Simple put by Uncle Bob,
Subtypes must be substitutable for their base types.
A subtype doesn't automatically become substitutable for its supertype. To be substitutable, the subtype must behave like its supertype.
An object's behavior is the contract that its clients can rely on.
Subtyping in Java requires the base class's properties and methods are available in the subclass.
However, behavioral subtyping means that not only does a subtype provide all of the methods in the supertype, but it must adhere to the behavioral specification of the supertype.
This ensures that any assumptions made by the clients about the supertype behavior are met by the subtype.
This is the additional constraint that the Liskov Substitution Principle brings to object-oriented design.
The following rules help us to create well-behaved subtypes.
Signature Rule -> Methods Argument types
The overridden subtype method argument types can be identical or wider than the supertype method argument types.
Signature Rule -> Return types
The return type of the overridden subtype method can be narrower than the return type of the supertype method.
This is called covariance of return types. Covariance indicates when a subtype is accepted in place of a supertype.
Signature Rule -> Exceptions
The subtype method can throw fewer or narrower (but not any additional or broader) exceptions than the supertype method.
Properties Rule -> Class Invariants
A class invariant is an assertion concerning object properties that must be true for all valid states of the object.
All subtype methods (inherited and new) must maintain or strengthen the supertype's class invariants.
Properties Rule -> History Constraints
All subclass methods (inherited or new) shouldn't allow state changes that the base class didn't allow.
Methods Rule -> Preconditions
A precondition should be satisfied before a method can be executed.
A subtype can weaken (but not strengthen) the precondition for a method it overrides.
Methods Rule -> Postconditions
A postcondition is a condition that should be met after a method is executed.
The subtype can strengthen (but not weaken) the postcondition for a method it overrides.
The Liskov Substitution Principle helps us model good inheritance hierarchies. It helps us prevent model hierarchies that don't conform to the Open/Closed principle.
Any inheritance model that adheres to the Liskov Substitution Principle will implicitly follow the Open/Closed principle.
More about it can be read here.
Interface Segregation Principle
Interface Segregation simply means that we should break larger interfaces into smaller one. Thus ensuring that implementing classes need not implement unwanted methods.
This principle was first defined by Robert C. Martin as,
“Clients should not be forced to depend upon interfaces that they do not use“.
The goal of this principle is to reduce the side effects of using larger interfaces by breaking application interfaces into smaller ones.
Precise application design and correct abstraction is the key behind the Interface Segregation Principle.
In case we’re dealing with polluted legacy interfaces that we cannot modify, the adapter design pattern can come in handy.
Adhering to this principle helps to avoid bloated interfaces with multiple responsibilities.
This eventually helps us to follow the Single Responsibility Principle as well.
More about this principle can be read here
Dependency Inversion Principle
"Depend upon abstractions, [not] concretions."
As per Robert Martin (Uncle Bob),
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
So, it's clear that at the core, the DIP is about inverting the classic dependency between high-level and low-level. components by abstracting away the interaction between them.
In traditional software development, high-level components depend on low-level ones. Thus, it's hard to reuse the high-level components.
Note that DIP is neither Dependency Injection nor Inversion of Control.
Inversion of Control (IoC)
IoC is a pattern in which the control of the flow of an application is reversed.
With traditional programming methodologies, our custom code has the control of the flow of an application. Conversely, with IoC, the control is transferred to an external framework or container (eg: Spring Framework).
The framework is an extendable codebase, which defines hook points for plugging in our own code.
In turn, the framework calls back our code through one or more specialized subclasses, using interfaces' implementations, and via annotations.
A good read about this can be found here
Dependency Injection (DI)
DI is about making software components to explicitly declare their dependencies or collaborators through their APIs, instead of acquiring them by themselves.
Without DI, software components are tightly coupled to each other. Hence, they're hard to reuse, replace, mock and test, which results in rigid designs.
With DI, the responsibility of providing the component dependencies and wiring object graphs is transferred from the components to the underlying injection framework.
Dependency Inversion, Inversion of Control and Dependency Injection all work well together and can be understood as the following quote from Martin Fowler's article,
DI is about wiring, IoC is about direction, and DIP is about shape [of the object upon which the code depends].
Refer this article which talks about how DI and IoC work together.
Interestingly, this principle is only achievable provided that OCP and LSP are followed.
Read more about this principle here.
If you still wonder, if SOLID is still relevant, read this.