focusing only on SOLID principles with Node.js
Definition: A class (or module) should have only one reason to change, meaning it should have only one job.
// BAD: Handles both order processing and email notifications
class OrderService {
processOrder(order) {
console.log("Processing order: ", order);
this.sendEmail(order);
}
sendEmail(order) {
console.log(`Email sent for order: ${order.id}`);
}
}// GOOD: Separating concerns
class OrderService {
constructor(emailService) {
this.emailService = emailService;
}
processOrder(order) {
console.log("Processing order: ", order);
this.emailService.sendEmail(order);
}
}
class EmailService {
sendEmail(order) {
console.log(`Email sent for order: ${order.id}`);
}
}
const emailService = new EmailService();
const orderService = new OrderService(emailService);
orderService.processOrder({ id: 123 });- What is SRP and why is it important in software design?
- How does SRP help in writing testable code?
- Can you give an example where violating SRP caused problems?
Definition: Software entities (classes, modules, functions) should be open for extension but closed for modification.
// BAD: Adding new payment methods requires modifying existing code
class PaymentService {
pay(order, type) {
if (type === "CreditCard") {
console.log("Processing Credit Card payment");
} else if (type === "PayPal") {
console.log("Processing PayPal payment");
}
}
}// GOOD: Using abstraction to extend functionality
class PaymentProcessor {
processPayment(order) {
throw new Error("Method not implemented");
}
}
class CreditCardPayment extends PaymentProcessor {
processPayment(order) {
console.log("Processing Credit Card payment");
}
}
class PayPalPayment extends PaymentProcessor {
processPayment(order) {
console.log("Processing PayPal payment");
}
}
const paymentMethods = [new CreditCardPayment(), new PayPalPayment()];
paymentMethods.forEach(method => method.processPayment({ id: 123 }));- How does OCP improve maintainability?
- What are some real-world examples of OCP?
- How do interfaces help in following OCP?
Definition: Objects of a superclass should be replaceable with objects of a subclass without breaking the application.
// BAD: Violates LSP because JSONStorage changes the return type
class Database {
save(data) {
console.log("Saving to DB", data);
}
}
class JSONStorage extends Database {
save(data) {
return JSON.stringify(data); // Violates LSP
}
}// GOOD: Maintains expected behavior
class Database {
save(data) {
console.log("Saving to DB", data);
}
}
class JSONStorage extends Database {
save(data) {
super.save(JSON.stringify(data));
}
}- What is LSP and why is it important?
- How do you ensure LSP in a system with multiple subclasses?
- What issues might arise from violating LSP?
Definition: Clients should not be forced to depend on interfaces they do not use.
// BAD: Single interface forces unused methods
class Worker {
work() {
console.log("Working...");
}
eat() {
console.log("Eating...");
}
}
class Robot extends Worker {
eat() {
throw new Error("Robots don’t eat"); // Violates ISP
}
}// GOOD: Separate interfaces
class Workable {
work() {}
}
class Eatable {
eat() {}
}
class Human extends Workable {
eat() {
console.log("Eating...");
}
work() {
console.log("Working...");
}
}
class Robot extends Workable {
work() {
console.log("Working without eating");
}
}- How does ISP improve modularity?
- Can you give an example where violating ISP caused issues?
- How do you refactor a large interface following ISP?
Definition: High-level modules should not depend on low-level modules; both should depend on abstractions.
// BAD: High-level module directly depends on low-level module
class MySQLDatabase {
connect() {
console.log("Connected to MySQL");
}
}
class UserService {
constructor() {
this.database = new MySQLDatabase(); // Violates DIP
}
getUser() {
this.database.connect();
}
}// GOOD: Using dependency injection and abstraction
class Database {
connect() {}
}
class MySQLDatabase extends Database {
connect() {
console.log("Connected to MySQL");
}
}
class UserService {
constructor(database) {
this.database = database;
}
getUser() {
this.database.connect();
}
}
const database = new MySQLDatabase();
const userService = new UserService(database);
userService.getUser();- How does DIP help in decoupling modules?
- What are the benefits of using dependency injection?
- How would you refactor an application that violates DIP?
Understanding and applying SOLID principles in Node.js applications ensures better maintainability, testability, and scalability. These principles help in writing cleaner, modular, and robust code for real-world applications.
Need help implementing SOLID in your project? 🚀