Talento | Employers

Principios SOLID: La base del código limpio y escalable

por Alonso Valencia    |    July 18, 2025    |      11 min de lectura

COMPARTIR :

A person with pink hands types on a keyboard in front of a large computer screen displaying código limpio. Floating code snippets, speakers, and a small superhero figurine are also visible on the desk. CodersLink 2025.

En la programación orientada a objetos (OOP), escribir código que sea mantenible, escalable y flexible es un reto constante. Para resolverlo, Robert C. Martin (Uncle Bob) propuso los principios SOLID, cinco directrices que permiten diseñar sistemas de software robustos y sostenibles.

Además de entender qué son, en este artículo profundizamos en por qué surgieron, qué problemas resuelven, ejemplos clásicos en código y en la vida real, así como el impacto que tienen en la arquitectura de software.

 

Origen y Razón de los Principios SOLID

Los principios SOLID emergen como respuesta al crecimiento en complejidad del software y a la necesidad de modularizarlo para facilitar el mantenimiento y la extensión sin consecuencias colaterales.

Fueron introducidos en el paper de Robert C. Martin en 2000 “Design Principles and Design Patterns” y popularizados por Michael Feathers, quien formó el acrónimo:

  • S: Single Responsibility Principle (Responsabilidad Única)
  • O: Open/Closed Principle (Abierto/Cerrado)
  • L: Liskov Substitution Principle (Sustitución de Liskov)
  • I: Interface Segregation Principle (Segregación de Interfaces)
  • D: Dependency Inversion Principle (Inversión de Dependencias)

Implementar SOLID reduce el code smell, el code rot y el temido código espagueti, al hacer las bases de software flexibles, reutilizables y con menos dependencias.

 

1. Single Responsibility Principle (SRP)

“Una clase debe tener una sola razón para cambiar.”

Este principio no sólo busca dividir funcionalidades, sino también limitar el impacto de los cambios. Una clase que hace demasiadas cosas es propensa a fallos al modificar alguna de sus responsabilidades.

Ejemplo en la vida real:
En un hospital, el cirujano no debería ser quien además administre el hospital, atienda la recepción y se encargue de la limpieza del quirófano. Cada especialista cumple una función distinta para garantizar eficiencia y calidad en la atención.

Ejemplo en código:
Un objeto Libro no debería imprimir su contenido. Debería existir un ImpresoraDeLibros que se encargue de esa responsabilidad.

Beneficios:

  • Menos dependencias.
  • Clases más pequeñas y enfocadas.
  • Mejor capacidad de testeo.

 

2. Open/Closed Principle (OCP)

“Las entidades de software deben estar abiertas para extensión pero cerradas para modificación.”

Cuando el negocio cambia, deberías poder agregar comportamiento sin alterar el código existente, evitando romper lo que ya funciona.

Ejemplo en la vida real:
Una aplicación de mensajería que inicialmente solo envía mensajes de texto puede extenderse para enviar imágenes o notas de voz sin modificar el código existente de envío de texto. Simplemente se agregan nuevos módulos que respetan la misma interfaz de “mensaje”.

Ejemplo en código:
Crear una clase base ProcesadorDePago y especializaciones como ProcesadorPayPal y ProcesadorTarjeta que extienden la funcionalidad.

Técnicas clave:

  • Herencia.
  • Composición.
  • Interfaces.

 

3. Liskov Substitution Principle (LSP)

“Los objetos de una subclase deben poder sustituir a objetos de la clase padre sin alterar el funcionamiento.”

El principio de Sustitución de Liskov previene herencias incorrectas que rompen el polimorfismo.

Ejemplo clásico:

Un auto eléctrico no debería ser una subclase de auto a gasolina si modificar el método de “llenar combustible” implica cambiar completamente su comportamiento. El auto eléctrico debería tener su propia jerarquía para manejar “recargar batería” sin afectar el diseño original.

Consecuencias:

  • Mantener coherencia entre clases padre e hijas.
  • Evitar el uso de herencia cuando la relación no es de verdadero “es-un”.

 

4. Interface Segregation Principle (ISP)

“Los clientes no deben estar forzados a depender de interfaces que no usan.”

Cuando una interfaz es demasiado general, obliga a las clases a implementar métodos irrelevantes.

Ejemplo en la vida real:
En una biblioteca infantil, el catálogo que se muestra a los niños debería contener solo libros apropiados para su edad, sin incluir novelas para adultos.

Aplicación en desarrollo:

  • Dividir interfaces grandes en pequeñas.
  • Interfaces especializadas por contexto.

Resultado:

  • Flexibilidad.
  • Menor acoplamiento.
  • Testeo más sencillo.

 

5. Dependency Inversion Principle (DIP)

“Las clases de alto nivel no deben depender de clases de bajo nivel. Ambas deben depender de abstracciones.”

Esto rompe la dependencia directa entre módulos específicos y permite usar inyección de dependencias.

Ejemplo en la vida real:
Un conductor no necesita conocer el funcionamiento interno del motor para manejar un automóvil. Solo interactúa con el volante, los pedales y la palanca de cambios, gracias a la abstracción del sistema de conducción.

Patrones relacionados:

  • Inyección de Dependencias.
  • Inversión de Control.

 

Consecuencias de No Aplicar SOLID

  • Code Smell: Módulos que huelen a error inminente.
  • Code Rot: Código que se vuelve inservible o frágil.
  • Vulnerabilidades de seguridad: Más propenso a fallos y brechas.

Otros Principios Relacionados

  • DRY (Don’t Repeat Yourself): Evitar la duplicidad.
  • KISS (Keep It Simple, Stupid): Mantenlo sencillo.

 

Ejemplos Prácticos

Single Responsibility Principle (SRP)

“Una clase debe tener una única razón para cambiar.”

Este principio no sólo busca dividir funcionalidades, sino también limitar el impacto de los cambios. Una clase que hace demasiadas cosas es propensa a fallos al modificar alguna de sus responsabilidades.

Ejemplo en código (en C++)

#include <iostream>
#include <string>

// Clase responsable de gestionar usuarios
class User {
private:
std::string username;
public:
User(const std::string& name) : username(name) {}
std::string getUsername() const {
return username;
}
};

// Clase responsable de la persistencia de usuarios
class UserRepository {
public:
void save(const User& user) {
std::cout << "Saving user: " << user.getUsername() << std::endl;
}
};

// Clase responsable de enviar notificaciones
class NotificationService {
public:
void sendWelcomeNotification(const User& user) {
std::cout << "Sending welcome email to: " << user.getUsername() << std::endl;
}
};

int main() {
User newUser("coder123");
UserRepository repository;
NotificationService notifier;

repository.save(newUser);
notifier.sendWelcomeNotification(newUser);

return 0;
}

Explicación

  • User: solo modela un usuario.
  • UserRepository: se encarga de persistir el usuario.
  • NotificationService: envía notificaciones.

Cada clase tiene una sola razón para cambiar: si la lógica de almacenamiento o la de notificación cambia, solo afecta a su propia clase.


Open/Closed Principle (OCP)

“Las entidades de software deben estar abiertas para extensión, pero cerradas para modificación.”

Este principio indica que podemos agregar nuevas funcionalidades a una entidad sin necesidad de modificar su código existente.

Ejemplo en código (en C++)

Supongamos que estamos desarrollando un sistema de reportes que inicialmente genera reportes en formato PDF. Ahora, queremos soportar también reportes en Excel sin modificar la clase existente.

#include <iostream>
#include <string>
#include <memory>

// Interfaz base para generadores de reporte
class ReportGenerator {
public:
virtual void generate(const std::string& data) = 0;
virtual ~ReportGenerator() = default;
};

// Implementación concreta para PDF
class PDFReportGenerator : public ReportGenerator {
public:
void generate(const std::string& data) override {
std::cout << "Generating PDF report with data: " << data << std::endl;
}
};

// Nueva implementación para Excel
class ExcelReportGenerator : public ReportGenerator {
public:
void generate(const std::string& data) override {
std::cout << "Generating Excel report with data: " << data << std::endl;
}
};

