JavaScript
January 27, 2025
Sebastian
As a JavaScript developer, applying the SOLID principles can help ensure a robust, maintainable, and scalable codebase. This article explores what SOLID is and how to apply it to your code.
SOLID is an acronym that stands for five design principles aimed at promoting simpler, more robust, and updatable code for software development in object-oriented languages. The principles were first introduced by Robert C. Martin (also known as „Uncle Bob”) and are widely adopted in the software development industry.
The Single Responsibility Principle states that a class should have only one reason to change. In other words, a class should have a single responsibility or purpose. This principle is essential in JavaScript, where functions and objects are often used to perform multiple tasks.
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
save() {
// Save user data to database
}
sendWelcomeEmail() {
// Send welcome email to user
}
}
In the example above, the User
class has two responsibilities: saving user data to the database and sending welcome emails. This can lead to tight coupling and make the class harder to maintain.
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
save() {
// Save user data to database
}
}
class EmailService {
sendWelcomeEmail(user) {
// Send welcome email to user
}
}
By separating the concerns of the User
class, we’ve made it easier to maintain and test.
The Open/Closed Principle states that a class should be open for extension but closed for modification. This means that you should be able to add new functionality to a class without modifying its existing code.
class PaymentGateway {
processPayment(paymentMethod) {
if (paymentMethod = 'paypal') {
// Process PayPal payment
} else if (paymentMethod = 'stripe') {
// Process Stripe payment
}
}
}
In the example above, the PaymentGateway
class is not open for extension because adding a new payment method requires modifying the existing code.
class PaymentGateway {
processPayment(paymentMethod) {
paymentMethod.process();
}
}
class PayPalPaymentMethod {
process() {
// Process PayPal payment
}
}
class StripePaymentMethod {
process() {
// Process Stripe payment
}
}
By using polymorphism, we’ve made it possible to add new payment methods without modifying the existing code.
The Liskov Substitution Principle states that subtypes should be substitutable for their base types. This means that any code that uses a base type should be able to work with a subtype without knowing the difference.
class Bird {
fly() {
// Fly
}
}
class Penguin extends Bird {
fly() {
throw new Error('Penguins cannot fly');
}
}
In the example above, the Penguin
class is not substitutable for the Bird
class because it throws an error when trying to fly.
class Bird {
fly() {
// Fly
}
}
class FlightlessBird {
tryToFly() {
// Try to fly, but fail
}
}
class Penguin extends FlightlessBird {
tryToFly() {
// Try to fly, but fail
}
}
By creating a separate hierarchy for flightless birds, we’ve made it possible to substitute a Penguin
for a FlightlessBird
without violating the LSP.
The Interface Segregation Principle states that a client should not be forced to depend on interfaces it does not use. This means that interfaces should be divided into smaller, more focused interfaces that meet the needs of specific clients.
interface Printable {
print();
fax();
scan();
}
class Document implements Printable {
print() {
// Print document
}
fax() {
throw new Error('Documents cannot fax');
}
scan() {
throw new Error('Documents cannot scan');
}
}
In the example above, the Document
class is forced to depend on the fax
and scan
methods of the Printable
interface, even though it does not use them.
interface Printable {
print();
}
interface Faxable {
fax();
}
interface Scannable {
scan();
}
class Document implements Printable {
print() {
// Print document
}
}
class FaxMachine implements Faxable {
fax() {
// Fax document
}
}
class Scanner implements Scannable {
scan() {
// Scan document
}
}
By dividing the Printable
interface into smaller, more focused interfaces, we’ve made it possible for clients to depend only on the interfaces they need.
The Dependency Inversion Principle states that high-level modules should not depend on low-level modules, but both should depend on abstractions. This means that high-level modules should not be tightly coupled to low-level modules, but instead should depend on interfaces or abstract classes.
class PaymentProcessor {
constructor(paymentGateway) {
this.paymentGateway = paymentGateway;
}
processPayment() {
this.paymentGateway.chargeCard();
}
}
class PayPalPaymentGateway {
chargeCard() {
// Charge card using PayPal
}
}
In the example above, the PaymentProcessor
class is tightly coupled to the PayPalPaymentGateway
class.
interface PaymentGateway {
chargeCard();
}
class PaymentProcessor {
constructor(paymentGateway: PaymentGateway) {
this.paymentGateway = paymentGateway;
}
processPayment() {
this.paymentGateway.chargeCard();
}
}
class PayPalPaymentGateway implements PaymentGateway {
chargeCard() {
// Charge card using PayPal
}
}
By depending on the PaymentGateway
interface instead of the PayPalPaymentGateway
class, we’ve made it possible to swap out the payment gateway without modifying the PaymentProcessor
class.
In conclusion, the SOLID principles are a set of guidelines for building robust, maintainable, and scalable software applications. By applying these principles to your JavaScript code, you can ensure that your applications are easy to modify, extend, and test.