Skip to content

SOLID

Single Responsibility Principle (SRP)

Wie im Clean Code dargelegt: „Es sollte niemals mehr als einen Grund geben, eine Klasse zu ändern“. Es ist verlockend, eine Klasse mit viel Funktionalität zu überfrachten, wie bei einem einzigen Reisekoffer. Das Problem ist mangelnde konzeptionelle Kohärenz und zahlreiche Änderungsanlässe. Die Minimierung von Klassenänderungen ist wichtig: Bei überladenen Klassen können Modifikationen unvorhersehbare Auswirkungen auf abhängige Module haben.

Schlecht:

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

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

  verifyCredentials() {
    // ...
  }
}

Gut:

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

Open/Closed Principle (OCP)

Wie Bertrand Meyer formulierte: „Softwareentitäten (Klassen, Module etc.) sollten offen für Erweiterungen, aber geschlossen für Modifikationen sein“. Nutzern sollte es möglich sein, neue Funktionen durch Erweiterungen hinzuzufügen, ohne bestehenden Code anzupassen.

Schlecht:

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
}

Gut:

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

Liskov Substitution Principle (LSP)

Dieses Prinzip besagt: Wenn S ein Subtyp von T ist, müssen T-Objekte durch S-Objekte ersetzbar sein, ohne die wesentlichen Programmeigenschaften (Korrektheit, Funktionserfüllung etc.) zu verändern.

In der Praxis bedeutet dies, dass Eltern- und Kindklassen austauschbar sein müssen. Das klassische Quadrat-Rechteck-Beispiel zeigt Probleme bei naiver Vererbung: Obwohl mathematisch ein Quadrat ein Rechteck ist, führt die „is-a“-Modellierung zu inkonsistentem Verhalten.

Schlecht:

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

Gut:

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

Interface Segregation Principle (ISP)

Obwohl JavaScript keine expliziten Interfaces besitzt, bleibt dieses Prinzip relevant. Clients sollten nicht zu Abhängigkeiten von ungenutzten Schnittstellen gezwungen werden.

Durch duck typing entstehen implizite Verträge. Typisches Beispiel sind Klassen mit übergroßen Konfigurationsobjekten – optionale Parameter verhindern überfrachtete Schnittstellen.

Modularisierte Optionen sind besser als monolithische Einstellungsobjekte, da Clients selten alle Parameter benötigen. Optionale Einstellungen vermeiden „fette Schnittstellen“.

Schlecht:

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.
  // ...
});

Gut:

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

Dependency Inversion Principle (DIP)

Dieses Prinzip umfasst zwei Kernregeln:

    1. High-Level-Module dürfen nicht von Low-Level-Modulen abhängen. Beide müssen von Abstraktionen abhängen.
    1. Abstraktionen dürfen nicht von Implementierungsdetails abhängen. Details müssen von Abstraktionen abhängen.

In AngularJS durch Dependency Injection (DI) realisiert. DIP reduziert Kopplung durch Abstraktionen – entscheidend für refaktorfreundlichen Code. Lose Kopplung vereinfacht Codeänderungen erheblich.

In JavaScript existieren implizite Verträge durch Methodensignaturen. Beispielsweise muss jedes Request-Modul für InventoryTracker die Methode requestItems bereitstellen.

Schlecht:

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

Gut:

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