// Cliente que usa la abstracción ReportGenerator
class ReportService {
private:
ReportGenerator& generator;
public:
ReportService(ReportGenerator& gen) : generator(gen) {}

void createReport(const std::string& data) {
generator.generate(data);
}
};

int main() {
PDFReportGenerator pdfGenerator;
ExcelReportGenerator excelGenerator;

ReportService pdfService(pdfGenerator);
ReportService excelService(excelGenerator);

pdfService.createReport("Annual Financial Data");
excelService.createReport("Monthly Sales Data");

return 0;
}

Explicación

  • ReportGenerator: define la abstracción.
  • PDFReportGenerator y ExcelReportGenerator: implementan la generación en distintos formatos.
  • ReportService : depende solo de la abstracción, por lo que es fácil extender soportando otros formatos como Word, sin modificar su código.

Beneficios

  • Permite extender sin tocar lo que ya funciona.
  • Facilita la escalabilidad.

Liskov Substitution Principle (LSP)

“Las subclases deben poder usarse en lugar de las clases padre sin afectar el comportamiento esperado.”

Ejemplo en código (en C++)

Vamos a ilustrarlo correctamente con un sistema de autenticación de usuarios:

#include <iostream>
#include <string>

// Clase abstracta base
class Authenticator {
public:
virtual bool login(const std::string& username, const std::string& password) = 0;
virtual ~Authenticator() = default;
};

// Autenticación basada en base de datos
class DatabaseAuthenticator : public Authenticator {
public:
bool login(const std::string& username, const std::string& password) override {
std::cout << "Authenticating " << username << " via Database." << std::endl;
return true; // Simulación
}
};

// Autenticación basada en OAuth
class OAuthAuthenticator : public Authenticator {
public:
bool login(const std::string& username, const std::string& password) override {
std::cout << "Authenticating " << username << " via OAuth." << std::endl;
return true; // Simulación
}
};

void authenticateUser(Authenticator& auth, const std::string& username, const std::string& password) {
if (auth.login(username, password)) {
std::cout << "Login successful for user: " << username << std::endl;
} else {
std::cout << "Login failed for user: " << username << std::endl;
}
}

int main() {
DatabaseAuthenticator dbAuth;
OAuthAuthenticator oauthAuth;

authenticateUser(dbAuth, "john_doe", "password123");
authenticateUser(oauthAuth, "jane_doe", "oauthpass");

return 0;
}

Ambos cumplen el contrato definido.


Interface Segregation Principle (ISP)

“Los clientes no deben estar forzados a depender de interfaces que no usan.”

Este principio recomienda dividir las interfaces grandes en múltiples interfaces específicas para cada necesidad.

Ejemplo en código (en C++)

Imaginemos un sistema que gestiona periféricos de una computadora: impresoras, escáners y fax. Si usamos una sola interfaz, los dispositivos estarían obligados a implementar métodos que no necesitan.

```cpp
#include <iostream>
#include <string>

// Interfaces específicas
class Printer {
public:
virtual void print(const std::string& document) = 0;
};

class Scanner {
public:
virtual void scan(const std::string& document) = 0;
};

class Fax {
public:
virtual void fax(const std::string& document) = 0;
};

// Impresora básica solo imprime
class BasicPrinter : public Printer {
public:
void print(const std::string& document) override {
std::cout << "Printing document: " << document << std::endl;
}
};

// Multifunction device implementa todo
class MultiFunctionDevice : public Printer, public Scanner, public Fax {
public:
void print(const std::string& document) override {
std::cout << "Multifunction printing: " << document << std::endl;
}

void scan(const std::string& document) override {
std::cout << "Scanning document: " << document << std::endl;
}

void fax(const std::string& document) override {
std::cout << "Faxing document: " << document << std::endl;
}
};

int main() {
BasicPrinter printer;
printer.print("Annual Report");

MultiFunctionDevice mfd;
mfd.print("Proposal Document");
mfd.scan("ID Proof");
mfd.fax("Signed Contract");

return 0;
}
```

Explicación

  • Cada interfaz (Printer, Scanner, Fax) define solo un comportamiento.
  • BasicPrinter solo implementa impresión.
  • MultiFunctionDevice implementa impresión, escaneo y fax.

