Skip to content

SOLID

Principe de responsabilité unique (SRP)

Comme l'indique Clean Code : « Une classe ne devrait jamais avoir plus d'une raison de changer ». Il est tentant de surcharger une classe avec de nombreuses fonctionnalités, comme lorsqu'on ne peut prendre qu'une valise en vol. Le problème est que la classe perdra sa cohésion conceptuelle et aura de multiples raisons de changer. Minimiser les modifications d'une classe est crucial car si vous modifiez une fonctionnalité dans une classe trop chargée, il devient difficile d'évaluer l'impact sur les modules dépendants.

Mauvais :

javascript
class UserSettings {
  constructor(user) {
    this.user = user;
  }

  changeSettings(settings) {
    if (this.verifyCredentials()) {
      // ...
    }
  }

  verifyCredentials() {
    // ...
  }
}

Bon :

javascript
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()) {
      // ...
    }
  }
}

Principe ouvert/fermé (OCP)

Selon Bertrand Meyer : « les entités logicielles (classes, modules, fonctions...) doivent être ouvertes à l'extension mais fermées à la modification ». Ce principe signifie qu'il faut permettre aux utilisateurs d'ajouter de nouvelles fonctionnalités sans modifier le code existant.

Mauvais :

javascript
class AjaxAdapter extends Adapter {
  constructor() {
    super();
    this.name = "ajaxAdapter";
  }
}

class NodeAdapter extends Adapter {
  constructor() {
    super();
    this.name = "nodeAdapter";
  }
}

class HttpRequester {
  constructor(adapter) {
    this.adapter = adapter;
  }

  fetch(url) {
    if (this.adapter.name === "ajaxAdapter") {
      return makeAjaxCall(url).then(response => {
        // transform response and return
      });
    } else if (this.adapter.name === "nodeAdapter") {
      return makeHttpCall(url).then(response => {
        // transform response and return
      });
    }
  }
}

function makeAjaxCall(url) {
  // request and return promise
}

function makeHttpCall(url) {
  // request and return promise
}

Bon :

javascript
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
    });
  }
}

Principe de substitution de Liskov (LSP)

Ce terme technique recouvre un concept simple : « Si S est un sous-type de T, alors les objets de type T peuvent être remplacés par des objets de type S sans altérer les propriétés souhaitables du programme (exactitude, tâches accomplies, etc.) ».

Concrètement, si une classe parente et une classe enfant existent, elles doivent pouvoir être utilisées de manière interchangeable sans produire de résultats incorrects. L'exemple classique Carré-Rectangle illustre ceci : mathématiquement un carré est un rectangle, mais une modélisation par héritage « est-un » crée des incohérences.

Mauvais :

javascript
class Rectangle {
  constructor() {
    this.width = 0;
    this.height = 0;
  }

  setColor(color) {
    // ...
  }

  render(area) {
    // ...
  }

  setWidth(width) {
    this.width = width;
  }

  setHeight(height) {
    this.height = height;
  }

  getArea() {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  setWidth(width) {
    this.width = width;
    this.height = width;
  }

  setHeight(height) {
    this.width = height;
    this.height = height;
  }
}

function renderLargeRectangles(rectangles) {
  rectangles.forEach(rectangle => {
    rectangle.setWidth(4);
    rectangle.setHeight(5);
    const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20.
    rectangle.render(area);
  });
}

const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);

Bon :

javascript
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;
  }
}

function renderLargeShapes(shapes) {
  shapes.forEach(shape => {
    const area = shape.getArea();
    shape.render(area);
  });
}

const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);

Principe de ségrégation des interfaces (ISP)

JavaScript n'a pas d'interfaces, ce principe s'applique donc moins strictement. Il reste néanmoins pertinent malgré l'absence de système de types.

L'ISP stipule que « les clients ne doivent pas être forcés de dépendre d'interfaces qu'ils n'utilisent pas ». En JavaScript, les interfaces sont des contrats implicites via le typage canard.

Un bon exemple est la gestion d'objets de configuration complexes. Ne pas obliger les clients à configurer des options superflues évite les « interfaces obèses ». Les paramètres optionnels préviennent ce problème.

Mauvais :

javascript
class DOMTraverser {
  constructor(settings) {
    this.settings = settings;
    this.setup();
  }

  setup() {
    this.rootNode = this.settings.rootNode;
    this.settings.animationModule.setup();
  }

  traverse() {
    // ...
  }
}

const $ = new DOMTraverser({
  rootNode: document.getElementsByTagName("body"),
  animationModule() {} // Most of the time, we won't need to animate when traversing.
  // ...
});

Bon :

javascript
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() {}
  }
});

Principe d'inversion des dépendances (DIP)

Ce principe repose sur deux règles essentielles :

    1. Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau. Les deux doivent dépendre d'abstractions.
    1. Les abstractions ne doivent pas dépendre des détails. Les détails doivent dépendre des abstractions.

Ce concept peut sembler complexe, mais l'Injection de Dépendances (DI) d'AngularJS en est une implémentation. Bien que distincts, le DIP empêche les modules de haut niveau de connaître les détails des modules de bas niveau, réduisant ainsi le couplage - facteur nuisible à la maintenabilité.

En JavaScript, les abstractions correspondent aux contrats implicites (méthodes/propriétés exposées). Par exemple, tout module Request pour un InventoryTracker devra implémenter requestItems.

Mauvais :

javascript
class InventoryRequester {
  constructor() {
    this.REQ_METHODS = ["HTTP"];
  }

  requestItem(item) {
    // ...
  }
}

class InventoryTracker {
  constructor(items) {
    this.items = items;

    // BAD: We have created a dependency on a specific request implementation.
    // We should just have requestItems depend on a request method: `request`
    this.requester = new InventoryRequester();
  }

  requestItems() {
    this.items.forEach(item => {
      this.requester.requestItem(item);
    });
  }
}

const inventoryTracker = new InventoryTracker(["apples", "bananas"]);
inventoryTracker.requestItems();

Bon :

javascript
class InventoryTracker {
  constructor(items, requester) {
    this.items = items;
    this.requester = requester;
  }

  requestItems() {
    this.items.forEach(item => {
      this.requester.requestItem(item);
    });
  }
}

class InventoryRequesterV1 {
  constructor() {
    this.REQ_METHODS = ["HTTP"];
  }

  requestItem(item) {
    // ...
  }
}

class InventoryRequesterV2 {
  constructor() {
    this.REQ_METHODS = ["WS"];
  }

  requestItem(item) {
    // ...
  }
}

// By constructing our dependencies externally and injecting them, we can easily
// substitute our request module for a fancy new one that uses WebSockets.
const inventoryTracker = new InventoryTracker(
  ["apples", "bananas"],
  new InventoryRequesterV2()
);
inventoryTracker.requestItems();