SOLID principles with JavaScript
Explain solid principles using JavaScript and Typescript examples.

A lifelong learner. Love to travel. Listen to music.
Search for a command to run...
Explain solid principles using JavaScript and Typescript examples.

A lifelong learner. Love to travel. Listen to music.
No comments yet. Be the first to comment.
Best Practices for Path Structure, Versioning, and Error Handling

Understanding the Performance Implications of async and defer Attributes

Overview static: Default position. It does not allow setting properties like top, bottom, left, right, z-index relative: Acts the same as static. but allows positioning it relatively by putting properties like top, bottom, left, right, z-index absolu...

Overview When an element in the browser has a conflict of styles, the browser uses a set of rules to determine which style should be rendered. This set of rules is defined as CSS Selector Specificity. For instance, for an element, if we have the foll...

Master block, inline, flex, and grid to build better web layouts

SOLID stands for 5 of the following principles,
Single Responsibility Principle
Open-Closed Principle
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle
The Single Responsibility Principle implies,
When violating the single responsibility principle,
// Breaking SRP by combining data and display responsibilities
class BandManager {
constructor() {
this.bands = [];
}
// Data management responsibilities
addBand(bandName) {
this.bands.push(bandName);
}
removeBand(bandName) {
this.bands = this.bands.filter(band => band !== bandName);
}
// Display responsibilities
displayBands() {
console.log("Current lineup:");
this.bands.forEach(band => {
console.log(`- ${band}`);
});
}
displayBandsAsTable() {
console.log("Band Lineup Table:");
console.log("------------------");
this.bands.forEach((band, index) => {
console.log(`${index + 1}. ${band}`);
});
console.log("------------------");
}
printBandsToFile(filename) {
console.log(`Saving band list to ${filename}...`);
// Code to write to file would go here
}
}
// Usage
const concertLineup = new BandManager();
concertLineup.addBand("Metallica");
concertLineup.addBand("Iron Maiden");
concertLineup.displayBands();
concertLineup.displayBandsAsTable();
Here the BandManager has two reasons to change,
Data management
Display management
A better approach using single responsibility principles is,
// Only responsible for band data management
class BandCollection {
constructor() {
this.bands = [];
}
addBand(bandName) {
this.bands.push(bandName);
}
removeBand(bandName) {
this.bands = this.bands.filter(band => band !== bandName);
}
getAllBands() {
return [...this.bands]; // Return a copy to prevent external modification
}
}
// Only responsible for displaying band information
class BandDisplay {
displayAsList(bands) {
console.log("Current lineup:");
bands.forEach(band => {
console.log(`- ${band}`);
});
}
displayAsTable(bands) {
console.log("Band Lineup Table:");
console.log("------------------");
bands.forEach((band, index) => {
console.log(`${index + 1}. ${band}`);
});
console.log("------------------");
}
}
// Only responsible for file operations
class FileService {
saveBandsToFile(bands, filename) {
console.log(`Saving band list to ${filename}...`);
// Code to write to file would go here
}
}
// Usage
const bandCollection = new BandCollection();
const display = new BandDisplay();
const fileService = new FileService();
bandCollection.addBand("Metallica");
bandCollection.addBand("Iron Maiden");
// Get the data from one class and display it using another
const bands = bandCollection.getAllBands();
display.displayAsList(bands);
display.displayAsTable(bands);
fileService.saveBandsToFile(bands, "concert_lineup.txt");
Here we isolate the logging in a separate module. Any time our logging mechanism is being changed, we can simply update the Logger class.
Open closed principle implies that,
Consider the following code, if we update another payment, we need to add another switch case. This modifies the existing class PaymentProcessor.
class PaymentAdapter {
makePayment() {
throw new Error('makePayment() must be implemented');
}
}
class StripePayment extends PaymentAdapter {
makePayment() {
console.log('Payment made using Stripe');
}
}
class PaypalPayment extends PaymentAdapter {
makePayment() {
console.log('Payment made using PayPal');
}
}
class PaymentProcessor {
constructor(paymentAdapter) {
this.paymentAdapter = paymentAdapter;
}
pay(type) {
switch (type) {
case 'stripe':
new StripePayment().makePayment();
break;
case 'paypal':
new PaypalPayment().makePayment();
break;
default:
throw new Error('Payment type not supported');
}
}
}
new PaymentProcessor().pay('stripe');
new PaymentProcessor().pay('paypal');
Instead, we can use the following way, where any time, we need a new payment adapter, we can pass it to the PaymentProcessor, with no need to update the class.
class PaymentAdapter {
makePayment() {
throw new Error('makePayment() must be implemented');
}
}
class StripePayment extends PaymentAdapter {
makePayment() {
console.log('Payment made using Stripe');
}
}
class PaypalPayment extends PaymentAdapter {
makePayment() {
console.log('Payment made using PayPal');
}
}
class PaymentProcessor {
constructor(paymentAdapter) {
this.paymentAdapter = paymentAdapter;
}
pay() {
this.paymentAdapter.makePayment();
}
}
const stripePayment = new StripePayment();
const paypalPayment = new PaypalPayment();
new PaymentProcessor(stripePayment).pay();
new PaymentProcessor(paypalPayment).pay();
Liskov Substitution Principle implies that,
Consider the following code, which violates the Liskov Substitution Principle.
// Base class
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
// Subclass that violates LSP
class Square extends Rectangle {
constructor(size) {
super(size, size);
}
// LSP VIOLATION: Changes behavior of parent method
setWidth(width) {
this.width = width;
this.height = width; // Also changes height!
}
// LSP VIOLATION: Changes behavior of parent method
setHeight(height) {
this.width = height; // Also changes width!
this.height = height;
}
}
// Client code - will break with Square
function testRectangle(rectangle) {
rectangle.setWidth(5);
rectangle.setHeight(4);
// Expects area to be 20, but with Square it will be 16!
if (rectangle.getArea() !== 20) {
console.log("LSP violation detected!");
}
}
const rect = new Rectangle(3, 3);
testRectangle(rect); // Works as expected
const square = new Square(3);
testRectangle(square); // Fails - LSP violation!
The code violates the principles,
A square is forced to inherit from a Rectangle despite having different invariants
The Square subclass changes the behavior of the parent methods setWidth() and setHeight()
When setting width or height, Square modifies both dimensions to maintain its "squareness"
Client code expecting independent width and height modification will break with a Square
The postcondition of the Rectangle methods (that only one dimension changes) is violated by the Square
We can fix the following to align with the principles,
Created a common base class (Shape) with only truly shared behavior
Removed the inheritance relationship between Rectangle and Square
Made both Rectangle and Square implement the Shape interface independently
Gave Square its appropriate method (setSize()) that aligns with its invariants
Ensured each class has methods that match its constraints and behavior
Client code now works with each shape according to its actual capabilities
The rewritten version aligns with the Liskov Substitution Principle:
// Base shape interface (could be a class)
class Shape {
getArea() {}
}
// Specific implementations
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(size) {
super();
this.size = size;
}
setSize(size) {
this.size = size;
}
getArea() {
return this.size * this.size;
}
}
// Client code is now shape-specific
function testRectangle(rectangle) {
if (!(rectangle instanceof Rectangle)) {
throw new Error("Rectangle expected");
}
rectangle.setWidth(5);
rectangle.setHeight(4);
console.log("Area:", rectangle.getArea()); // Always 20
}
function testSquare(square) {
if (!(square instanceof Square)) {
throw new Error("Square expected");
}
square.setSize(4);
console.log("Area:", square.getArea()); // Always 16
}
const rect = new Rectangle(3, 3);
testRectangle(rect);
const square = new Square(3);
testSquare(square);
The Interface Segregation Principle implies that an interface should not have any properties that are not used or required by the class.
In the following example, we have a Shape interface that is implemented in the Square class and Cube class. We can calculate the volume for a cube, not for a square. So when we implement Shape for the Square class, it throws an error for the volume method.
interface Shape {
area: () => void;
volume: () => void;
}
class Square implements Shape {
height: number;
width: number;
constructor(height: number) {
this.height = height;
this.width = height;
}
area () {
console.log(this.height * this.width);
}
volume() {
throw new Error('Volume can not valculated on 2d shape');
}
}
class Cube implements Shape {
height: number;
width: number;
length: number;
constructor(height: number, width: number, length: number) {
this.height = height;
this.width = height;
this.length = length;
}
area () {
console.log(this.height * this.width);
}
volume () {
console.log(this.height * this.width * this.length);
}
}
const square = new Square(5);
square.area();
square.volume();
const cube = new Cube(5, 6, 7);
cube.area();
cube.volume();
To resolve the issue, we can introduce an extended interface Shape3D. This new interface will have the volume method and Will be used when creating the Cube class.
interface Shape {
area: () => void;
}
interface Shape3D extends Shape {
volume: () => void;
}
class Square implements Shape {
height: number;
width: number;
constructor(height: number) {
this.height = height;
this.width = height;
}
area () {
console.log(this.height * this.width);
}
}
class Cube implements Shape3D {
height: number;
width: number;
length: number;
constructor(height: number, width: number, length: number) {
this.height = height;
this.width = height;
this.length = length;
}
area () {
console.log(this.height * this.width);
}
volume () {
console.log(this.height * this.width * this.length);
}
}
const square = new Square(5);
square.area();
const cube = new Cube(5, 6, 7);
cube.area();
cube.volume();
The dependency Inversion Principle implies,
High-level modules should not depend on low-level modules, both should depend on abstractions
Abstractions should not depend on details, details should depend on abstractions
In the following code, we are making CourseService class according to the CourseController. Here the Courservice should not depend on CourseController, instead, both should depend on the interface.
class CourseService {
get() {
console.log('All the courses');
}
}
class CourseController {
constructor() {
this.courseService = new courseService(); // direct dependency
}
getAllCourse() {
this.courseService.get();
}
}
const courseController = new CourseController();
courseController.getAllCourse();
To resolve this, we need a service to which the CourseService can depend on an abstract.
So,
The CourseController now does not depend on the CourseService directly
Both the CourseService class (By implementing interface) and CourseController class (by setting interface as type of CourseService) depends on the Service interface
interface Service {
get: () => void;
}
// Low level module depends on the abstraction
class CourseService implements Service {
get() {
console.log('All the courses');
}
}
class CourseController {
courseService: Service; // High level module depends on the abstraction
constructor(courseService: Service) {
this.courseService = courseService; // No direct dependency
}
getAllCourse() {
this.courseService.get();
}
}
const courseService = new CourseService();
const courseController = new CourseController(courseService);
courseController.getAllCourse();