Principios SOLID y como aplicarlos

Eduardo Acosta
7 min readJan 13, 2023

--

*Todas las Ilustraciones de este post le pertenecen a Ugonna Thelma

S.O.L.I.D son las siglas en inglés de cinco principios de diseño de software que son considerados fundamentales para escribir código limpio, fácil de mantener y escalable. Estos principios son:

S — Single Responsibility (Responsabilidad Unica)

El principio de Responsabilidad Única, también conocido como Single Responsibility Principle (SRP), es uno de los principios fundamentales de S.O.L.I.D y se refiere a la idea de que cada módulo o clase debe tener una sola responsabilidad y esa responsabilidad debe estar completamente encapsulada por la clase. Esto ayuda a asegurar que el código sea fácil de entender, mantener y escalar.

Un ejemplo de código en Node.js que ilustra el SRP es el siguiente:

class Empleado {
constructor(nombre, salario) {
this.nombre = nombre;
this.salario = salario;
}
calcularSalario() {
// lógica para calcular el salario del empleado
}
agregarEmpleado() {
// lógica para agregar un empleado a la base de datos
}
eliminarEmpleado() {
// lógica para eliminar un empleado de la base de datos
}
}

class Autenticacion {
constructor(usuario, contrasena) {
this.usuario = usuario;
this.contrasena = contrasena;
}
verificarCredenciales() {
// lógica para verificar las credenciales del usuario
}
}

En este ejemplo, la clase “Empleado” se encarga de las operaciones relacionadas con un empleado, como calcular su salario, agregarlo o eliminarlo de la base de datos. Por otro lado, la clase “Autenticacion” se encarga de verificar las credenciales del usuario. Cada clase tiene una sola responsabilidad y está completamente encapsulada, lo que significa que si necesitamos cambiar la lógica de cálculo de salario, solo tendríamos que modificar la clase “Empleado” y no afectaría a la clase “Autenticacion”.

Al seguir el principio de Responsabilidad Única, nuestro código se vuelve más limpio, fácil de entender y mantener, lo que a su vez ayuda a prevenir problemas de escalabilidad en el futuro.

O — Open-Closed (Abierto a Extension, Cerrado a Modificacion)

El principio de Abierto/Cerrado, también conocido como Open/Closed Principle (OCP), es otro de los principios fundamentales de S.O.L.I.D y se refiere a la idea de que los módulos o clases deben estar abiertos para extensión, pero cerrados para modificación. Esto significa que podemos agregar nueva funcionalidad a una clase sin tener que modificar su código fuente.

Un ejemplo de código en Node.js que ilustra el OCP es el siguiente:

class Empleado {
constructor(nombre, salario) {
this.nombre = nombre;
this.salario = salario;
}
calcularSalario() {
// lógica para calcular el salario del empleado
}
}

class Gerente extends Empleado {
constructor(nombre, salario, bono) {
super(nombre, salario);
this.bono = bono;
}
calcularSalario() {
return super.calcularSalario() + this.bono;
}
}

En este ejemplo, la clase “Empleado” tiene un método “calcularSalario” que se encarga de calcular el salario del empleado. La clase “Gerente” extiende a la clase “Empleado” y agrega un nuevo método “calcularSalario” que incluye el cálculo del bono. Sin embargo, la clase “Empleado” no ha sido modificada, sigue funcionando como antes.

Al seguir el principio de Abierto/Cerrado, nuestro código se vuelve más fácil de extender y mantener ya que no tenemos que modificar el código existente para agregar nuevas funcionalidades. Esto también ayuda a reducir el riesgo de introducir errores en el código existente al hacer cambios.

Es importante notar que el OCP no significa que no podamos modificar el código existente, sino que debemos tener cuidado de no romper el comportamiento existente al hacerlo. Una buena práctica es crear una clase hija que extienda a la clase existente, y agregar la nueva funcionalidad en ella, en lugar de modificar la clase existente.

L — Liskov Substitution (Sustitucion Liskov)

El principio de Sustitución de Liskov, también conocido como Liskov Substitution Principle (LSP), es otro de los principios fundamentales de S.O.L.I.D y se refiere a la idea de que las subclases deben ser intercambiables con sus clases base sin afectar la correctitud del programa. En otras palabras, si una clase A es una subclase de B, entonces podemos utilizar un objeto de la clase B donde se espera un objeto de la clase A sin causar problemas.

Un ejemplo de código en Node.js que ilustra el LSP es el siguiente:

class Cuadrado {
constructor(lado) {
this.lado = lado;
}
area() {
return this.lado * this.lado;
}
}

class Rectangulo extends Cuadrado {
constructor(lado, alto) {
super(lado);
this.alto = alto;
}
area() {
return this.lado * this.alto;
}
}

