How to Apply SOLID Principles in JavaScript: A Practical Guide from clean-code-javascript
Apply SOLID principles in JavaScript by splitting responsibilities into focused classes, depending on abstractions rather than concrete implementations, and using composition over inheritance to keep modules open for extension but closed for modification.
The SOLID principles remain foundational for maintainable software architecture, even in dynamically typed languages. The ryanmcdermott/clean-code-javascript repository dedicates a comprehensive section of its README.md to implementing these five design principles using modern JavaScript patterns. Understanding how to translate Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion into your codebase improves testability and reduces regression risks.
Single Responsibility Principle (SRP)
A class or module should have only one reason to change. In JavaScript, this means avoiding "god objects" that handle authentication, data management, and UI logic simultaneously.
According to the repository's guidance in README.md, split concerns into tiny, focused units that can be composed together:
// Bad: Class mixes authentication with settings management
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
if (this.verifyCredentials()) {
// change settings
}
}
verifyCredentials() {
// auth logic
}
}
// Good: Separated responsibilities via composition
class UserAuth {
constructor(user) {
this.user = user;
}
verifyCredentials() {
// auth logic only
}
}
class UserSettings {
constructor(user) {
this.user = user;
this.auth = new UserAuth(user);
}
changeSettings(settings) {
if (this.auth.verifyCredentials()) {
// settings logic only
}
}
}
Open/Closed Principle (OCP)
Modules should be open for extension but closed for modification. Instead of editing existing source code to add functionality, depend on abstractions that allow new behaviors to be plugged in.
The clean-code-javascript guide demonstrates this using adapter patterns in README.md:
// Bad: Conditional logic checks adapter type explicitly
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
if (this.adapter.name === 'ajaxAdapter') {
return this.adapter.request(url);
} else if (this.adapter.name === 'nodeAdapter') {
return this.adapter.request(url);
}
}
}
// Good: Each adapter implements a common contract
class AjaxAdapter {
request(url) {
// XMLHttpRequest logic
}
}
class NodeAdapter {
request(url) {
// HTTP module logic
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
return this.adapter.request(url); // No modification needed for new adapters
}
}
Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types without altering program correctness. Violations often occur with "is-a" inheritance relationships that break behavioral contracts.
As implemented in the repository examples, prefer composition when inheritance causes unexpected side effects:
// Bad: Square mutates dimensions in ways Rectangle doesn't
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;
}
}
class Square extends Rectangle {
setWidth(width) {
this.width = this.height = width;
}
setHeight(height) {
this.width = this.height = height;
}
}
// Good: Independent shapes with explicit contracts
class Shape {
setColor(color) {
// color logic
}
render(area) {
// render logic
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(length) {
super();
this.length = length;
}
getArea() {
return this.length ** 2;
}
}
Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use. In JavaScript, which lacks native interfaces, this translates to keeping configuration objects small and optional rather than "fat" parameter lists.
The DOMTraverser example from README.md illustrates lean API design:
// Bad: Forces dependency on animation module
const traverser = new DOMTraverser({
rootNode: document.body,
animationModule() {
// rarely used but required
}
});
// Good: Optional configuration keeps interfaces minimal
const traverser = new DOMTraverser({
rootNode: document.body,
options: {
animationModule() {
// only present when needed
}
}
});
Dependency Inversion Principle (DIP)
High-level modules should depend on abstractions, not concrete implementations. Use dependency injection to pass collaborators via constructors rather than importing or instantiating them internally.
The repository's InventoryTracker example in README.md shows proper inversion of control:
// Bad: High-level module creates its own low-level dependency
class InventoryTracker {
constructor(items) {
this.items = items;
this.requester = new InventoryRequester(); // concrete coupling
}
requestItems() {
this.items.forEach(item => {
this.requester.requestItem(item);
});
}
}
// Good: Dependency injected, allowing any implementation
class InventoryTracker {
constructor(items, requester) {
this.items = items;
this.requester = requester;
}
requestItems() {
this.items.forEach(item => {
this.requester.requestItem(item);
});
}
}
class InventoryRequesterV2 {
requestItem(item) {
return fetch(`https://api.example.com/items/${item}`);
}
}
// Inject the specific implementation needed
const tracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterV2());
Summary
- Single Responsibility Principle: Split classes into focused units like
UserAuthandUserSettingsrather than combining concerns. - Open/Closed Principle: Use adapter patterns where
HttpRequesterdepends on arequestabstraction rather than checkingadapter.nameconditionals. - Liskov Substitution Principle: Ensure subclasses like
SquareandRectanglemaintain behavioral contracts; prefer composition when "is-a" relationships fail. - Interface Segregation Principle: Keep JavaScript option objects optional and minimal to avoid forcing unused dependencies.
- Dependency Inversion Principle: Inject concrete implementations like
InventoryRequesterinto high-level classes via constructors rather than hard-coding them.
Frequently Asked Questions
Does JavaScript support interfaces for implementing ISP?
JavaScript does not have native interface constructs like TypeScript or Java. Instead, you apply Interface Segregation Principle by keeping function parameters and configuration objects small and optional. The clean-code-javascript guide recommends using optional options objects rather than fat parameter lists to ensure clients only depend on what they actually use.
When should I choose composition over inheritance for LSP?
Choose composition when a subclass cannot truly substitute for its parent without side effects. The repository's Square/Rectangle example demonstrates that violating geometric contracts causes bugs. When behavior varies significantly, compose objects like UserSettings containing a UserAuth instance rather than forcing an inheritance hierarchy.
How does dependency injection work without static typing?
Dependency injection in JavaScript relies on duck typing and constructor injection. As shown in the InventoryTracker example, you pass any object that implements the expected method signature (like requestItem) into the constructor. This decoupling allows swapping implementations (HTTP, WebSocket, or mock objects) without modifying the InventoryTracker class.
Are SOLID principles different in JavaScript compared to Java or C++?
The core concepts remain identical, but implementation differs due to JavaScript's dynamic nature and first-class functions. You use object literals and function composition rather than abstract classes or rigid interfaces. The clean-code-javascript repository emphasizes that JavaScript's flexibility makes SOLID even more critical to prevent "everything-in-one" modules common in loose typing environments.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →