# Diseño orientado a objetos (Object-Oriented Design) El diseño orientado a objetos permite organizar el software mediante componentes que encapsulan estado, comportamiento y responsabilidades. Su objetivo es facilitar la comprensión, evolución y mantenimiento del sistema mediante objetos cohesionados, contratos claros y dependencias controladas. Este documento desarrolla los principios establecidos en el `SKILL.md` principal y debe interpretarse siguiendo sus criterios de aplicación. **El objetivo no es maximizar el número de clases, interfaces o patrones, sino encontrar una organización sencilla y coherente con el dominio y los requisitos reales.** ## 1. Diseño guiado por responsabilidades (Responsibility-Driven Design, RDD) La idea fundamental del diseño guiado por responsabilidades es: > "Los objetos se definen principalmente por sus responsabilidades, no por los datos que almacenan." Una responsabilidad describe aquello que un objeto conoce, hace o decide dentro del sistema. ### 1.1. Identificación de objetos Al analizar requisitos, utiliza las siguientes pistas: 1. **Sustantivos:** pueden sugerir entidades, objetos de valor o conceptos del dominio. 2. **Verbos:** pueden sugerir operaciones, comportamientos o casos de uso. 3. **Conceptos del dominio:** pueden justificar objetos con identidad, objetos de valor o servicios especializados. Por ejemplo, ante el requisito: "Un cliente puede realizar un pedido que contiene productos y al que se pueden aplicar descuentos." Podrían identificarse: - `Customer`: cliente. - `Order`: pedido. - `OrderItem`: línea de pedido. - `Product`: producto. - `DiscountPolicy`: política de descuentos. Sin embargo, **no conviertas automáticamente cada sustantivo en una clase ni cada verbo en un método**. Comprueba que los elementos identificados representan conceptos reales y responsabilidades necesarias. ### 1.2. Identificación de responsabilidades Para cada objeto, considera: - **¿Qué conoce?** Información o estado del que es responsable. - **¿Qué hace?** Operaciones o comportamientos que proporciona. - **¿Qué decide?** Reglas o decisiones que le corresponden. - **¿Qué invariantes protege?** Condiciones que deben mantenerse válidas. - **¿De quién depende?** Colaboradores necesarios para cumplir su responsabilidad. El objetivo es asignar cada comportamiento al componente que tenga la información y responsabilidad adecuadas. ### 1.3. Estereotipos de objetos Los estereotipos permiten identificar responsabilidades habituales. | Estereotipo | Propósito | Ejemplo | |---|---|---| | **Information Holder (Contenedor de información)** | Mantiene información o estado del dominio. | `User`, `Product`, `Address` | | **Structurer (Estructurador)** | Gestiona relaciones entre objetos. | `OrderItems`, `UserGroup` | | **Service Provider (Proveedor de servicios)** | Ejecuta operaciones o proporciona capacidades específicas. | `PaymentProcessor`, `EmailSender` | | **Coordinator (Coordinador)** | Orquesta procesos y la colaboración entre componentes. | `OrderFulfillmentService` | | **Controller (Controlador)** | Recibe solicitudes, gestiona decisiones de control y delega operaciones. | `CheckoutController` | | **Interfacer (Adaptador de interfaces)** | Adapta información o contratos entre sistemas. | `UserApiAdapter`, `DatabaseMapper` | Estos estereotipos son orientativos. Una clase puede combinar responsabilidades compatibles sin que ello implique necesariamente un problema de diseño. Tampoco es obligatorio que toda clase encaje perfectamente en una de estas categorías. ### 1.4. Preguntas fundamentales Para cada clase, módulo o componente, pregúntate: 1. ¿Cuál es su responsabilidad principal? 2. ¿Qué motivos justificarían modificarlo? 3. ¿Está asumiendo responsabilidades independientes? 4. ¿Sus dependencias son necesarias? 5. ¿Existe una organización más sencilla? 6. ¿Representa correctamente un concepto o comportamiento del dominio? Si las respuestas no resultan claras, revisa el diseño. **No refactorices automáticamente una clase porque no corresponda a un patrón conocido o porque supere una métrica numérica.** --- ## 2. Tell, Don't Ask (Ordena, no preguntes) Este principio propone delegar el comportamiento en los objetos responsables, en lugar de consultar su estado interno y tomar decisiones desde el exterior. ### Ejemplo: retirada de dinero **Menos adecuado: lógica de negocio fuera del objeto** ```typescript // MAL: El consumidor modifica directamente el saldo. if (account.getBalance() >= amount) { account.setBalance(account.getBalance() - amount); } ``` Este enfoque permite que distintos consumidores implementen reglas diferentes y puede comprometer las invariantes de la cuenta. **Más adecuado: comportamiento encapsulado** ```typescript const result = account.withdraw(amount); if (result.isSuccess()) { // Continuar con la operación. } ``` La cuenta es responsable de comprobar si la retirada está permitida y actualizar su estado correctamente. ### Criterios de aplicación - Encapsula las reglas de negocio junto al estado que deben proteger. - Evita que los consumidores reproduzcan decisiones propias de un objeto. - Expón operaciones que expresen intenciones del dominio. - Reduce la necesidad de consultar y modificar repetidamente el estado interno. ### Excepciones legítimas No apliques *Tell, Don't Ask* indiscriminadamente. Consultar datos es adecuado cuando se trabaja con: - DTO. - Modelos de presentación. - Respuestas de API. - Objetos de configuración. - Estructuras destinadas al transporte de datos. - Consultas o proyecciones de solo lectura. **El principio debe proteger el encapsulamiento y las invariantes, no impedir el acceso legítimo a la información.** --- ## 3. Diseño por contrato (Design by Contract, DbC) El diseño por contrato establece las condiciones que deben cumplirse antes, durante y después de ejecutar una operación. ### Elementos de un contrato **Precondiciones:** condiciones que deben cumplirse antes de ejecutar una operación. **Postcondiciones:** garantías que proporciona una operación al completarse correctamente. **Invariantes:** condiciones que deben mantenerse válidas durante la vida observable del objeto. ### Ejemplo ```typescript class BankAccount { constructor(private balance: Money) { if (balance.isNegative()) { throw new InvalidInitialBalanceError(); } } withdraw(amount: Money): WithdrawResult { if (!amount.isPositive()) { return WithdrawResult.invalidAmount(); } if (this.balance.isLessThan(amount)) { return WithdrawResult.insufficientFunds(); } this.balance = this.balance.subtract(amount); return WithdrawResult.success(this.balance); } } ``` En este ejemplo: - **Invariante:** el saldo nunca puede ser negativo. - **Precondición de éxito:** el importe debe ser positivo y no superar el saldo disponible. - **Postcondición de éxito:** el saldo disminuye exactamente en el importe retirado. - **Resultado de rechazo:** se devuelve un resultado que identifica el motivo sin modificar el saldo. El ejemplo presupone que `Money` impide operaciones entre monedas incompatibles y utiliza una representación monetaria adecuada. ### Criterios de diseño por contrato - Define las invariantes relevantes del dominio. - Valida las precondiciones en los límites apropiados. - Evita que las operaciones dejen objetos en estados inválidos. - Expresa claramente los resultados esperados. - Utiliza tipos para representar restricciones cuando sea apropiado. - No dupliques validaciones innecesariamente en todas las capas. - Distingue entre errores excepcionales y rechazos de negocio esperados. No es obligatorio documentar expresamente cada precondición y postcondición cuando el contrato resulte evidente mediante los tipos, nombres y pruebas. ### Contratos y compatibilidad Al modificar una operación existente: - Identifica sus consumidores. - Mantén las garantías previamente establecidas. - No debilites invariantes sin una justificación funcional. - Evita cambios silenciosos en el tratamiento de errores. - Comprueba el principio de sustitución de Liskov cuando exista herencia o implementación de interfaces. --- ## 4. Composición frente a herencia (Composition Over Inheritance) La composición permite construir comportamientos mediante la colaboración entre objetos. La herencia permite especializar tipos y reutilizar contratos o comportamientos comunes. **Prefiere composición cuando aporte mayor flexibilidad, menor acoplamiento o una representación más adecuada del dominio.** ### 4.1. Riesgos de la herencia - Acoplamiento entre clases base y derivadas. - Dependencia de detalles de implementación heredados. - Dificultad para modificar clases base sin afectar a sus derivados. - Jerarquías difíciles de comprender. - Relaciones artificiales de tipo "es un". - Problemas de sustitución cuando las clases derivadas alteran contratos. Estos riesgos no implican que la herencia sea incorrecta por definición. ### 4.2. Cuándo utilizar herencia Puede ser apropiada cuando: - Existe una relación conceptual real de especialización. - Las clases derivadas cumplen el contrato del tipo base. - El framework requiere una jerarquía concreta. - Se utiliza deliberadamente un patrón como *Template Method*. - La reutilización y extensión del comportamiento justifican la estructura. No utilices herencia únicamente para compartir unas pocas líneas de implementación. ### 4.3. Composición mediante políticas **Ejemplo basado en herencia:** ```typescript class PremiumUser extends User { getDiscount(): number { return 20; } } ``` Este diseño puede resultar válido, pero vincula el comportamiento del descuento con la jerarquía de usuarios. **Alternativa mediante composición:** ```typescript interface DiscountPolicy { calculate(): number; } ``` ```typescript class User { constructor( private readonly discountPolicy: DiscountPolicy, ) {} getDiscount(): number { return this.discountPolicy.calculate(); } } ``` ```typescript class PremiumDiscount implements DiscountPolicy { calculate(): number { return 20; } } class StandardDiscount implements DiscountPolicy { calculate(): number { return 5; } } class NoDiscount implements DiscountPolicy { calculate(): number { return 0; } } ``` ```typescript const premiumUser = new User(new PremiumDiscount()); const standardUser = new User(new StandardDiscount()); const regularUser = new User(new NoDiscount()); ``` La composición permite variar la política sin crear nuevas subclases de usuario. ### Criterios para elegir - Utiliza composición cuando necesites combinar o sustituir comportamientos independientes. - Utiliza herencia cuando exista una especialización legítima con sustitución segura. - No introduzcas una interfaz y varias clases si un condicional sencillo resuelve adecuadamente un comportamiento estable. - Evita crear estrategias intercambiables para variaciones hipotéticas. - Considera funciones y objetos sencillos como alternativas a jerarquías complejas. **La composición es una preferencia de diseño, no una prohibición de utilizar herencia.** --- ## 5. Ley de Deméter (Law of Demeter) La Ley de Deméter, también conocida como principio de mínimo conocimiento, propone limitar las dependencias de un componente respecto a las estructuras internas de otros. De manera orientativa, un método debería colaborar principalmente con: 1. El propio objeto (`this`). 2. Los parámetros recibidos. 3. Los objetos que crea. 4. Sus colaboradores directos. ### Ejemplo **Acoplamiento a estructuras internas:** ```typescript const city = order.getCustomer().getAddress().getCity(); ``` Este código conoce la organización interna del pedido, el cliente y su dirección. **Alternativa con una operación de dominio:** ```typescript const city = order.getShippingCity(); ``` La segunda alternativa puede reducir el acoplamiento cuando la ciudad de envío sea una responsabilidad del pedido. ### Interpretación correcta La Ley de Deméter no significa que deba existir un único punto por línea. No prohíbe: - APIs fluidas. - Encadenamiento de operaciones sobre colecciones. - Acceso a estructuras de datos públicas. - Navegación legítima por DTO. - Composición de operaciones sobre valores inmutables. ### Criterios de aplicación - Evita que una clase dependa de detalles internos innecesarios de sus colaboradores. - Prefiere operaciones que expresen comportamientos del dominio. - No añadas métodos delegadores triviales únicamente para ocultar una cadena de acceso. - Evalúa si la navegación introduce un acoplamiento real. - Prioriza la claridad y la estabilidad de los contratos. --- ## 6. Encapsulamiento (Encapsulation) El encapsulamiento consiste en controlar el acceso al estado y a los detalles internos de un componente, exponiendo únicamente los contratos necesarios. ### 6.1. Niveles de encapsulamiento 1. **Datos:** controlar el acceso y modificación del estado. 2. **Implementación:** ocultar los mecanismos internos. 3. **Tipos:** desacoplar consumidores de implementaciones concretas cuando sea necesario. 4. **Diseño:** ocultar decisiones arquitectónicas que no deban afectar a los consumidores. No todos los componentes necesitan aplicar todos estos niveles. ### 6.2. Protección de invariantes **Ejemplo problemático:** ```typescript class Order { public items: Item[] = []; public total = 0; } ``` Un consumidor puede modificar el estado sin respetar las reglas del pedido. ```typescript order.items.push(item); order.total = -999; ``` **Alternativa encapsulada:** ```typescript class Order { private readonly items: Item[] = []; private total: Money; constructor() { this.total = Money.zero('EUR'); } addItem(item: Item): void { const nextTotal = this.total.add(item.total); this.validateTotal(nextTotal); this.items.push(item); this.total = nextTotal; } getTotal(): Money { return this.total; } private validateTotal(total: Money): void { if (total.isNegative()) { throw new InvalidOrderTotalError(); } } } ``` Este ejemplo presupone que `Money` es inmutable y que las operaciones utilizadas preservan sus invariantes. ### Consideraciones - Mantén privadas las estructuras internas cuyo acceso pueda comprometer invariantes. - Expón operaciones de dominio para modificar estados protegidos. - Utiliza propiedades de solo lectura cuando sea adecuado. - Evita devolver referencias mutables que permitan modificar indirectamente el estado. - No introduzcas getters o métodos delegadores innecesarios. - No encapsules indiscriminadamente datos cuyo propósito legítimo sea ser públicos. ### Encapsulamiento e inmutabilidad La inmutabilidad facilita razonar sobre el estado y evita modificaciones inesperadas. Prefiere valores inmutables cuando resulte apropiado, especialmente para objetos de valor. No obstante, las entidades y otros objetos pueden mantener estado mutable siempre que las modificaciones estén controladas. --- ## 7. Polimorfismo (Polymorphism) El polimorfismo permite utilizar distintas implementaciones mediante un contrato común. Puede mejorar la extensibilidad cuando existen comportamientos alternativos bien definidos. **No debe utilizarse indiscriminadamente para sustituir todos los condicionales.** ### 7.1. Implementación mediante condicionales ```typescript type ShippingType = 'standard' | 'express' | 'overnight'; function calculateShipping( method: ShippingType, orderValue: number, ): number { if (method === 'standard') { return orderValue < 50 ? 5 : 0; } if (method === 'express') { return 15; } return 25; } ``` Esta implementación es sencilla y puede ser perfectamente adecuada cuando los métodos de envío son pocos y estables. ### 7.2. Alternativa polimórfica Cuando los métodos de envío incorporan reglas propias y evolucionan independientemente, puede resultar útil definir un contrato. Las implementaciones canónicas de `StandardShipping`, `ExpressShipping` y el consumidor `calculateShipping` se mantienen en [principios-solid.md](principios-solid.md), sección 2.3. Aquí se muestra únicamente una variante adicional para evitar mantener copias del mismo código. ```typescript interface ShippingMethod { calculateCost(orderValue: number): number; } ``` ```typescript class OvernightShipping implements ShippingMethod { calculateCost(_orderValue: number): number { return 25; } } ``` ### 7.3. Cuándo utilizar polimorfismo Considera este enfoque cuando: - Existan varias implementaciones con comportamiento significativo. - Cada alternativa tenga reglas propias. - Sea necesario incorporar nuevos comportamientos sin modificar consumidores. - Los condicionales relacionados aparezcan repetidamente. - Las implementaciones deban probarse o evolucionar de forma independiente. Evítalo cuando: - El comportamiento sea muy sencillo. - Las alternativas sean pocas y estables. - La jerarquía incremente innecesariamente el número de componentes. - El contrato común sea artificial. - Una función o un mapa de operaciones proporcione una solución más clara. **El polimorfismo debe reducir el acoplamiento o simplificar la evolución, no limitarse a eliminar estructuras `if` o `switch`.** --- ## 8. Objetos de valor y entidades (Value Objects vs Entities) Los objetos de valor y las entidades representan conceptos diferentes dentro del modelo de dominio. ### 8.1. Objetos de valor (Value Objects) Los objetos de valor: - Se definen por sus atributos y significado. - No poseen una identidad independiente relevante para el dominio. - Deben ser inmutables desde la perspectiva de sus consumidores. - Se comparan por sus valores. - Pueden encapsular validaciones, invariantes y operaciones. Ejemplos habituales: - `Money` - `Email` - `Address` - `DateRange` ### Ejemplo: Money ```typescript class Money { constructor( private readonly amount: number, private readonly currency: string, ) { if (!Number.isFinite(amount)) { throw new InvalidMoneyAmountError(); } } equals(other: Money): boolean { return ( this.amount === other.amount && this.currency === other.currency ); } add(other: Money): Money { if (this.currency !== other.currency) { throw new CurrencyMismatchError(); } return new Money( this.amount + other.amount, this.currency, ); } } ``` Este ejemplo ilustra la igualdad por valor y la inmutabilidad, pero no constituye una implementación monetaria completa. En sistemas de producción, evita errores de precisión monetaria utilizando una representación adecuada, como unidades mínimas enteras o tipos decimales. ### Criterios para utilizar objetos de valor - Introduce objetos de valor cuando exista semántica de dominio. - Encapsula validaciones e invariantes relevantes. - Evita utilizar tipos primitivos intercambiables cuando puedan provocar errores. - No crees objetos de valor para cualquier cadena o número. - Considera tipos nominales de TypeScript cuando únicamente necesites diferenciar identificadores. - No introduzcas abstracciones que no aporten seguridad, comportamiento o expresividad. ### 8.2. Entidades (Entities) Las entidades: - Poseen una identidad que permanece aunque cambien sus atributos. - Normalmente permiten modificar su estado mediante operaciones controladas. - Se comparan por identidad cuando corresponde. - Pueden encapsular reglas e invariantes del dominio. Ejemplos habituales: - `User` - `Order` - `Product` ### Ejemplo: User ```typescript class User { constructor( private readonly id: UserId, private email: Email, private name: Name, ) {} equals(other: User): boolean { return this.id.equals(other.id); } changeEmail(newEmail: Email): void { this.email = newEmail; } } ``` El usuario conserva su identidad aunque cambie su dirección de correo electrónico. La operación `changeEmail` podría incorporar reglas adicionales, como comprobar restricciones de negocio, según las responsabilidades del modelo. ### Diferencia fundamental | Característica | Objeto de valor | Entidad | |---|---|---| | Identidad propia | No | Sí | | Comparación | Por valor | Por identidad | | Mutabilidad | Preferentemente inmutable | Puede modificar su estado | | Responsabilidad | Representar un concepto mediante sus valores | Representar un concepto con continuidad e identidad | | Ejemplos | `Money`, `Email` | `User`, `Order` | No conviertas automáticamente todos los datos en entidades u objetos de valor. Las estructuras de transporte de datos también tienen un papel legítimo dentro de una arquitectura. --- ## 9. Agregados (Aggregates) Un agregado es un conjunto de entidades y objetos de valor que se trata como una unidad de consistencia dentro de un dominio. Este concepto procede del diseño guiado por el dominio (*Domain-Driven Design*, DDD). ### 9.1. Elementos de un agregado **Raíz del agregado (Aggregate Root):** - Constituye el punto principal de acceso al agregado. - Protege sus invariantes. - Controla las modificaciones de los elementos internos. - Define los límites de consistencia. **Elementos internos:** - Pueden incluir entidades y objetos de valor. - Participan en las reglas del agregado. - No deberían modificarse desde el exterior eludiendo la raíz. ### 9.2. Ejemplo: pedido Supongamos que un pedido tiene un importe máximo permitido. La raíz del agregado debe impedir que una operación deje el pedido en un estado inválido. ```typescript class Order { private items: OrderItem[] = []; addItem(product: Product, quantity: number): void { const item = new OrderItem(product, quantity); const nextItems = [...this.items, item]; const nextTotal = this.calculateTotal(nextItems); this.validateTotal(nextTotal); this.items = nextItems; } removeItem(itemId: ItemId): void { const nextItems = this.items.filter( (item) => !item.id.equals(itemId), ); const nextTotal = this.calculateTotal(nextItems); this.validateTotal(nextTotal); this.items = nextItems; } private calculateTotal(items: OrderItem[]): Money { return items.reduce( (total, item) => total.add(item.total), Money.zero('EUR'), ); } private validateTotal(total: Money): void { if (total.isGreaterThan(MAX_ORDER_VALUE)) { throw new OrderTotalExceededError(); } } } ``` El ejemplo presupone que los importes utilizan una moneda compatible y que `OrderItem` valida la cantidad y calcula correctamente su importe. ### 9.3. Preservación de invariantes Observa que el ejemplo calcula el nuevo estado antes de modificar la colección. El orden es importante: 1. Preparar el estado propuesto. 2. Calcular sus consecuencias. 3. Validar las invariantes. 4. Aplicar la modificación. De este modo, si la validación falla, el estado del pedido permanece sin cambios. **No modifiques primero el agregado para validar después, salvo que exista un mecanismo fiable que garantice la reversión del estado.** ### 9.4. Acceso a través de la raíz **Incorrecto: modificación externa que evita las reglas** ```typescript order.items.push(new OrderItem(product, 2)); ``` Este acceso no debe estar disponible si permite incumplir las invariantes del agregado. **Correcto: operación controlada** ```typescript order.addItem(product, 2); ``` La raíz aplica las reglas necesarias antes de modificar el estado. ### 9.5. Criterios para diseñar agregados - Define agregados alrededor de invariantes que deban mantenerse consistentes. - Utiliza raíces de agregado cuando exista una necesidad real de controlar modificaciones conjuntas. - Evita agregados excesivamente grandes. - No incluyas entidades dentro de un mismo agregado únicamente porque estén relacionadas. - Considera el coste transaccional y la concurrencia. - Mantén referencias entre agregados mediante identificadores cuando sea apropiado. - Distingue las operaciones que necesitan consistencia inmediata de las que admiten consistencia eventual. - No introduzcas agregados DDD para modelos sencillos que no necesiten este nivel de organización. ### 9.6. Agregados y persistencia La raíz del agregado controla las invariantes de dominio, pero no garantiza por sí sola la atomicidad de la persistencia. Cuando sea necesario: - Utiliza transacciones. - Considera mecanismos de control de concurrencia. - Evita modificaciones parciales de datos persistidos. - Define explícitamente los límites de consistencia. - Gestiona los conflictos entre operaciones concurrentes. **La consistencia del agregado requiere tanto un modelo de dominio correcto como una estrategia de persistencia adecuada.** --- ## 10. Responsabilidades y servicios No todas las operaciones pertenecen naturalmente a una entidad. Algunas responsabilidades necesitan coordinar varios objetos o interactuar con servicios externos. ### Servicios de dominio Representan operaciones del dominio que no pertenecen claramente a una única entidad u objeto de valor. Deben contener comportamiento de negocio y evitar dependencias innecesarias de infraestructura. ### Servicios de aplicación Orquestan casos de uso. Pueden encargarse de: - Coordinar entidades y servicios de dominio. - Controlar transacciones. - Invocar repositorios. - Gestionar operaciones externas. - Aplicar políticas de autorización cuando corresponda. Evita trasladar todas las reglas de negocio a los servicios de aplicación si pertenecen naturalmente a las entidades u objetos de valor. ### Servicios de infraestructura Implementan capacidades técnicas como: - Persistencia. - Correo electrónico. - Acceso a archivos. - Comunicación con APIs externas. - Servicios de mensajería. Cuando resulte apropiado, utiliza contratos que permitan mantener desacopladas las políticas de dominio de los detalles de infraestructura. ### Evitar servicios todopoderosos Una clase denominada `ApplicationManager`, `BusinessService` o `DataProcessor` puede estar acumulando responsabilidades independientes. Revisa: - Si contiene operaciones sin relación clara. - Si necesita demasiadas dependencias. - Si cambia por motivos muy diferentes. - Si concentra reglas que deberían pertenecer a otros objetos. No dividas automáticamente los servicios grandes sin identificar previamente responsabilidades coherentes. --- ## 11. Criterios para evitar la sobreingeniería Un diseño orientado a objetos no es necesariamente mejor por incorporar más clases, interfaces o patrones. Antes de introducir una abstracción, considera: 1. ¿Qué problema concreto resuelve? 2. ¿Qué responsabilidad representa? 3. ¿Reduce realmente el acoplamiento? 4. ¿Simplifica las pruebas o el mantenimiento? 5. ¿Existe una alternativa más directa? 6. ¿Su coste estructural está justificado? ### Evita especialmente - Interfaces con una única implementación creadas sin necesidad de desacoplamiento. - Jerarquías de herencia destinadas únicamente a compartir código trivial. - Factorías que solo encapsulan constructores sencillos. - Servicios que únicamente delegan llamadas. - Objetos de valor sin semántica propia. - Agregados sin invariantes relevantes. - Patrones introducidos para necesidades hipotéticas. - Refactorizaciones destinadas exclusivamente a cumplir límites numéricos. ### Favorece - Responsabilidades claras. - Contratos comprensibles. - Dependencias controladas. - Composición cuando aporte flexibilidad. - Encapsulamiento de invariantes. - Simplicidad estructural. - Evolución incremental del diseño. --- ## 12. Lista de comprobación de diseño orientado a objetos ### Responsabilidades - [ ] ¿Cada objeto tiene una responsabilidad identificable? - [ ] ¿Sus comportamientos pertenecen al concepto que representa? - [ ] ¿He evitado clases que acumulen responsabilidades independientes? - [ ] ¿Las dependencias son necesarias? - [ ] ¿La organización facilita la evolución del sistema? ### Encapsulamiento - [ ] ¿Las invariantes están protegidas? - [ ] ¿El estado mutable se modifica mediante operaciones controladas? - [ ] ¿He evitado exponer referencias mutables que comprometan el estado? - [ ] ¿Los contratos públicos son claros? - [ ] ¿Los datos se exponen cuando su finalidad lo justifica? ### Composición y herencia - [ ] ¿La composición mejora el diseño en este caso? - [ ] ¿Las relaciones de herencia representan especializaciones legítimas? - [ ] ¿Los subtipos respetan los contratos del tipo base? - [ ] ¿He evitado jerarquías artificiales? - [ ] ¿Las abstracciones responden a necesidades reales? ### Objetos de valor y entidades - [ ] ¿He distinguido correctamente identidad y valor? - [ ] ¿Los objetos de valor protegen sus invariantes? - [ ] ¿Las entidades conservan su identidad al modificar sus atributos? - [ ] ¿He evitado encapsular primitivas innecesariamente? - [ ] ¿Las operaciones de dominio están correctamente ubicadas? ### Agregados - [ ] ¿Existe una necesidad real de utilizar un agregado? - [ ] ¿La raíz protege las invariantes? - [ ] ¿Las modificaciones se realizan mediante operaciones controladas? - [ ] ¿Los límites de consistencia están correctamente definidos? - [ ] ¿He considerado transacciones y concurrencia cuando corresponde? - [ ] ¿He evitado agregados excesivamente grandes? ### Complejidad - [ ] ¿La solución es suficientemente sencilla? - [ ] ¿He evitado patrones innecesarios? - [ ] ¿Existe una alternativa más directa? - [ ] ¿La estructura facilita las pruebas? - [ ] ¿He respetado la arquitectura existente? - [ ] ¿La modificación mantiene el comportamiento y los contratos requeridos? --- ## Principio final > "Los objetos deben organizarse alrededor de responsabilidades reales y proteger las reglas del dominio, no existir únicamente para satisfacer patrones de diseño." El diseño orientado a objetos debe facilitar la comprensión, la evolución y la verificación del software. Utiliza entidades, objetos de valor, servicios, composición, herencia y agregados cuando aporten ventajas concretas. **Una arquitectura sencilla, cohesionada y con contratos bien definidos es preferible a una estructura artificialmente compleja que únicamente aparenta seguir principios de diseño orientado a objetos.**