function areaTotal(figuras) {
let area = 0;
for (let figura of figuras) {
area += figura.area();
}
return area;
}
const figuras = [new Cuadrado(2), new Rectangulo(2, 4)];
console.log(areaTotal(figuras));

En este ejemplo, la clase “Cuadrado” tiene un método “area” que se encarga de calcular el área de un cuadrado. La clase “Rectangulo” extiende a la clase “Cuadrado” y sobrescribe el método “area” para calcular el área de un rectángulo. Sin embargo, el método “areaTotal” espera una lista de figuras y llama al método “area” en cada una de ellas, independientemente de si es un cuadrado o un rectángulo.

Al seguir el principio de Sustitución de Liskov, podemos utilizar un objeto de una subclase en cualquier lugar donde se espera un objeto de la clase base, sin causar problemas en el comportamiento del programa. Esto ayuda a asegurar que nuestro código sea más flexible y escalable.

I — Interface Segregation (Segregacion de Interfaces)

El principio de Segregación de Interfaces, también conocido como Interface Segregation Principle (ISP), es otro de los principios fundamentales de S.O.L.I.D y se refiere a la idea de que las interfaces deben ser pequeñas y específicas, en lugar de grandes y genéricas. Esto significa que en lugar de tener una interfaz con muchos métodos que no se utilizan, debemos crear varias interfaces pequeñas y específicas para cada grupo de funcionalidades relacionadas.

Un ejemplo de código en Node.js que ilustra el ISP es el siguiente:

interface Volador {
volar();
}
interface Nadador {
nadar();
}
interface Caminante {
caminar();
}
class Pajaro implements Volador, Nadador, Caminante {
volar() {/*...*/}
nadar() {/*...*/}
caminar() {/*...*/}
}
class Pez implements Nadador {
nadar() {/*...*/}
}
class Persona implements Caminante {
caminar() {/*...*/}
}

En este ejemplo, tenemos tres interfaces: “Volador”, “Nadador” e “Caminante”, cada una con un solo método. La clase “Pajaro” implementa las tres interfaces, ya que es un animal que puede volar, nadar y caminar. Por otro lado, la clase “Pez” solo implementa la interfaz “Nadador” ya que solo puede nadar y la clase “Persona” solo implementa la interfaz “Caminante” ya que solo puede caminar.

Al seguir el principio de Segregación de Interfaces, nuestro código se vuelve más fácil de entender y mantener ya que las interfaces son específicas y solo contienen los métodos necesarios para un conjunto específico de funcionalidades. Además, al utilizar interfaces específicas en lugar de una interfaz genérica, evitamos obligar a las clases que las implementen a implementar métodos que no utilizan.

Es importante tener en cuenta que el ISP no significa que debamos tener una interfaz para cada clase, sino que debemos ser conscientes del acoplamiento que genera tener interfaces con muchos métodos y tratar de minimizarlo.

D — Dependency Inversion (Inversion de Dependencias)

Dependency Inversion (Inversión de Dependencias) es un principio de diseño que se refiere a la abstracción de las dependencias entre componentes de software. En lugar de tener componentes altamente acoplados que dependen directamente de otras dependencias, se utilizan interfaces o contratos para establecer las dependencias. De esta manera, los componentes se vuelven menos dependientes de una implementación específica y más fáciles de cambiar y probar.

Un ejemplo de inversión de dependencias podría ser una aplicación para un servicio de mensajería. En lugar de tener un componente de envío de correo electrónico que depende directamente de una librería de correo electrónico específica, se utiliza una interfaz de mensajería que es implementada por una clase de correo electrónico específica.

// Interfaz de mensajería
interface IMessenger {
send(to: string, message: string): void;
}
// Implementación de correo electrónico
class EmailMessenger implements IMessenger {
send(to: string, message: string): void {
// lógica de envío de correo electrónico
}
}
// Componente de envío de mensajes
class MessageSender {
messenger: IMessenger;
constructor(messenger: IMessenger) {
this.messenger = messenger;
}
sendMessage(to: string, message: string): void {
this.messenger.send(to, message);
}
}
// Uso
const emailMessenger = new EmailMessenger();
const messageSender = new MessageSender(emailMessenger);
messageSender.sendMessage("example@example.com", "Hello World!");j

En este ejemplo, el componente de envío de mensajes (MessageSender) depende de la interfaz de mensajería (IMessenger) en lugar de una implementación específica. Esto significa que se podría reemplazar fácilmente la implementación de correo electrónico con una implementación de mensajería de texto o de mensajería instantánea sin tener que modificar el componente de envío de mensajes.

--

--

Responses (1)