Skip to content

SOLID

Principio de Responsabilidad Única (SRP)

Como se indica en Clean Code: "Nunca debería haber más de una razón para que una clase cambie". Es tentador saturar una clase con mucha funcionalidad, similar a cuando solo puedes llevar una maleta en tu vuelo. El problema es que tu clase perderá cohesión conceptual y tendrá múltiples motivos para cambiar. Minimizar la cantidad de veces que necesitas modificar una clase es crucial, ya que si concentras demasiada funcionalidad en una sola clase, alterar una parte puede dificultar la comprensión de cómo esto afectará a otros módulos dependientes en tu base de código.

Malo:

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

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

  verifyCredentials() {
    // ...
  }
}

Bueno:

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

Principio Abierto/Cerrado (OCP)

Según Bertrand Meyer: "las entidades de software (clases, módulos, funciones, etc.) deben estar abiertas para extensión pero cerradas para modificación". Esto significa que debemos permitir añadir nuevas funcionalidades sin alterar el código existente.

Malo:

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
}

Bueno:

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

Principio de Sustitución de Liskov (LSP)

Este concepto simple tiene una definición compleja: "Si S es subtipo de T, los objetos de tipo T deben poder sustituirse por objetos de tipo S sin alterar las propiedades del programa (corrección, tareas realizadas, etc.)".

En términos prácticos: si una subclase hereda de una clase padre, ambas deben ser intercambiables sin errores. El ejemplo clásico es Cuadrado-Rectángulo: matemáticamente un cuadrado es un rectángulo, pero modelarlo mediante herencia "es-un" genera problemas.

Malo:

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

Bueno:

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

Principio de Segregación de Interfaces (ISP)

Aunque JavaScript no tiene interfaces, este principio sigue siendo relevante. ISP establece que "los clientes no deben depender de interfaces que no usen". En JavaScript, los contratos de interfaz son implícitos por duck typing.

Un buen ejemplo en JavaScript son las clases que requieren objetos de configuración grandes. Es beneficioso no obligar a los clientes a configurar opciones innecesarias, ya que generalmente no necesitarán todas. Hacerlas opcionales ayuda a evitar una «interfaz obesa».

Malo:

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

Bueno:

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

Principio de Inversión de Dependencias (DIP)

Este principio establece dos reglas clave:

    1. Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones.
    1. Las abstracciones no deben depender de detalles. Los detalles deben depender de abstracciones.

En AngularJS se implementa mediante Inyección de Dependencias (DI). DIP evita que módulos de alto nivel conozcan detalles de bajo nivel, reduciendo el acoplamiento. Esto facilita la refactorización del código.

En JavaScript, las abstracciones son contratos implícitos. Por ejemplo, todo módulo Request para un InventoryTracker debe implementar el método requestItems.

Malo:

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

Bueno:

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