Clean Code JavaScript

repository·master·Indexed 13 days ago

https://github.com/ryanmcdermott/clean-code-javascript

A 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.

Tokens
9K
Snippets
36
Records
39
Agent score
50%

What's inside Clean Code JavaScript

  1. Introduction to clean-code-javascript

    master
    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.
  2. Apply the Liskov Substitution Principle (LSP)

    master

    If 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 Shape instead.

    // 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;
      }
    }
  3. Ensure functions do only one thing

    master

    This 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();
    }
  4. Prefer composition over inheritance

    master

    When modeling data, prefer composition (a "has-a" relationship) over inheritance (an "is-a" relationship) where possible.

    When to use Inheritance:

    1. The relationship is strictly "is-a" (e.g., Human is an Animal).
    2. You need to reuse code from base classes.
    3. 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 Employee has TaxData, but an Employee is not a type of TaxData), 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);
      }
    }
  5. Apply the Single Responsibility Principle (SRP)

    master

    A 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()) {
          // ...
        }
      }
    }
  6. Apply the Open/Closed Principle (OCP)

    master

    Software 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/else based 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
        });
      }
    }
  7. Avoid side effects

    master

    A 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:

    1. Centralize them: Instead of having many functions write to a file, use a single dedicated service.
    2. 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() }];
    };
  8. Avoid conditionals using polymorphism

    master

    Large switch or if/else blocks 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();
      }
    }
  9. Make objects have private members using closures

    master

    In 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 Doe
  10. Apply the Interface Segregation Principle (ISP)

    master

    Clients 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() {}
      }
    });
  11. Maintain a single level of abstraction in functions

    master

    A 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) { /* ... */ }
  12. Apply the Dependency Inversion Principle (DIP)

    master

    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 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();