What is SOLID?
SOLID is an acronym for five software design principles introduced by Robert C. Martin in the 2000s, that aim to help us structure our code in order to:
- Tolerate change.
- Ease code understanding.
- Write components that can be used in many software systems.
SOLID stands for:
- S: Single Responsibility Principle
- O: Open-Closed Principle
- L: Liskov Substitution Principle
- I: Interface Segregation Principle
- D: Dependency Inversion Principle
S - Single Responsibility
A Class should have one, and only one reason to change.
If our classes assume multiple responsibilities, they will be highly coupled thus making them more difficult to maintain.
What’s a reason to change?
Uncle Bob states that this principle is about people. This means that when you write a software module, and changes are requested on that module, those changes can only originate from a single person or a tightly group of people representing a single narrowly defined business function.
Another definition for this principle is:
Gather together those things that change for the same reason, and separate those things that change for different reasons.
This can also be considered the definition of Separation of Concerns.
The following piece of code shows a violation of the SRP in which the book class is both a representation of an entity and also implements the persistence of such an entity.
(S) - Wrong Approach
class Book {
constructor(private _author: string, private _title: "string) {}"
get author(): string {
return this._author;
}
get title(): string {
return this._title;
}
save(): void {
// Save book in the database.
}
}
(S) - Better Approach
class Book {
constructor(private _author: string, private _title: "string) {}"
get author(): string {
return this._author;
}
get title(): string {
return this._title;
}
}
interface RepositoryInterface<T> {
save(entity: T): void;
}
class BookRepository<T extends Book> implements RepositoryInterface<T> {
save(book: Book): void {
// Save book in the database
}
}
O - Open for Extension - Closed for Modification
Software entities should be open for extension but closed for modification.
This principle states that software entities must be extensible without modifying the existing code. In order to achieve this, we need to make abstractions. By doing this, we’ll be able to extend the behavior of a class without changing a single line of code.
(O) - Wrong Approach
class Rectangle {
constructor(private _width: number, private _height: number) {}
get height(): number {
return this._height;
}
get width(): number {
return this._width;
}
}
class Square {
constructor(private _height: number) {}
get height(): number {
return this._height;
}
}
class AreaCalculator {
private shapes: any[];
constructor(shapes: any[]) {
this.shapes = shapes;
}
public sum() {
return this.shapes.reduce((acc, shape) => {
if (shape instanceof Square) {
acc += Math.pow(shape.height, 2);
}
if (shape instanceof Rectangle) {
acc += shape.height * shape.width;
}
return acc;
}, 0);
}
}
(O) - Better Approach
interface Shape {
area() : number;
}
class Rectangle implements Shape {
constructor(private _width: number, private _height: number) {}
public area() : number {
return this._height * this._width;
}
}
export class Square implements Shape {
constructor(private _height: number) {}
public area() : number {
return Math.pow(this._height, 2);
}
}
class AreaCalculator {
private shapes: Shape[];
constructor(shapes: Shape[]) {
this.shapes = shapes;
}
public sum() : number {
return this.shapes
.reduce((acc, shape) => acc += shape.area(), 0);
}
}
L - Liskov Substitution Principle
Inherit from a Parent Class but without modifying the parent’s method behavior.
This principle states that objects must be replaceable by instances of their subtypes without altering the correct functioning of the system.
A classic example of a violation of this principle is the Rectangle-Square problem. The Square class extends the Rectangle class and assumes that the width and height are equal. When calculating the area of a square, we’d get the wrong value.
(L) - Wrong Approach
class Rectangle {
constructor(private _width: number, private _height: number) {}
public area() : number {
return this._height * this._width;
}
}
class Square extends Rectangle {}
(L) - Better Approach
interface Shape {
area() : number;
}
class Rectangle implements Shape {
constructor(private _width: number, private _height: number) {}
public area() : number {
return this._height * this._width;
}
}
class Square implements Shape {
constructor(private _height: number) {}
public area() : number {
return Math.pow(this._height, 2);
}
}
I - Interface Segregation
Many client-specific interfaces are better than one general-purpose interface.
This principle states that classes should never implement interfaces that they don’t need to use. If they do, we’ll end up having not implemented methods in our classes. This can be solved by creating specific interfaces instead of general-purpose interfaces.
(I) - Wrong Approach
interface VehicleInterface {
drive(): string;
fly(): string;
}
class FutureCar implements VehicleInterface {
public drive() : string {
return 'Driving Car.';
}
public fly() : string {
return 'Flying Car.';
}
}
class Car implements VehicleInterface {
public drive() : string {
return 'Driving Car.';
}
public fly() : string {
throw new Error('Not implemented method.');
}
}
class Airplane implements VehicleInterface {
public drive() : string {
throw new Error('Not implemented method.');
}
public fly() : string {
return 'Flying Airplane.';
}
}
(I) - Better Approach
interface CarInterface {
drive() : string;
}
interface AirplaneInterface {
fly() : string;
}
class FutureCar implements CarInterface, AirplaneInterface {
public drive() {
return 'Driving Car.';
}
public fly() {
return 'Flying Car.'
}
}
class Car implements CarInterface {
public drive() {
return 'Driving Car.';
}
}
class Airplane implements AirplaneInterface {
public fly() {
return 'Flying Airplane.';
}
}
D - Dependency Inversion
Entities must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions.
This principle states that a class should not depend on another class, but instead on an abstraction of that class. It allows loose coupling and more reusability.
(D) - Wrong Approach
class MemoryStorage {
private storage: any[];
constructor() {
this.storage = [];
}
public insert(record: any): void {
this.storage.push(record);
}
}
class PostService {
private db = new MemoryStorage();
createPost(title: string) {
this.db.insert(title);
}
}
(D) - Better Approach
interface DatabaseStorage {
insert(record: any): void;
}
class MemoryStorage implements DatabaseStorage {
private storage: any[];
constructor() {
this.storage = [];
}
public insert(record: any): void {
this.storage.push(record);
}
}
class PostService {
private db: DatabaseStorage;
constructor(db: DatabaseStorage) {
this.db = db;
}
createPost(title: string) {
this.db.insert(title);
}
}
Top comments (2)
i've read a lot of solid explainers in my life, and this one was really well done. the only thing i would recommend is adding syntax colouring by putting the name of the language after the three backticks; a small thing, but nice.
Thank you for your comments, I will apply those changes!