Skip to main content Link Menu Expand (external link) Document Search Copy Copied

SOLID

Contenidos
  1. Single Responsibility
  2. Open Closed
  3. Liskov Substitution
  4. Interface Segregation
  5. Dependency Inversion

El principio SOLID es un conjunto de cinco principios de diseño de software que se utilizan para crear sistemas flexibles, escalables y fáciles de mantener. Cada letra del acrónimo SOLID representa un principio específico, a continuación se detalla cada uno de ellos:

Single Responsibility

Este principio establece que una clase debe tener una sola responsabilidad y una sola razón para cambiar. En otras palabras, cada clase debe ser responsable de una sola tarea y no debe tener demasiadas responsabilidades. Si una clase tiene demasiadas responsabilidades, se vuelve difícil de mantener y modificar.

En el código proporcionado, la clase TodoList tiene la responsabilidad de manejar una lista de tareas pendientes y también incluye los métodos “save” y “load” para manejar archivos. Este no es un enfoque adecuado para aplicar el principio SRP.

class TodoList {
  constructor() {
    this.items = []
  }

  addItem(text) {
    this.items.push(text)
  }

  removeItem(index) {
    this.items = items.splice(index, 1)
  }

  toString() {
    return this.items.toString()
  }

  save(filename, content) {
    fs.writeFileSync(filename, content)
  }
  
  load(filename) {
     // Some implementation
  }
}

Para aplicar el principio SRP, se podría crear una nueva clase llamada TodoListService que se encargue exclusivamente de manejar los archivos. Esta clase tendría los métodos “save” y “load” y sería responsable de manejar la persistencia de datos.

class TodoListService {
  save(filename, content) {
    fs.writeFileSync(filename, content)
  }
  
  load(filename) {
     // Some implementation
  }
}

class TodoList {
  constructor() {
    this.items = []
    this.service = new TodoListService() // create an instance of TodoListService
  }

  addItem(text) {
    this.items.push(text)
  }

  removeItem(index) {
    this.items = items.splice(index, 1)
  }

  toString() {
    return this.items.toString()
  }

  // use methods from TodoListService
  save(filename, content) {
    this.service.save(filename, content)
  }
  
  load(filename) {
    this.service.load(filename)
  }
}

En este nuevo código, la clase TodoListService es responsable de manejar el almacenamiento y la recuperación de datos, mientras que la clase TodoList se centra exclusivamente en la lista de tareas pendientes. La clase TodoList ahora utiliza una instancia de TodoListService para guardar y cargar datos, separando así la responsabilidad de manejar la lista de tareas y la responsabilidad de manejar los archivos. Esto hace que el código sea más modular y más fácil de mantener.

Open Closed

El principio Open/Closed (OCP) es uno de los cinco principios del SOLID, que establece que las entidades de software (clases, módulos, funciones, etc.) deben estar abiertas para la extensión pero cerradas para la modificación.

En otras palabras, el principio OCP significa que el comportamiento de una entidad de software (por ejemplo, una clase) debe ser fácilmente extensible sin necesidad de modificar su código fuente existente. Esto se logra diseñando la entidad de software de tal manera que nuevos comportamientos se puedan agregar mediante la adición de nuevas clases o módulos sin necesidad de modificar el código existente.

Veamos el siguiente ejemplo:

class Coder {
  constructor(fullName, language, hobby, education, workplace, position) {
    this.fullName = fullName
    this.language = language
    this.hobby = hobby
    this.education = education
    this.workplace = workplace
    this.position = position
  }
}

class CoderFilter {
  filterByName(coders, fullName) {
    return coders.filter(coder => coder.fullName === fullName)
  }

  filterBySize(coders, language) {
    return coders.filter(coder => coder.language === language)
  }

  filterByHobby(coders, hobby) {
    return coders.filter(coder => coder.hobby === hobby)
  }
}

En el código proporcionado, la clase CoderFilter es responsable de filtrar valores de una lista de instancias de la clase Coder. La implementación actual de dicha clase viola el principio Open Closed, ya que no es fácilmente extensible para manejar nuevos tipos de filtros si se agregan más propiedades.

Para aplicar el principio OCP, se podría implementar un método en la clase CoderFilter que generalice los filtros y acepte la propiedad y el valor a filtrar.

class Coder {
  constructor(fullName, language, hobby, education, workplace, position) {
    this.fullName = fullName
    this.language = language
    this.hobby = hobby
    this.education = education
    this.workplace = workplace
    this.position = position
  }
}

class CoderFilter {
  filterByProp(coders, prop, value) {
    return coders.filter(coder => coder[prop] === value)
  }
}

De esta manera, la clase CoderFilter estaría abierta para extensión al permitir que se agreguen nuevos filtros sin la necesidad de modificarla.

Liskov Substitution

El principio de sustitución de Liskov establece que una clase derivada debe poder sustituirse por su clase base sin afectar el comportamiento del programa.

class Rectangle {
  constructor(width, height) {
    this._width = width
    this._height = height
  }

  get width() {
    return this._width
  }
  get height() {
    return this._height
  }

  set width(value) {
    this._width = value
  }
  set height(value) {
    this._height = value
  }

  getArea() {
    return this._width * this._height
  }
}

class Square extends Rectangle {
  constructor(size) {
    super(size, size)
  }
}

En el código proporcionado, se viola este principio ya que la clase Square deriva de la clase Rectangle pero tiene un comportamiento diferente. Mientras que la clase Rectangle tiene dos propiedades de ancho y alto, que pueden ser modificadas independientemente, la clase Square tiene solo un tamaño que se aplica tanto al ancho como al alto.

