Clean Code JavaScript
repository·master·Indexed 13 days ago
https://github.com/ryanmcdermott/clean-code-javascriptA collection of JavaScript-specific clean code principles adapted from Robert C. Martin's 'Clean Code'. This guide provides guidelines for producing readable, reusable, and refactorable software in the JavaScript ecosystem, covering topics such as meaningful naming, function argument limits, avoiding side effects, and favoring functional programming.
What's inside Clean Code JavaScript
- This project provides guidelines for producing readable, reusable, and refactorable software in JavaScript. It is an adaptation of Robert C. Martin's Clean Code principles specifically for the JavaScript ecosystem. These are not strict style rules, but rather a touchstone for assessing code quality and improving software engineering practices.
Apply the Liskov Substitution Principle (LSP)
masterIf a class is a subtype of another, objects of the parent type should be replaceable with objects of the child type without altering the correctness of the program. A common mistake is using inheritance for relationships that aren't truly interchangeable (e.g., modeling a Square as a Rectangle where setting width/height independently breaks the Square's properties). In such cases, use a more general base class like
Shapeinstead.// Good: Rectangle and Square both inherit from Shape and implement getArea independently class Shape { setColor(color) { /* ... */ } render(area) { /* ... */ } } 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 * this.length; } }Ensure functions do only one thing
masterThis is a fundamental rule: functions should be isolated to a single action. When a function performs multiple tasks, it becomes harder to compose, test, and reason about. If you find a function doing multiple things, refactor it into smaller, specialized functions.
// Good: Breaking complex logic into single-purpose functions function emailActiveClients(clients) { clients.filter(isActiveClient).forEach(email); } function isActiveClient(client) { const clientRecord = database.lookup(client); return clientRecord.isActive(); }Prefer composition over inheritance
masterWhen modeling data, prefer composition (a "has-a" relationship) over inheritance (an "is-a" relationship) where possible.
When to use Inheritance:
- The relationship is strictly "is-a" (e.g.,
Humanis anAnimal). - You need to reuse code from base classes.
- You want to make global changes to all derived classes via a single base class change.
When to use Composition:
If the relationship is "has-a" (e.g., an
EmployeehasTaxData, but anEmployeeis not a type ofTaxData), use composition by storing the related object as a property within the main class.class EmployeeTaxData { constructor(ssn, salary) { this.ssn = ssn; this.salary = salary; } } class Employee { constructor(name, email) { this.name = name; this.email = email; } setTaxData(ssn, salary) { this.taxData = new EmployeeTaxData(ssn, salary); } }- The relationship is strictly "is-a" (e.g.,
Apply the Single Responsibility Principle (SRP)
masterA class should have only one reason to change. Avoid 'jam-packing' a class with multiple functionalities, as this reduces conceptual cohesion and makes it difficult to predict how changes to one part of the class will affect other modules. Instead, split responsibilities into separate, specialized classes.
// Good: Responsibilities are split into UserAuth and UserSettings class UserAuth { constructor(user) { this.user = user; } verifyCredentials() { // ... } } class UserSettings { constructor(user) { this.user = user; this.auth = new UserAuth(user); } changeSettings(settings) { if (this.auth.verifyCredentials()) { // ... } } }Apply the Open/Closed Principle (OCP)
masterSoftware entities (classes, modules, functions, etc.) should be open for extension but closed for modification. You should be able to add new functionalities without changing existing code. Avoid using conditional logic (like
if/elsebased on a type name) to handle different implementations; instead, rely on a common interface or method that all implementations provide.// Good: HttpRequester uses a common .request() method on the adapter class AjaxAdapter extends Adapter { constructor() { super(); this.name = "ajaxAdapter"; } request(url) { // request and return promise } } class NodeAdapter extends Adapter { constructor() { super(); this.name = "nodeAdapter"; } request(url) { // request and return promise } } class HttpRequester { constructor(adapter) { this.adapter = adapter; } fetch(url) { return this.adapter.request(url).then(response => { // transform response and return }); } }Avoid side effects
masterA function should ideally take a value and return a value without affecting anything else. Side effects include modifying global variables, writing to files, or mutating input arguments.
To manage side effects:
- Centralize them: Instead of having many functions write to a file, use a single dedicated service.
- Avoid mutating inputs: When dealing with mutable types like Objects and Arrays, always clone the input, modify the clone, and return it. This prevents unexpected bugs in other parts of the application that hold references to the original data.
// Good: Avoiding mutation by returning a new array (cloning) const addItemToCart = (cart, item) => { return [...cart, { item, date: Date.now() }]; };Avoid conditionals using polymorphism
masterLarge
switchorif/elseblocks that check a type to decide behavior are often a sign that you should use polymorphism. Instead of checking a type property, define a common interface and implement specific behaviors in subclasses.// Good: Using polymorphism instead of a switch statement class Boeing777 extends Airplane { getCruisingAltitude() { return this.getMaxAltitude() - this.getPassengerCount(); } } class AirForceOne extends Airplane { getCruisingAltitude() { return this.getMaxAltitude(); } }Make objects have private members using closures
masterIn environments where ES6 private class fields are not used (or for ES5 and below), you can achieve true privacy by using closures. By defining variables within a factory function and only exposing specific methods via the returned object, the internal state remains inaccessible from the outside, even if properties are deleted from the returned object.
function makeEmployee(name) { return { getName() { return name; } }; } const employee = makeEmployee("John Doe"); console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe delete employee.name; console.log(`Employee name: ${employee.getName()}`); // Employee name: John DoeApply the Interface Segregation Principle (ISP)
masterClients should not be forced to depend on interfaces (or implicit contracts in JS) that they do not use. In JavaScript, this means avoiding 'fat interfaces'—large, complex settings objects passed to constructors. Instead, structure your configuration so that optional features are nested or modular, preventing the client from having to provide unnecessary setup for unused functionality.
// Good: Using an 'options' object to keep the primary settings object lean class DOMTraverser { constructor(settings) { this.settings = settings; this.options = settings.options; this.setup(); } setup() { this.rootNode = this.settings.rootNode; this.setupOptions(); } setupOptions() { if (this.options.animationModule) { // ... } } traverse() { /* ... */ } } const $ = new DOMTraverser({ rootNode: document.getElementsByTagName("body"), options: { animationModule() {} } });Maintain a single level of abstraction in functions
masterA function should not mix different levels of abstraction. If a function contains low-level implementation details (like regex parsing) alongside high-level orchestration (like building an AST), it is doing too much. Split these into separate functions to improve reusability and testability.
// Good: High-level orchestration function function parseBetterJSAlternative(code) { const tokens = tokenize(code); const syntaxTree = parse(tokens); syntaxTree.forEach(node => { // parse... }); } // Low-level implementation functions function tokenize(code) { /* ... */ } function parse(tokens) { /* ... */ }Apply the Dependency Inversion Principle (DIP)
masterHigh-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 JavaScript, this is often achieved through Dependency Injection (DI). Instead of a class instantiating its own dependencies (which creates tight coupling), pass the dependencies into the constructor. This allows you to swap implementations (e.g., switching from HTTP to WebSockets) without changing the high-level logic.
// Good: Injecting the requester into the InventoryTracker class InventoryTracker { constructor(items, requester) { this.items = items; this.requester = requester; } requestItems() { this.items.forEach(item => { this.requester.requestItem(item); }); } } class InventoryRequesterV2 { constructor() { this.REQ_METHODS = ["WS"]; } requestItem(item) { /* ... */ } } const inventoryTracker = new InventoryTracker( ["apples", "bananas"], new InventoryRequesterV2() ); inventoryTracker.requestItems();