Advantages Of Using The Single Responsibility Principle
Easier Maintenance
When a class or module is only responsible for one aspect of system functionality, it's much easier to understand, maintain, and update. Changes for a specific feature or in response to a change in requirements should only affect a single class. You don't have to worry about a modification rippling out to other unrelated sections of your code.
class User {
name: string;
email: string;
constructor(name: string, email: string) {
this.name = name;
this.email = email;
}
saveUserToDB() {
// Implementation here
}
sendWelcomeEmail() {
// Implementation here
}
}
In the above example, the User class handles both database operations and email operations. If there's a change in the database structure or email system, the User class would need to be updated.
class User {
name: string;
email: string;
constructor(name: string, email: string) {
this.name = name;
this.email = email;
}
}
class UserDB {
saveUser(user: User) {
// Implementation here
}
}
class EmailService {
sendWelcomeEmail(user: User) {
// Implementation here
}
}
Now, the User class, UserDB class, and EmailService class each have a single responsibility. If there are changes to how we interact with the database or email system, we only need to update UserDB or EmailService respectively. This makes the system easier to maintain.
Time To Transition From JavaScript To TypeScript
Level Up Your TypeScript And Object Oriented Programming Skills. The only complete TypeScript course on the marketplace you building TypeScript apps like a PRO.
SEE COURSE DETAILSImproved Understandability
Code that follows SRP tends to be more readable and understandable. Each class has a single focus, and its purpose is generally clear to developers. This saves a lot of time in code comprehension, which is a big part of software development.
class User {
name: string;
email: string;
constructor(name: string, email: string) {
this.name = name;
this.email = email;
}
}
class UserDB {
saveUser(user: User) {
// Implementation here
}
}
class EmailService {
sendWelcomeEmail(user: User) {
// Implementation here
}
}
By splitting large classes into smaller ones each with a single responsibility, the purpose of each class becomes more apparent. In the previous example, it's immediately clear that User represents a user, UserDB handles database interactions for users, and EmailService handles email operations.
Easier Testing
It's generally easier to write unit tests for code that follows SRP. Each unit test should correspond to a single functionality or behaviour. If a class has a single responsibility, you can write focused tests that check whether that responsibility is being fulfilled correctly. In contrast, a class that handles multiple responsibilities can have complex interdependencies that make it harder to test.
With SRP, we can write more focused tests. Here's how you might test the UserDB and EmailService classes:
// Testing UserDB
describe("UserDB", () => {
it("should save user to the database", () => {
const userDB = new UserDB();
const user = new User("John", "john@example.com");
// Your test implementation here
});
});
// Testing EmailService
describe("EmailService", () => {
it("should send a welcome email to the user", () => {
const emailService = new EmailService();
const user = new User("John", "john@example.com");
// Your test implementation here
});
});
Reduced Coupling
The SRP helps reduce coupling between different parts of a system. If a class or module takes on too many responsibilities, changes to one area can inadvertently affect another, creating a fragile system where small changes can have large, unpredictable effects. By ensuring that each class has only one reason to change, you minimize the impact of changes and reduce the risk of creating bugs in unrelated features.
Let's consider an example using a Book class that includes methods for both data management (CRUD operations) and data presentation (generating a HTML display of book data). This design violates the Single Responsibility Principle (SRP) and can lead to high coupling.
class Book {
title: string;
author: string;
constructor(title: string, author: string) {
this.title = title;
this.author = author;
}
// Methods related to data management
createBook() {
// Implementation here
}
readBook() {
// Implementation here
}
updateBook() {
// Implementation here
}
deleteBook() {
// Implementation here
}
// Method related to data presentation
displayHTML() {
return `<h1>${this.title}</h1><p>${this.author}</p>`;
}
}
In this scenario, changes to how books are stored or retrieved from a database might affect the way books are displayed in HTML, and vice versa. This high coupling can lead to unintended consequences and bugs.
class Book {
title: string;
author: string;
constructor(title: string, author: string) {
this.title = title;
this.author = author;
}
// Methods related to data management
createBook() {
// Implementation here
}
readBook() {
// Implementation here
}
updateBook() {
// Implementation here
}
deleteBook() {
// Implementation here
}
}
class BookPresenter {
book: Book;
constructor(book: Book) {
this.book = book;
}
displayHTML() {
return `<h1>${this.book.title}</h1><p>${this.book.author}</p>`;
}
}
In this refactored code, the Book class is only responsible for data management, while the BookPresenter class is responsible for displaying the book data in HTML. The two classes are decoupled, so changes in one won't affect the other. For example, you can change how books are displayed in HTML without worrying about how these changes might affect data management. This decoupling makes the code easier to maintain and less prone to bugs.
Increased Reusability
Classes that do one thing and do it well are more likely to be reusable in different contexts. If a class combines multiple responsibilities, it's less likely to fit neatly into a new context where only some of its functionality is required. Following SRP can help you build a library of highly reusable components, increasing your codebase's flexibility and efficiency.
In the refactored Book
example, we have two separate classes: Book
for
managing book data and BookPresenter
for presenting book data in HTML format.
Now, consider a situation where you want to present book data not just in HTML,
but also in plain text or JSON format for different purposes, like for a
plaintext email or an API response. With the SRP applied, it's easy to add new
presenter classes without modifying the Book
class or the BookPresenter
class. Here's how you could do that:
class TextBookPresenter {
book: Book;
constructor(book: Book) {
this.book = book;
}
displayText() {
return `Title: ${this.book.title}, Author: ${this.book.author}`;
}
}
class JSONBookPresenter {
book: Book;
constructor(book: Book) {
this.book = book;
}
displayJSON() {
return JSON.stringify({ title: this.book.title, author: this.book.author });
}
}
This way, Book
can be reused across different contexts (HTML display, text
display, JSON display) without any modification to its code. That's the power of
reusability that comes from adhering to the Single Responsibility Principle. By
keeping classes focused on one task, we can extend functionality without
modifying existing classes, which is in alignment with the Open/Closed
Principle, another principle of SOLID.
On the other hand, in the original Book
class before applying SRP, if you
wanted to add a feature to display books in text or JSON format, you would have
to modify the Book
class itself, potentially disrupting its existing
functionality. The original Book
class is less reusable because its
responsibilities are too diverse, making it harder to extend its functionality
without introducing new dependencies and potential bugs.
What Can You Do Next 🙏😊
If you liked the article, consider subscribing to Cloudaffle, my YouTube Channel, where I keep posting in-depth tutorials and all edutainment stuff for software developers.