DEV Community

Cover image for SOLID principles tutorial with NodeJS
Ryosuke Yano
Ryosuke Yano

Posted on

SOLID principles tutorial with NodeJS

Introduction

In software development, writing maintainable, scalable, and robust code is crucial for long-term success. One set of principles that helps achieve these goals is SOLID. This is an acronym for five object-oriented design principles that guide developers in writing high-quality code. In this article, we will explore each SOLID principle and illustrate their implementation with a practical example of a Book Management App in NodeJS using TypeScript.

*You can just read and learn this principle from this article, but if you want to practice the tutorial, please set up your development environment following the directions on this repository SOLID Principles Tutorial. (You can set up less than 5 min!)

1. Single Responsibility Principle (SRP)

This principle states that a class should have only one reason to change, meaning it should have a single responsibility. In our Book Management App, we will follow SRP by creating separate classes for different responsibilities.

Implementation:

  • "Book": Represents a book entity and holds book-related properties.
// book.ts

class Book {
  name: string;
  authorName: string;
  year: number;
  price: number;

  constructor(name: string, authorName: string, year: number, price: number, isbn: string) {
    this.name = name;
    this.authorName = authorName;
    this.year = year;
    this.price = price;
  }
}

export default Book;
Enter fullscreen mode Exit fullscreen mode
  • "BookRepository": Manages the collection of books and performs CRUD operations on the book list.
// bookRepository.ts

import Book from './book';

class BookRepository {
  private books: Book[];

  constructor() {
    this.books = [];
  }

  addBook(book: Book) {
    this.books.push(book);
  }

  getAllBooks(): Book[] {
    return this.books;
  }
}

export default BookRepository;
Enter fullscreen mode Exit fullscreen mode

The "Book" class takes care of book-related properties, such as "name", "authorName", "year", "price", and "isbn"
. Meanwhile, the "BookRepository" class handles adding books to the collection and retrieving all books. Each class has a clear and distinct responsibility, adhering to the Single Responsibility Principles.

2. Open-Closed Principles

This principle states that classes should be open for extension but closed for modification. In our Book Management App, we will use inheritance to achieve this principle.

Implementation:
We have two classes that inherit from the "Book" class:

  • "FictionBook": Represents a specific type of book (non-fiction) and extends the "Book" class.
// fictionBook.ts

import Book from './book';

class FictionBook extends Book {}

export default FictionBook;
Enter fullscreen mode Exit fullscreen mode
  • "NonFictionBook": Represents another type of book (non-fiction) and extends the "Book" class.
// nonFictionBook.ts

import Book from './book';

class NonFictionBook extends Book {}

export default NonFictionBook;
Enter fullscreen mode Exit fullscreen mode

With the Open-Closed Principle, we can add new types of books (e.g., FantasyBook, BiographyBook) without modifying the existing code. This promotes code flexibility and reusability.

3. Liskov Substitution Principle (LSP):

This principle states that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program. In our Book Management App, we will ensure that subclasses behave as expected when used in place of the parent class.

Implementation:
Both "FictionBook" and "NonFictionBook" classes are subclasses of the "Book" class. This allows us to treat any book type as a regular book ("Book") without breaking the application's functionality. For example, we can add a "getType()" method to each subclass to return its specific type.

// fictionBook.ts

import Book from './book';

class FictionBook extends Book {
  getType(): string {
    return "Fiction Book";
  }
}

export default FictionBook;
Enter fullscreen mode Exit fullscreen mode
// nonFictionBook.ts

import Book from './book';

class NonFictionBook extends Book {
  getType(): string {
    return "Non-Fiction Book";
  }
}

export default NonFictionBook;
Enter fullscreen mode Exit fullscreen mode

4. Interface Segregation Principle (ISP)

This principle states that a class should not be forced to implement an interface it does not use. In our Book Management App, we will create separate interfaces to ensure that each class implements only the relevant methods.