Cuando se crea un objeto Square, se establece su tamaño en el constructor, y si se intenta cambiar su ancho o alto a través de los métodos set width o set height, entonces el comportamiento no será el esperado. En resumen, el problema es que la clase Square no se puede usar como una instancia de la clase base Rectangle sin cambiar el comportamiento del programa, lo que viola el principio de sustitución de Liskov.

Una solución podría ser reimplementar los métodos set width y set height en la clase Square para garantizar que el tamaño se aplique tanto al ancho como al alto.

class Rectangle {
  constructor(width, height) {
    this._width = width
    this._height = height
  }

  get width() {
    return this._width
  }
  get height() {
    return this._height
  }

  set width(value) {
    this._width = value
  }
  set height(value) {
    this._height = value
  }

  getArea() {
    return this._width * this._height
  }
}

class Square extends Rectangle {
  constructor(size) {
    super(size, size)
  }
  set width(value) {
    this._width = value
    this._height = value
  }
  set height(value) {
    this._height = value
    this._width = value
  }
}

Con esta implementación, cuando se cambia el ancho o el alto de square, se establecen ambos valores en el mismo valor, lo que garantiza que getArea() siempre funcione correctamente para Square y cumpla con el principio de sustitución de Liskov.

Interface Segregation

El principio de “Interface Segregation” (Segregación de Interfaces) es uno de los cinco principios SOLID de programación orientada a objetos. Este principio establece que una clase no debe depender de métodos que no utiliza. Es decir, si una clase necesita ciertos métodos, sólo debe depender de ellos y no de otros que no necesite.

El principio de “Interface Segregation” promueve la creación de interfaces más pequeñas y especializadas en lugar de interfaces grandes y generales. Esto permite que las clases que implementan estas interfaces tengan sólo los métodos que necesitan, lo que simplifica el código y lo hace más fácil de entender y mantener.

class Phone {
  constructor() {}

  phoneCall(number) {}

  takePhoto() {}

  connectToWifi() {}
}

class IPhone extends Phone {}

class Nokia3310 extends Phone {}

En este caso, también se rompe el principio de “Interface Segregation” porque la clase Phone tiene métodos que no son necesarios en todas las clases que heredan de ella. Por ejemplo, la clase Nokia3310 no puede tomar fotos ni conectarse a Wi-Fi, por lo que no debería tener esos métodos.

Para resolver este problema, podemos crear una clase intermedia llamada SmartPhone que implemente los métodos takePhoto() y connectToWifi(). Luego, podemos hacer que la clase IPhone herede de SmartPhone, y la clase Nokia3310 herede directamente de la clase Phone. De esta manera, IPhone tendrá acceso a los métodos de SmartPhone, mientras que Nokia3310 no los tendrá. La jerarquía de clases quedaría así: `

class Phone {
  constructor() {}

  phoneCall(number) {}

}

class SmartPhone extends Phone {
  takePhoto() { }
  connectToWifi() { }

}

class IPhone extends SmartPhone {}

class Nokia3310 extends Phone {}

De esta manera, se respeta el principio de “Interface Segregation”, ya que cada clase sólo depende de los métodos que necesita, y no está obligada a implementar métodos que no necesita.

Dependency Inversion

El principio de Dependency Inversion (Inversión de Dependencias) es uno de los cinco principios SOLID de programación orientada a objetos. Este principio establece que los módulos de alto nivel no deben depender de los módulos de bajo nivel, sino que ambos deben depender de abstracciones. Además, las abstracciones no deben depender de los detalles, sino que los detalles deben depender de las abstracciones.

class PurchaseHandler {
  processPayment(paymentDetails, amount) {

    const paymentSuccess = PayPalService.requestPayment(paymentDetails, amount)

    if (paymentSuccess) {
      // Do something
      return true
    }

    // Do something
    return false
  }
}

class PayPalService {
  static requestPayment(paymentDetails, amount) {
    // Make some call to server
  }
}

En el código proporcionado, la clase PurchaseHandler depende directamente de la clase PayPalService. Esto significa que si se agrega un nuevo proveedor de pagos o se cambia el proveedor actual, la clase PurchaseHandler se verá afectada y se requerirán cambios en su código para adaptarse a los nuevos cambios. Esto rompe el principio de Dependency Inversion.

Para resolver este problema, podemos crear una propiedad en nuestra clase PurchaseHandler de manera que en su constructor reciba el servicio de pago y no generar una dependencia directa con el código del servicio de pago, de manera de que quien utilice esta clase, sea el encargado de proporcionar en el constructor el servicio que implemente el método requestPayment.

class PurchaseHandler {
  constuctor(paymentService) {
    this.paymentService = paymentService
  }

  processPayment(paymentDetails, amount) {

    const paymentSuccess = this.paymentService.requestPayment(paymentDetails, amount)

    if (paymentSuccess) {
      // Do something
      return true
    }

    // Do something
    return false
  }
}

class PayPalService {
  static requestPayment(paymentDetails, amount) {
    // Make some call to server
  }
}
class OtherPaymentService {
  static requestPayment(paymentDetails, amount) {
    // Make some call to server
  }
}

Para cumplir con el principio de Dependency Inversion, se ha invertido la dependencia de PurchaseHandler de una implementación específica de proveedor de pagos (PayPalService) a una abstracción (paymentService). De esta manera, PurchaseHandler depende de una abstracción en lugar de depender directamente de las clases de bajo nivel (PayPalService o OtherPaymentService).