Así evitamos forzar a que dispositivos simples deban implementar métodos innecesarios.


Dependency Inversion Principle (DIP)

“Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones.”

Este principio sugiere que el código debe depender de interfaces o abstracciones, no de implementaciones concretas. Así logramos un sistema desacoplado y flexible.

Ejemplo en código (en C++)

Implementemos un sistema donde un equipo de desarrollo utiliza un sistema de control de versiones sin importar si es Git, SVN u otro.

#include <iostream>
#include <string>

// Abstracción para control de versiones
class VersionControl {
public:
virtual void commit(const std::string& message) = 0;
virtual void push() = 0;
virtual void pull() = 0;
virtual ~VersionControl() = default;
};

// Implementación concreta: Git
class GitControl : public VersionControl {
public:
void commit(const std::string& message) override {
std::cout << "Git commit: " << message << std::endl;
}

void push() override {
std::cout << "Git push to remote repository" << std::endl;
}

void pull() override {
std::cout << "Git pull from remote repository" << std::endl;
}
};

// Implementación concreta: SVN
class SVNControl : public VersionControl {
public:
void commit(const std::string& message) override {
std::cout << "SVN commit: " << message << std::endl;
}

void push() override {
std::cout << "SVN push (commit + update remote)" << std::endl;
}

void pull() override {
std::cout << "SVN update from repository" << std::endl;
}
};

// Módulo de alto nivel: equipo de desarrollo
class DevelopmentTeam {
private:
VersionControl& vcs;
public:
DevelopmentTeam(VersionControl& versionControl) : vcs(versionControl) {}

void workflow(const std::string& message) {
vcs.commit(message);
vcs.push();
vcs.pull();
}
};

int main() {
GitControl git;
SVNControl svn;

DevelopmentTeam teamGit(git);
DevelopmentTeam teamSVN(svn);

teamGit.workflow("Initial Git Commit");
teamSVN.workflow("Initial SVN Commit");

return 0;
}

Explicación

  • VersionControl es la  abstracción.
  • GitControly SVNControl son las implementaciones concretas.
  • DevelopmentTeam solo conoce la abstracción, por lo que no importa qué sistema de control de versiones se use.

Beneficios

  • Flexibilidad para cambiar la implementación sin afectar al cliente.
  • Facilita pruebas con mocks o stubs.

 

Conclusiones

Adoptar los principios SOLID es mucho más que una práctica técnica: es una filosofía que transforma la forma en que concebimos, diseñamos y evolucionamos el software. Cada principio actúa como un pilar en la arquitectura de sistemas modernos, proporcionando una base sólida para enfrentar los desafíos del crecimiento, la complejidad y el cambio constante en los requerimientos.

Implementar SOLID genera beneficios tangibles:

  • Software robusto y sostenible: las bases bien estructuradas permiten escalar sin temor a romper lo existente.
  • Mantenibilidad a largo plazo: el código se vuelve más sencillo de entender, adaptar y depurar.
  • Colaboración eficiente: los equipos de desarrollo trabajan con mayor sincronía al manejar módulos bien definidos y desacoplados.
  • Facilidad para las pruebas: la separación de responsabilidades y la inversión de dependencias facilitan el desarrollo de pruebas unitarias y de integración.

Más allá del código, SOLID fomenta una mentalidad orientada a la evolución continua. Permite anticiparse a la deuda técnica, reduce el impacto de los cambios y facilita la incorporación de nuevas tecnologías o paradigmas sin necesidad de reestructurar el sistema por completo.

En un entorno donde el software es un activo estratégico, conocer y aplicar SOLID es una ventaja competitiva que distingue a quienes no solo escriben código, sino que también diseñan soluciones resilientes, escalables y preparadas para el futuro.

 

Master Class

WordPress con el poder de REST API

Aprenderás porqué WordPress sigue siendo utilizado en gran cantidad de sitios alrededor del mundo, cómo debes instalarlo, darle mantenimiento correctamente y también cómo puedes usar todas las tecnologías actuales como REST API para sacarle más provecho.