Implementation:
We have an interface named "BookRepositoryInterface" with two methods:
"addBook(book: Book)" and "getAllbooks(): Book[]". By having a specific interface for "BookRepository", we prevent unrelated classes from implementing unnecessary methods.

// bookRepositoryInterface.ts

import Book from './book';

interface BookRepositoryInterface {
  addBook(book: Book): void;
  getAllBooks(): Book[];
}

export default BookRepositoryInterface;
Enter fullscreen mode Exit fullscreen mode

5. Dependency Inversion Principle (DIP):

This principle states that high-level modules should not depend on low-level modules. Instead, both should depend on abstractions. In our Book Management App, we will follow DIP by using dependency injection.

Implementation:
We have a "BookRepository" class that implements the "BookRepositoryInterface" interface. The "app.ts" module injects the "BookRepository" instance into the "bookRepository" variable. This approach allows for loose coupling between modules, making it easy to swap implementation without changing the app's core logic.

// app.ts

import Book from './book';
import FictionBook from './fictionBook';
import NonFictionBook from './nonFictionBook';
import BookRepository from './bookRepository';

const bookRepository = new BookRepository();

const book1 = new Book("The Great Gatsby", "F. Scott Fitzgerald", 1925, 15);
const fictionBook = new FictionBook("Harry Potter", "J.K. Rowling", 1997, 20);
const nonFictionBook = new NonFictionBook("Ikigai: The Japanese Secret to a Long and Happy Life",
  "Héctor García and Francesc Miralles",
  2016,
  30);

bookRepository.addBook(book1);
bookRepository.addBook(fictionBook);
bookRepository.addBook(nonFictionBook);

const allBooks = bookRepository.getAllBooks();
console.log(allBooks);
Enter fullscreen mode Exit fullscreen mode

By applying SOLID principles in our Book Management App using NodeJS and TypeScript, we have achieved clean, maintainable, and flexible code. This Single Responsibility Principle ensures each class has a single responsibility, while the Open-Closed Principle allows for easy extension without modifying existing code. The Liskov Substitution Principle creates clear and concise interfaces. Finally, the Dependency Inversion Principle promotes decoupling and flexibility through dependency injection.

Incorporating SOLID principles into your projects can lead to more robust and scalable applications that are easier to maintain and extend. I hope that this tutorial helps you become a more proficient and effective developer.

Top comments (4)

Collapse
 
roniquericketts profile image
Ronique Ricketts

Firstly, thank you for this article. I shall practice these so I can land a job. I don’t see where you used the Interface you created. 🤔

Collapse
 
ryosuke profile image
Ryosuke Yano

Oh, I created it but haven't used it... Thank you for letting me know

Collapse
 
iulia_mariamoldovan_13ed profile image
Iulia Maria Moldovan • Edited

I have a question: If the Book class does not have getType(), how can it not break the Liskov substitution principle? I believe the example is not complete. Subclasses should maintain the behavior expected by the superclass. If a superclass method expects certain preconditions, the subclass should not strengthen them. Similarly, if a method guarantees certain postconditions, the subclass should not weaken them.
Please check this example and update the Book example if you consider this useful (I use Python for simplicity):

class Bird:
    def fly(self):
        raise NotImplementedError("This bird can't fly")

class FlyingBird(Bird):
    def fly(self):
        return "Flying"

class Sparrow(FlyingBird):
    def fly(self):
        return "Sparrow flying"

class Ostrich(Bird):
    pass

def let_bird_fly(bird: Bird):
    try:
        return bird.fly()
    except NotImplementedError:
        return "This bird can't fly"

# Usage
sparrow = Sparrow()
print(let_bird_fly(sparrow))  # Output: Sparrow flying

ostrich = Ostrich()
print(let_bird_fly(ostrich))  # Output: This bird can't fly
Enter fullscreen mode Exit fullscreen mode
Collapse
 
prabu_venkatesan_e8f7ad9e profile image
Prabu Venkatesan

Thanks for posting the Tutorial.