Principios de Diseño: SOLID

Daniel Gutiérrez
4 min readMay 2, 2024

Cuando hablamos de Principios de Diseño de Software nos referimos a lineamientos de alto nivel que guían nuestras decisiones arquitectónicas y nos ayudan a crear mejor código. SOLID es una piedra angular en esta materia.

🤔Pero… ¿Qué es SOLID?

Son 5 Principios de Diseño popularizados por Robert C. Martin (a.k.a. Uncle Bob) que abordan la problemática de construir un Software que sea robusto y mantenible en el tiempo.

Estos principios son:

  • Single Responsibility principle
  • Open-Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle
Imagen generada usando DALL-E

Single Responsibility Principle

Una clase debería tener sólo una razón para cambiar.

Ya sea aplicado a una clase o a una función, este principio se relaciona con el concepto de “bajo acoplamiento”, si una clase tiene sólo una responsabilidad tendrá sólo un motivo para cambiar, si tiene sólo un motivo para cambiar su mantenimiento será más fácil.

🚩Sin SRP

export class EmailNotificationService extends NotificationService {
sendNotification(template: string, params: string[]): void {
// 1. genera mensaje a partir del template
// 2. envía por email
}

}

✅ Con SRP

export class EmailNotificationService extends NotificationService {
sendNotification(message: string): void {
// 1. envía por email
}
}

export class TemplateEngine {

generateMessage(template: string, params: string[]): string {
// 1. genera mensaje a partir del template
}

}

Open-Closed Principle

Un módulo debiera estar abierto para su extensión, pero cerrado para su modificación.

Este principio busca evitar la rígidez en el código, haciendo que puedas adaptarte a cambios en los requerimientos sin necesidad de rescribir tanto código. Mientras menos código cambias, menor es la probabilidad de introducir bugs.

🚩 Sin OCP


export class NotificationService {

constructor(
private emailService: EmailService, private smsService: SMSService
){
}

public sendNotification(type: string, message: string): void {
if ("EMAIL" === type) {
this.emailService.send(message);
} else if ("SMS" === type) {
this.smsService.send(message);
} else {
throw new NotImplementedException();
}
}

}

✅Con OCP

export abstract class NotificationService {
abstract sendNotification(message: string): void;
}

export class EmailNotificationService extends NotificationService {
sendNotification(message: string): void {
// envía por email
}
}

export class PushNotificationService extends NotificationService {
sendNotification(message: string): void {
// envía notificación Push
}
}

Liskov Substitution Principle

Una clase derivada debe ser capaz de sustituir a la clase padre, sin afectar la correcta ejecución de un programa.

Nombrado así por Barbara Liskov, establece que una clase derivada debe ser capaz de sustituir a la clase padre, sin afectar la correcta ejecución de un programa. Simple, pero fundamental.

🚩 Sin LSP

export abstract class NotificationService {
abstract sendNotification(message: string): void;
abstract sendImage(imageBase64: string): void;
}

export class EmailNotificationService extends NotificationService {
// implementa métodos
}

export class SMSNotificationService extends NotificationService {
// implementa métodos abstractos
// cómo implemento el sendImage? 😓
}

✅Con LSP

export abstract class NotificationService {
abstract sendNotification(message: string): void;
}

export class EmailNotificationService extends NotificationService {
// La implementación puede ir en la concreción, o una interfaz distinta (ISP)
sendImage(...

// implementa métodos abstractos
}

export class SMSNotificationService extends NotificationService {
// implementa métodos abstractos
}

Interface Segregation Principle

Muchas interfaces específicas son mejores que una interfaz de propósito general

Este principio promueve interfaces más acotadas y fáciles de mantener. Por ejemplo si tienes un módulo que tiene distintos clientes que dependen de distintas funcionalidades, llámese A y B, el cliente B no debería verse afectado por cambios en la funcionalidad del cliente A.

🚩 Sin ISP

export abstract class NotificationService {
abstract sendNotification(message: string): void;
abstract sendImage(imageBase64: string): void;
}

export class EmailNotificationService extends NotificationService {
// implementa métodos
}
export class SMSNotificationService extends NotificationService {
// implementa métodos abstractos
// cómo implemento el sendImage? 😓
}

✅Con ISP

export interface NotificationService {
sendNotification(message: string): void;
}

export interface ImageNotificationService {
sendImage(imageBase64: string): void;
}

export interface VideoNotificationService {
sendVideo(videoPath: string): void;
}

export class EmailNotificationService implements NotificationService, ImageNotificationService, VideoNotificationService {
// implementa métodos
}

export class SMSNotificationService implements NotificationService {
// implementa métodos
}

Dependency Inversion Principle

Depende de las abstracciones, no dependas de las concreciones.

“Cada dependencia en tu sistema debe depender de una interfaz o una clase abstracta”. En la práctica esto puede muy estricto, lo que se busca evitar es depender de concreciones que sean volátiles, o que su interfaz de uso este demasiado ligada a su implementación.

🚩 Sin DIP

export class EmailService{

constructor(
private rabbitMQService: RabbitMQService
){

...
}

✅Con DIP

export class EmailService{

constructor(
//abstrae el servicio de la implementación MQ utilizada
private messagingService: MessagingService
){

...
}

📖Algunas Referencias, para profundizar

2000. Robert C. Martin —Article: Design Principles and Design Patterns.

2002. Robert C. Martin — Agile Software Development, Principles, Patterns, and Practices.

--

--