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;
- "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;
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;
- "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;
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;
// nonFictionBook.ts
import Book from './book';
class NonFictionBook extends Book {
getType(): string {
return "Non-Fiction Book";
}
}
export default NonFictionBook;
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;
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);
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)
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. 🤔
Oh, I created it but haven't used it... Thank you for letting me know
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):
Thanks for posting the Tutorial.