# Principios SOLID ## Introducción Los principios SOLID constituyen un conjunto de criterios de diseño orientados a desarrollar software flexible, mantenible, comprensible y verificable. Su aplicación adecuada permite: - Reducir el acoplamiento innecesario. - Mejorar la cohesión de clases y módulos. - Facilitar la evolución de funcionalidades. - Proteger contratos y comportamientos existentes. - Simplificar las pruebas y el mantenimiento. - Controlar el impacto de los cambios. Estos principios deben aplicarse con criterio profesional y teniendo en cuenta las necesidades reales del sistema. **SOLID no exige utilizar siempre interfaces, herencia, patrones de diseño ni arquitecturas complejas.** El objetivo es mejorar la estructura del software, no maximizar el número de abstracciones. Este documento desarrolla los criterios establecidos en el `SKILL.md` principal. --- ## 1. S — Principio de responsabilidad única (Single Responsibility Principle, SRP) > "Una clase debería tener una, y solo una, razón para cambiar." ### 1.1. Qué problema resuelve SRP ayuda a evitar clases o módulos que acumulan responsabilidades independientes. Estas estructuras suelen presentar: - Dificultad para comprender su comportamiento. - Cambios frecuentes por motivos diferentes. - Pruebas complejas. - Dependencias excesivas. - Mayor riesgo de regresiones. - Acoplamiento entre funcionalidades que deberían evolucionar por separado. ### 1.2. Cómo aplicarlo Cada clase o módulo debe representar una responsabilidad coherente. Una responsabilidad no equivale necesariamente a un único método, operación o dato. Una clase puede realizar varias operaciones relacionadas y seguir cumpliendo SRP. **Ejemplo: responsabilidades mezcladas** ```typescript id="lzj3ab" // MAL: Mezcla reglas de negocio, persistencia y generación de documentos. class Order { calculateTotal(): Money { // Calcular el importe del pedido. } async saveToDatabase(): Promise { // Persistir el pedido. } generateInvoice(): Invoice { // Generar la factura. } } ``` En este caso, la clase puede cambiar por motivos independientes: - Modificación de reglas de cálculo. - Cambios en la persistencia. - Cambios en la generación de facturas. **Alternativa: responsabilidades diferenciadas** ```typescript id="u0b51p" class Order { private readonly items: OrderItem[] = []; addItem(item: OrderItem): void { this.items.push(item); } calculateTotal(): Money { return calculateOrderTotal(this.items); } } ``` ```typescript id="9drm12" class OrderRepository { async save(order: Order): Promise { // Persistir el pedido. } } ``` ```typescript id="e1iz3a" class InvoiceGenerator { generate(order: Order): Invoice { // Generar una factura a partir del pedido. } } ``` La entidad `Order` mantiene las operaciones relacionadas con sus reglas de negocio, mientras que la persistencia y la generación de documentos pertenecen a responsabilidades diferenciadas. Los ejemplos son ilustrativos y presuponen los tipos y colaboradores necesarios. ### 1.3. Preguntas para detectar problemas - ¿Tiene esta clase varios motivos independientes para cambiar? - ¿Sus operaciones responden a una responsabilidad coherente? - ¿Distintos requisitos obligan a modificar partes no relacionadas? - ¿La clase depende de demasiados componentes externos? - ¿Sería más sencillo probar y mantener determinadas responsabilidades por separado? ### 1.4. Evitar una interpretación excesivamente estricta No dividas una clase únicamente porque su descripción incluya la palabra «y». Por ejemplo, una entidad `Order` puede: - Añadir artículos. - Eliminar artículos. - Calcular importes. - Comprobar restricciones. - Cambiar de estado. Todas estas operaciones pueden pertenecer a una única responsabilidad: gestionar el comportamiento del pedido. **SRP busca separar motivos de cambio independientes, no reducir todas las clases a una única operación.** --- ## 2. O — Principio abierto/cerrado (Open/Closed Principle, OCP) > "Las entidades de software deberían estar abiertas a la extensión, pero cerradas a la modificación." ### 2.1. Qué problema resuelve OCP busca reducir la necesidad de modificar código estable cada vez que se incorpora una variante de comportamiento. Esto resulta especialmente útil cuando: - Existen múltiples implementaciones de una capacidad. - Se incorporan nuevas variantes con frecuencia. - Los componentes tienen contratos estables. - Modificar lógica existente supone un riesgo elevado. ### 2.2. Cómo aplicarlo Cuando exista una necesidad real de extensión, diseña contratos que permitan incorporar nuevos comportamientos sin modificar innecesariamente los consumidores. **Ejemplo: cálculo de gastos de envío** ```typescript id="e33kiw" // Implementación sencilla basada en condicionales. type ShippingType = 'standard' | 'express'; class ShippingCalculator { calculate(type: ShippingType, orderValue: number): number { if (type === 'standard') { return orderValue < 50 ? 5 : 0; } return 15; } } ``` Esta implementación no es necesariamente incorrecta. Si los tipos de envío son pocos y estables, puede ser la solución más sencilla y mantenible. Sin embargo, cuando las políticas de envío evolucionan independientemente, puede resultar conveniente introducir abstracciones. ### 2.3. Alternativa extensible ```typescript id="0kb5qt" interface ShippingMethod { calculateCost(orderValue: number): number; } ``` ```typescript id="mky8hb" class StandardShipping implements ShippingMethod { calculateCost(orderValue: number): number { return orderValue < 50 ? 5 : 0; } } ``` ```typescript id="p9o7s1" class ExpressShipping implements ShippingMethod { calculateCost(_orderValue: number): number { return 15; } } ``` Para incorporar un nuevo método: ```typescript id="bw5t7r" class SameDayShipping implements ShippingMethod { calculateCost(_orderValue: number): number { return 25; } } ``` Un consumidor puede utilizar el contrato común: ```typescript id="d8t4vk" function calculateShipping( method: ShippingMethod, orderValue: number, ): number { return method.calculateCost(orderValue); } ``` Este diseño permite incorporar nuevas políticas sin modificar el cálculo del consumidor. No obstante, el mecanismo que selecciona o registra las implementaciones puede necesitar cambios según la arquitectura utilizada. ### 2.4. Aplicación pragmática - Introduce puntos de extensión cuando exista una necesidad identificada. - Mantén estables los contratos que tengan consumidores. - Favorece composición y polimorfismo cuando reduzcan el acoplamiento. - Evita crear interfaces para comportamientos que probablemente no variarán. - No introduzcas extensibilidad hipotética. - Acepta modificaciones de código existente cuando sean la solución más clara. ### 2.5. OCP a nivel arquitectónico A nivel de arquitectura, OCP favorece la existencia de módulos con responsabilidades claras y contratos estables. Esto permite incorporar funcionalidades nuevas con un impacto controlado. Sin embargo, **no significa que toda funcionalidad nueva deba implementarse exclusivamente añadiendo archivos o clases**. Modificar código existente es legítimo cuando cambian sus requisitos o responsabilidades. El objetivo es minimizar modificaciones innecesarias en componentes estables, no prohibirlas. --- ## 3. L — Principio de sustitución de Liskov (Liskov Substitution Principle, LSP) > "Los subtipos deben poder sustituir a sus tipos base sin alterar la corrección del programa." ### 3.1. Qué problema resuelve LSP evita jerarquías o implementaciones que incumplen las expectativas establecidas por un contrato común. Cuando se vulnera este principio, suelen aparecer: - Comprobaciones del tipo concreto en los consumidores. - Condicionales especiales para determinadas implementaciones. - Errores inesperados al sustituir componentes. - Contratos inconsistentes. - Dependencias del comportamiento interno de cada implementación. ### 3.2. El contrato es más que una firma Dos implementaciones no son necesariamente sustituibles por tener los mismos métodos. La sustitución requiere respetar el comportamiento esperado, incluyendo: - Precondiciones. - Postcondiciones. - Invariantes. - Semántica de los resultados. - Comportamiento ante errores. - Garantías relevantes para los consumidores. ### 3.3. Ejemplo de incumplimiento Supongamos que el contrato establece que un descuento nunca puede ser negativo. ```typescript id="j3zfk5" interface DiscountPolicy { getDiscount(orderValue: number): number; } ``` Una implementación correcta: ```typescript id="tr7txy" class StandardDiscount implements DiscountPolicy { getDiscount(_orderValue: number): number { return 5; } } ``` Una implementación incorrecta: ```typescript id="q0laxd" class InvalidDiscount implements DiscountPolicy { getDiscount(_orderValue: number): number { return -5; } } ``` Aunque ambas cumplen la interfaz TypeScript, la segunda incumple el contrato semántico si este exige descuentos no negativos. ### 3.4. Protección del contrato El contrato puede expresarse mediante documentación, tipos, validaciones y pruebas. ```typescript id="e1iu3o" class FixedDiscount implements DiscountPolicy { constructor(private readonly amount: number) { if (!Number.isFinite(amount) || amount < 0) { throw new InvalidDiscountError(); } } getDiscount(_orderValue: number): number { return this.amount; } } ``` La validación impide crear una política de descuento fijo con un importe negativo. No obstante, validar únicamente esta clase no garantiza que todas las implementaciones de `DiscountPolicy` cumplan el contrato. Para ello pueden utilizarse pruebas de contrato compartidas y validaciones en los límites apropiados. ### 3.5. Reglas de sustitución Una implementación sustitutiva debe: - Respetar las invariantes del contrato. - No exigir precondiciones más estrictas que las comprometidas por el tipo base. - No proporcionar postcondiciones más débiles. - Mantener el significado de los resultados. - Respetar las garantías de error relevantes. - No introducir efectos secundarios incompatibles con el contrato. ### 3.6. Ejemplo con repositorios ```typescript id="60hovx" interface UserRepository { findById(id: UserId): Promise; save(user: User): Promise; } ``` Las implementaciones: ```typescript id="kp45w8" class InMemoryUserRepository implements UserRepository { // Implementación en memoria. } ``` ```typescript id="q2v45i" class PostgresUserRepository implements UserRepository { // Implementación sobre PostgreSQL. } ``` Pueden sustituirse cuando respetan el mismo contrato funcional. Esto incluye: - Significado de `null` cuando no existe el usuario. - Garantías de persistencia. - Restricciones de integridad. - Tratamiento de errores. - Comportamientos relevantes para los consumidores. **Compartir una interfaz no garantiza automáticamente la sustituibilidad.** --- ## 4. I — Principio de segregación de interfaces (Interface Segregation Principle, ISP) > "Los clientes no deberían verse obligados a depender de métodos que no utilizan." ### 4.1. Qué problema resuelve ISP evita interfaces excesivamente amplias que obligan a los consumidores o implementaciones a conocer capacidades innecesarias. Los síntomas habituales incluyen: - Interfaces con numerosas operaciones independientes. - Implementaciones parciales. - Métodos vacíos. - Excepciones de funcionalidad no soportada. - Dependencias innecesarias. - Cambios en interfaces que afectan a consumidores no relacionados. ### 4.2. Ejemplo problemático ```typescript id="d9qvm1" interface WarehouseDevice { printLabel(orderId: string): void; scanBarcode(): string; packageItem(orderId: string): void; } ``` Una impresora sencilla no debería verse obligada a implementar operaciones que no admite. ```typescript id="z83zrv" class BasicPrinter implements WarehouseDevice { printLabel(_orderId: string): void { // Imprimir etiqueta. } scanBarcode(): string { throw new Error('Operación no soportada'); } packageItem(_orderId: string): void { throw new Error('Operación no soportada'); } } ``` ### 4.3. Alternativa con interfaces cohesionadas ```typescript id="80gu9m" interface LabelPrinter { printLabel(orderId: string): void; } ``` ```typescript id="e4lqq1" interface BarcodeScanner { scanBarcode(): string; } ``` ```typescript id="a113zj" interface ItemPackager { packageItem(orderId: string): void; } ``` Una impresora implementa únicamente su capacidad real: ```typescript id="b9ksk8" class BasicPrinter implements LabelPrinter { printLabel(_orderId: string): void { // Imprimir etiqueta. } } ``` Los consumidores pueden depender de las capacidades que necesitan. ### 4.4. Aplicación pragmática - Diseña interfaces alrededor de necesidades reales de los consumidores. - Agrupa operaciones que representen una responsabilidad coherente. - Divide interfaces cuando obliguen a depender de capacidades no utilizadas. - Evita crear una interfaz independiente para cada método sin una razón clara. - No fragmentes contratos cohesionados únicamente para reducir su tamaño. ### 4.5. Señales de revisión Revisa una interfaz cuando: - Varias implementaciones contienen métodos vacíos. - Algunas operaciones lanzan excepciones de no implementación de forma sistemática. - Los consumidores utilizan subconjuntos completamente independientes. - Los cambios afectan a componentes que no necesitan la funcionalidad modificada. Sin embargo, un método opcional o una operación no soportada no demuestra por sí solo una vulneración de ISP. Evalúa el contrato y el contexto antes de modificarlo. **ISP trata de reducir dependencias innecesarias, no de maximizar el número de interfaces.** --- ## 5. D — Principio de inversión de dependencias (Dependency Inversion Principle, DIP) > "Los módulos de alto nivel no deberían depender de los módulos de bajo nivel. Ambos deberían depender de abstracciones." ### 5.1. Qué problema resuelve DIP busca evitar que las reglas de negocio queden acopladas innecesariamente a detalles técnicos. Ejemplos: - Bases de datos concretas. - Proveedores de correo electrónico. - APIs externas. - Sistemas de almacenamiento. - Frameworks. - Implementaciones de infraestructura. Este acoplamiento puede dificultar las pruebas, la evolución o la sustitución de componentes. ### 5.2. Ejemplo de dependencia concreta ```typescript id="r5k4a2" class OrderService { private readonly emailService = new SendGridEmailService(); async confirmOrder(email: string): Promise { await this.emailService.send(email, 'Pedido confirmado'); } } ``` La clase conoce directamente al proveedor de correo electrónico. Si cambia el proveedor, puede ser necesario modificar el servicio de aplicación. ### 5.3. Dependencia de una abstracción Define un contrato que represente la capacidad necesaria. ```typescript id="k8p2ag" interface EmailService { send(to: string, message: string): Promise; } ``` El servicio depende del contrato: ```typescript id="5g7pvd" class OrderService { constructor( private readonly emailService: EmailService, ) {} async confirmOrder(email: string): Promise { await this.emailService.send(email, 'Pedido confirmado'); } } ``` Las implementaciones concretas pueden variar: ```typescript id="gcl3yt" const sendGridOrderService = new OrderService( new SendGridEmailService(), ); ``` ```typescript id="k85qhq" const sesOrderService = new OrderService( new SesEmailService(), ); ``` En una prueba puede utilizarse una implementación controlada: ```typescript id="f4yb6z" const testOrderService = new OrderService( new FakeEmailService(), ); ``` El servicio de aplicación no necesita conocer los detalles internos de cada proveedor. ### 5.4. Inversión de dependencias e inyección de dependencias Son conceptos relacionados, pero diferentes. **Inversión de dependencias (DIP):** Principio de diseño que establece cómo deben organizarse las dependencias entre políticas y detalles. **Inyección de dependencias (DI):** Técnica mediante la cual un componente recibe sus colaboradores desde el exterior. Puede realizarse mediante: - Constructores. - Parámetros. - Factorías. - Composición explícita. - Contenedores de dependencias. **Aplicar DIP no exige utilizar un contenedor de inyección de dependencias.** ### 5.5. Evitar abstracciones innecesarias No es obligatorio crear una interfaz para cada clase. Una dependencia concreta puede resultar apropiada cuando: - El componente es sencillo y estable. - No existe una necesidad real de sustitución. - No introduce acoplamiento arquitectónico problemático. - La abstracción adicional no aporta claridad ni capacidad de evolución. - Se trata de un objeto de valor, una utilidad pura u otro componente concreto cuyo contrato es suficientemente estable. La dependencia de abstracciones debe justificarse por su utilidad, no por el cumplimiento mecánico de DIP. ### 5.6. Regla de dependencias arquitectónicas En arquitecturas limpias, hexagonales o equivalentes, las dependencias del código fuente deben orientarse hacia las políticas de alto nivel. Representación simplificada: ```text id="y99n58" Infraestructura → Aplicación → Dominio exterior medio interior ``` Esta representación expresa la dirección deseada de las dependencias en una arquitectura organizada de esa forma. En la práctica, la infraestructura puede implementar contratos definidos en la aplicación o el dominio. Lo importante es que las reglas de negocio no dependan innecesariamente de los detalles de infraestructura. ### 5.7. Aplicación pragmática - Mantén independientes las reglas de negocio respecto a detalles técnicos cuando resulte necesario. - Define contratos en los límites donde aporten desacoplamiento. - Utiliza inyección de dependencias cuando facilite la composición o las pruebas. - No introduzcas interfaces y adaptadores para todas las operaciones. - Evita contenedores complejos en aplicaciones que puedan resolverse mediante composición directa. - Respeta la arquitectura existente salvo que exista una razón técnica o funcional para modificarla. --- ## 6. Aplicación de SOLID a nivel arquitectónico Los principios SOLID pueden orientar el diseño de módulos, subsistemas y límites arquitectónicos. No obstante, su aplicación a nivel de arquitectura requiere interpretar correctamente cada principio. | Principio | Aplicación arquitectónica | |---|---| | **SRP** | Módulos y subsistemas con responsabilidades cohesionadas y motivos de cambio relacionados. | | **OCP** | Contratos y puntos de extensión que permiten incorporar capacidades sin modificar innecesariamente componentes estables. | | **LSP** | Implementaciones sustituibles que respetan los contratos funcionales y no funcionales relevantes. | | **ISP** | Interfaces y contratos entre módulos adaptados a las necesidades reales de sus consumidores. | | **DIP** | Separación entre políticas de negocio y detalles técnicos mediante dependencias orientadas adecuadamente. | ### 6.1. SRP en módulos y subsistemas Un módulo puede contener numerosas clases y operaciones siempre que representen una responsabilidad coherente. No debe interpretarse que cada módulo o contexto delimitado (*bounded context*) tenga que realizar una única operación. ### 6.2. OCP en arquitectura Favorece módulos estables que puedan ampliarse mediante nuevos componentes cuando exista una necesidad real de extensión. No significa que las funcionalidades nuevas deban implementarse exclusivamente añadiendo módulos. ### 6.3. LSP en contratos distribuidos En sistemas distribuidos, compartir un contrato técnico no garantiza sustituibilidad. Deben considerarse también: - Semántica de las operaciones. - Garantías de consistencia. - Tratamiento de errores. - Idempotencia cuando corresponda. - Compatibilidad de versiones. - Restricciones operativas relevantes. Dos microservicios que exponen la misma API no son necesariamente intercambiables. ### 6.4. ISP entre módulos Los contratos deben exponer las capacidades necesarias sin obligar a los consumidores a depender de funcionalidades ajenas. Esto no exige crear un servicio o API independiente para cada operación. ### 6.5. DIP en arquitectura Las reglas de negocio deben mantenerse desacopladas de detalles de infraestructura cuando esa separación facilite la evolución del sistema. La arquitectura concreta debe adaptarse a los requisitos y complejidad reales. **SOLID no obliga a utilizar Clean Architecture, arquitectura hexagonal o microservicios en cualquier proyecto.** --- ## 7. Errores habituales al aplicar SOLID ### Aplicar SRP como "una clase, un método" Una responsabilidad puede incluir numerosas operaciones relacionadas. Dividirlas indiscriminadamente puede reducir la cohesión. ### Aplicar OCP mediante abstracciones especulativas No crees extensibilidad para requisitos hipotéticos. Introduce puntos de extensión cuando exista una necesidad identificada. ### Considerar LSP únicamente como compatibilidad de tipos Compartir una interfaz no garantiza cumplir el mismo contrato semántico. Comprueba invariantes, resultados y expectativas. ### Aplicar ISP creando demasiadas interfaces Las interfaces deben representar capacidades coherentes. No dividas contratos únicamente para reducir el número de métodos. ### Aplicar DIP mediante interfaces para todas las clases DIP no consiste en prohibir dependencias concretas. Consiste en evitar que las políticas importantes dependan innecesariamente de detalles técnicos. ### Introducir patrones sin necesidad SOLID no exige utilizar automáticamente: - Repositorios. - Factorías. - Estrategias. - Adaptadores. - Contenedores de dependencias. - Jerarquías de herencia. Utiliza estos mecanismos cuando resuelvan un problema concreto. ### Refactorizar código estable sin justificación No modifiques una implementación que funciona correctamente únicamente para ajustarla a una interpretación más rígida de SOLID. Evalúa el beneficio esperado, el riesgo y el alcance de la tarea. --- ## 8. Referencia rápida | Principio | Idea fundamental | Señal de revisión | |---|---|---| | **SRP** | Una responsabilidad coherente y motivos de cambio relacionados. | Clase que acumula responsabilidades independientes. | | **OCP** | Permitir extensiones justificadas sin modificar innecesariamente código estable. | Variantes frecuentes que obligan a cambiar múltiples componentes. | | **LSP** | Las implementaciones deben respetar el contrato común. | Comportamientos incompatibles o comprobaciones especiales por tipo. | | **ISP** | Los consumidores dependen únicamente de las capacidades necesarias. | Interfaces amplias con operaciones irrelevantes o implementaciones parciales. | | **DIP** | Evitar dependencias innecesarias entre políticas de alto nivel y detalles técnicos. | Reglas de negocio fuertemente acopladas a infraestructura concreta. | --- ## 9. Lista de comprobación SOLID ### Responsabilidad única (SRP) - [ ] ¿Cada clase o módulo tiene una responsabilidad coherente? - [ ] ¿Existen motivos de cambio independientes que justifiquen una separación? - [ ] ¿He evitado fragmentar responsabilidades relacionadas? - [ ] ¿La organización facilita comprender y mantener el código? ### Abierto/cerrado (OCP) - [ ] ¿Existe una necesidad real de extensión? - [ ] ¿Puedo incorporar variantes sin modificar innecesariamente código estable? - [ ] ¿Las abstracciones introducidas están justificadas? - [ ] ¿He evitado diseñar para requisitos hipotéticos? ### Sustitución de Liskov (LSP) - [ ] ¿Las implementaciones respetan el contrato común? - [ ] ¿Se mantienen las invariantes? - [ ] ¿Las precondiciones y postcondiciones son compatibles? - [ ] ¿Los consumidores pueden utilizar otra implementación sin tratamientos especiales? - [ ] ¿Las diferencias de comportamiento están correctamente definidas? ### Segregación de interfaces (ISP) - [ ] ¿Cada interfaz representa capacidades coherentes? - [ ] ¿Los consumidores dependen de operaciones que realmente necesitan? - [ ] ¿Existen implementaciones forzadas a soportar operaciones irrelevantes? - [ ] ¿He evitado fragmentar interfaces artificialmente? ### Inversión de dependencias (DIP) - [ ] ¿Las reglas de negocio dependen innecesariamente de detalles técnicos? - [ ] ¿Las abstracciones están situadas en límites apropiados? - [ ] ¿La inyección de dependencias aporta ventajas concretas? - [ ] ¿He evitado interfaces y adaptadores innecesarios? - [ ] ¿Se respeta la arquitectura existente y sus contratos? --- ## Principio final > "Los principios SOLID deben facilitar el cambio, reducir el acoplamiento y mejorar la comprensión del software, no convertirse en una obligación de introducir más clases e interfaces." Aplica SOLID con criterio profesional, considerando las necesidades del dominio, el tamaño del sistema, sus requisitos de evolución y el coste de mantenimiento. **Una solución sencilla, cohesionada y correctamente desacoplada es preferible a una arquitectura artificialmente compleja que únicamente aparenta cumplir SOLID.**