SOLID
Contenidos
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
).