S.O.L.I.D Principle in a nutshell

Kola Grey
4 min readDec 10, 2022
Photo by La-Rel Easter on Unsplash

SOLID is an acronym for five principles of object-oriented software design. These principles are intended to make software more maintainable and scalable. The principles are:

Single Responsibility Principle

A class should have only one responsibility or one reason to change. This means that a class should do one thing and do it well, without being concerned with other aspects of the system.

class User {
constructor(name, email) {
this.name = name;
this.email = email;
}

getName() {
return this.name;
}

getEmail() {
return this.email;
}

setEmail(email) {
this.email = email;
}
}

In the above example, the User class has a single responsibility: representing a user with a name and email address. Its methods are all related to this responsibility and do not perform any unrelated tasks.

Open/Closed Principle

Classes should be open for extension but closed for modification. This means that classes should be designed in a way that allows them to be extended to add new behavior without modifying existing code.

class Shape {
getArea() {
// method to be implemented by subclasses
}
}

class Circle extends Shape {
constructor(radius) {
this.radius = radius;
}

getArea() {
return Math.PI * this.radius * this.radius;
}
}

class Rectangle extends Shape {
constructor(width, height) {
this.width = width;
this.height = height;
}

getArea() {
return this.width * this.height;
}
}

In the above example, the Shape class is open for extension because it provides an abstract getArea() method that can be implemented by subclasses. The Circle and Rectangle classes are closed for modification because they extend the Shape class without modifying its behavior.

Liskov Substitution Principle

Subclasses should be substitutable for their base classes. This means that objects of a subclass should be able to be used in any context where objects of the base class can be used, without causing any problems.

class Vehicle {
startEngine() {
// method to be implemented by subclasses
}
}

class Car extends Vehicle {
startEngine() {
console.log('Vroom, vroom!');
}
}

class ElectricCar extends Vehicle {
startEngine() {
console.log('Silent, but deadly...');
}
}

function testVehicle(vehicle) {
vehicle.startEngine();
}

const car = new Car();
const electricCar = new ElectricCar();

testVehicle(car); // logs 'Vroom, vroom!'
testVehicle(electricCar); // logs 'Silent, but deadly...'

In the above example, the Vehicle class defines an abstract startEngine() method that is implemented by the Car and ElectricCar classes. The testVehicle() function expects a Vehicle instance and calls its startEngine() method, regardless of whether the instance is a Car or an ElectricCar.

This demonstrates the Liskov Substitution Principle because the derived classes can be used interchangeably with the base class.

Interface Segregation Principle

Clients should not be forced to depend on methods they do not use. This means that a class should not implement an interface if it does not use all of the methods defined in that interface.

class Document {
constructor(title, content) {
this.title = title;
this.content = content;
}

getTitle() {
return this.title;
}

getContent() {
return this.content;
}

setTitle(title) {
this.title = title;
}

setContent(content) {
this.content = content;
}
}

class ReadOnlyDocument extends Document {
constructor(title, content) {
super(title, content);
}

setTitle(title) {
throw new Error('Cannot modify a read-only document.');
}

setContent(content) {
throw new Error('Cannot modify a read-only document.');
}
}

const doc1 = new Document('My Document', 'Some content.');
doc1.setTitle('New Title');

const doc2 = new ReadOnlyDocument('My Read-Only Document', 'Some content.');
doc2.setTitle('New Title'); // throws an error

In the above example, the Document class defines a set of methods for managing a document's title and content. The ReadOnlyDocument class extends the Document class and overrides the setTitle() and setContent() methods to throw an error if they are called, thus implementing a read-only interface for the document.

This allows you to use the Document class when you need to modify a document, and the ReadOnlyDocument class when you only need to read its contents.

Dependency Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions. This means that classes should not depend on concrete implementations of other classes, but rather on abstractions or interfaces.

class UserRepository {
constructor() {
this.users = [];
}

getById(id) {
return this.users.find(user => user.id === id);
}

add(user) {
this.users.push(user);
}

update(id, data) {
const user = this.getById(id);
if (user) {
Object.assign(user, data);
}
}

delete(id) {
this.users = this.users.filter(user => user.id !== id);
}
}

class UserService {
constructor(userRepository) {
this.userRepository = userRepository;
}

getUser(id) {
return this.userRepository.getById(id);
}

updateUser(id, data) {
return this.userRepository.update(id, data);
}

deleteUser(id) {
return this.userRepository.delete(id);
}
}

const userRepository = new UserRepository();
const userService = new UserService(userRepository);

In the above example, the UserService class depends on the UserRepository class to manage user records. However, the UserService class does not depend on a specific implementation of the UserRepository class. Instead, it depends on an abstract interface that defines the required behavior of the repository.

This allows you to use any class that implements the required interface as the user repository, without modifying the UserService class. For example, you could create a MockUserRepository class that simulates the behavior of a user repository for testing purposes, and use it with the UserService class without making any changes to the UserService class.

Conclusion

In JavaScript, these principles can be applied in a number of ways. For example, you can use the class keyword and the extends keyword to create a base class and subclasses, and you can use interfaces (which are just objects with specific methods) to define contracts that your classes must implement. You can also use dependency injection to invert dependencies between classes, and you can use composition to provide more granular control over the behavior of your classes.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Kola Grey
Kola Grey

Written by Kola Grey

Currently working within the following ecosystem: Web. Mobile. Social Media. Exploring more ways to deliver more value. Discovery in motion...

No responses yet

Write a response