Principios SOLID
Una serie práctica sobre los principios SOLID con ejemplos y patrones para ayudarte a escribir aplicaciones TypeScript mantenibles, testeables y escalables (Angular & Next.js).
Esta guía desglosa los principios SOLID uno por uno para que cada concepto deje de sentirse como una abstracción y se convierta en una técnica concreta que puedes aplicar hoy. Verás modelos mentales, anti-patrones y una historia que comienza con una sola responsabilidad y termina con un grafo de dependencias que prefiere interfaces sobre implementaciones.
SOLID es un acrónimo acuñado por Robert C. Martin (Uncle Bob) que captura cinco principios de diseño para escribir código mantenible, escalable y testeable. Ya sea que estés construyendo un backend en C# o un frontend en TypeScript, estas ideas se traducen en las mismas verdades fundamentales: mantén los módulos pequeños, respeta los contratos y depende de abstracciones, no de detalles concretos.
1. Principio de Responsabilidad Única (SRP)
Idea Central: Una clase o módulo debe tener una y solo una razón para cambiar. Si te encuentras enumerando más de una responsabilidad, ya la has violado.
Por qué Importa: Cuando una clase maneja múltiples preocupaciones, digamos, parsear la entrada del usuario y enviar correos, un cambio en la lógica del correo te obliga a re-testear el parseo. Quedas atrapado en una red de efectos secundarios. Separar responsabilidades aísla el riesgo y simplifica las pruebas.
En la Práctica: En lugar de un UserService que haga todo, crea un UserValidator, un UserRepository y un EmailNotifier. Cada clase tiene una sola razón para cambiar.
Señales de Alerta: Nombres de clase como “Manager”, “Service” o “Handler” que sugieren múltiples trabajos; archivos de test que cubren comportamientos muy diferentes en un solo setup.
2. Principio Abierto/Cerrado (OCP)
Idea Central: Una clase debe estar abierta a la extensión pero cerrada a la modificación. El nuevo comportamiento debe llegar a través de herencia, composición o interfaces, no reescribiendo código existente.
Por qué Importa: Cada vez que abres una clase funcional para agregar una característica, arriesgas introducir bugs e invalidar tests. Al diseñar para extensión, las nuevas funcionalidades llegan en código nuevo, dejando la lógica probada intacta.
En la Práctica: En lugar de modificar una clase Logger para soportar múltiples formatos de salida, crea una clase base abstracta y deriva FileLogger, ConsoleLogger y CloudLogger. En TypeScript, usa interfaces y patrones strategy.
Señales de Alerta: Cadenas largas de if-else que verifican tipos o enums de string; comentarios diciendo “TODO: refactorizar para la próxima feature”; modificaciones al código viejo cada sprint.
3. Principio de Sustitución de Liskov (LSP)
Idea Central: Los objetos de una clase derivada deben poder reemplazar objetos de la clase base sin romper la aplicación.
Por qué Importa: Liskov asegura que las jerarquías de herencia permanezcan predecibles y seguras. Si tu código trata objetos como instancias de su tipo base, sustituir un tipo derivado nunca debería causar sorpresas.
En la Práctica: Una clase Rectangle tiene métodos SetWidth y SetHeight. Un Square podría intentar mantener sus lados iguales, violando el contrato. Liskov dice: si Square hereda de Rectangle, debe cumplir todas las promesas de Rectangle sin comportamiento sorpresivo.
Señales de Alerta: Clases derivadas que lanzan “NotImplementedException”; subclases que ignoran silenciosamente llamadas a métodos; verificaciones condicionales como if (obj instanceof SpecificType) dispersas en tu lógica.
4. Principio de Segregación de Interfaces (ISP)
Idea Central: Los clientes nunca deberían ser forzados a depender de interfaces que no usan.
Por qué Importa: Las interfaces gordas crean acoplamiento. Si una clase pequeña depende de una interfaz enorme, queda acoplada a cosas que no le importan. Segregar interfaces reduce la superficie de contacto.
En la Práctica: En lugar de una interfaz IDataProvider masiva, divídela en IReader, IWriter e ICache. Las clases que solo leen datos dependen solo de IReader.
Señales de Alerta: Interfaces con docenas de métodos; clases implementando interfaces dejando la mayoría de métodos vacíos; tests que mockean objetos enormes solo para probar una pequeña funcionalidad.
5. Principio de Inversión de Dependencias (DIP)
Idea Central: Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones.
Por qué Importa: Si tu lógica de negocio crea o llama directamente implementaciones de bajo nivel, encadenas tu lógica core a esas implementaciones. Invertir la dependencia te permite cambiar implementaciones sin tocar el código central.
En la Práctica: En lugar de un UserService que instancie directamente un SqlUserRepository, inyecta una interfaz IUserRepository. En tiempo de ejecución, pasas la implementación concreta. Esta es la base de la inyección de dependencias.
Señales de Alerta: Uso de new disperso en tu lógica de negocio; tests que requieren una base de datos real o una API para ejecutarse; código de alto nivel importando archivos de implementación de bajo nivel.
Cómo Trabajan Juntos
Los cinco principios no son reglas aisladas; se refuerzan mutuamente. SRP mantiene las clases enfocadas. OCP asegura que agregar comportamiento no requiera modificar clases existentes. LSP garantiza que los tipos derivados sean seguros. ISP evita forzar a una clase a implementar más de lo que debería. Y DIP ata todo asegurando que las políticas de alto nivel dependan solo de abstracciones.
Cuando aplicas los cinco, terminas con un código más fácil de testear, menos frágil ante cambios de requerimientos, y mucho más agradable de mantener.
Lo que Sigue
Cada artículo de esta serie toma un principio y lo recorre en profundidad. Verás errores comunes, anti-patrones y ejemplos concretos de refactorización en C# y TypeScript. El objetivo no es memorizar reglas sino desarrollar una intuición para cuando tu código empieza a doler, y cómo arreglarlo antes de que el dolor se vuelva severo.
5 artículos
Principio de Responsabilidad Única
Explica el Principio de Responsabilidad Única con ejemplos que dividen responsabilidades en unidades enfocadas y testeables para un código más claro y mantenible.
Principio Abierto/Cerrado
Muestra cómo diseñar componentes y módulos que están abiertos a la extensión pero cerrados a la modificación usando patrones prácticos de TypeScript.
Principio de Sustitución de Liskov
Demuestra la Sustitución de Liskov con ejemplos que aseguran que las subclases pueden reemplazar de forma segura los tipos base sin romper el comportamiento del sistema.
Principio de Segregación de Interfaces
Te guía a crear interfaces enfocadas para que los clientes dependan solo de lo que usan, reduciendo el acoplamiento y mejorando la flexibilidad.
Principio de Inversión de Dependencias
Explica la Inversión de Dependencias con ejemplos de inyección y abstracciones para desacoplar módulos, haciendo el código más fácil de testear y evolucionar.