Abstracción útil: elegir lo esencial para un caso de uso
Abstraer no es “hacerlo genérico”, sino seleccionar conscientemente lo que importa para un caso de uso y ocultar lo accidental. Una abstracción útil reduce el acoplamiento con detalles volátiles (reglas que cambian, formatos, proveedores) y mantiene estable lo que el resto del sistema necesita. La pregunta guía es: ¿qué necesita el consumidor para cumplir su tarea, sin saber cómo se logra?
En práctica, una buena abstracción suele verse como un contrato claro (qué se promete) y una implementación que puede cambiar (cómo se cumple). Si el contrato es demasiado amplio o demasiado “futurista”, se vuelve frágil y difícil de usar.
Cohesión y separación de preocupaciones (SoC) en términos prácticos
Cohesión mide qué tan relacionadas están las responsabilidades dentro de una clase/módulo. Alta cohesión significa que todo lo que hay dentro “empuja” hacia el mismo objetivo. Separación de preocupaciones significa que cada módulo se ocupa de una preocupación principal (cálculo de negocio, persistencia, UI, integración, etc.) y no mezcla decisiones de otras capas.
- Señal de alta cohesión: si cambian las reglas de descuentos, cambias un lugar; si cambia el formato de salida, cambias otro lugar distinto.
- Señal de baja cohesión: una clase que calcula descuentos, formatea strings, registra logs, consulta base de datos y decide mensajes de UI.
- Regla práctica: separa por razón de cambio. Si dos fragmentos de código cambian por motivos distintos, no deberían vivir juntos.
Diseño por contratos (DbC) aplicado: precondiciones, postcondiciones e invariantes
El diseño por contratos convierte suposiciones implícitas en reglas explícitas. Esto mejora la comunicación entre módulos y reduce errores silenciosos.
- Precondiciones: lo que debe cumplirse antes de llamar a un método (responsabilidad del llamador). Ej.: “el subtotal no puede ser negativo”.
- Postcondiciones: lo que el método garantiza al terminar (responsabilidad del método). Ej.: “el total final es >= 0 y <= subtotal”.
- Invariantes: condiciones que siempre deben mantenerse para un objeto/módulo. Ej.: “una regla de descuento nunca devuelve un monto mayor que el subtotal”.
En Java/C# normalmente expresas contratos con validaciones (throw), tipos (por ejemplo, usar decimal para dinero), y pruebas. En C# puedes apoyarte en ArgumentException/ArgumentOutOfRangeException; en Java en IllegalArgumentException. El objetivo no es “llenar de ifs”, sino proteger límites y documentar expectativas.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
Módulo ejemplo: cálculo de descuentos con requisitos cambiantes
Partimos de un requisito simple: “calcular el total aplicando descuentos según el tipo de cliente”. Luego llegan cambios: promociones por temporada, cupones, límites máximos, y reglas por categoría. Usaremos este módulo para decidir qué abstraer y cuándo.
Paso 1: implementación directa (útil para aprender, mala para cambiar)
Primera versión: una función con condicionales. Es válida si el requisito es estable y pequeño, pero se degrada cuando aparecen variaciones.
// Java (versión inicial, no ideal para cambios frecuentes) public class DiscountCalculator { public double finalTotal(double subtotal, String customerType, boolean hasCoupon) { if (subtotal < 0) throw new IllegalArgumentException("subtotal < 0"); double discount = 0.0; if ("VIP".equals(customerType)) { discount += 0.15; } else if ("REGULAR".equals(customerType)) { discount += 0.05; } if (hasCoupon) { discount += 0.10; } double total = subtotal * (1.0 - discount); if (total < 0) total = 0; return total; } }Problemas típicos: cada nueva regla agrega ramas, el orden de aplicación se vuelve confuso, y es difícil probar combinaciones sin duplicación.
Paso 2: identificar el contrato estable (qué necesita el resto del sistema)
Antes de crear interfaces, define el contrato que el sistema realmente consume. En este caso, el resto del sistema quiere: “dado un contexto de compra, obtener un total final y/o un detalle”. Si aún no necesitas detalle, no lo inventes.
- Contrato mínimo:
Money calculateTotal(Purchase purchase)o equivalente. - Precondiciones: subtotal no negativo; moneda consistente; cliente válido.
- Postcondiciones: total entre 0 y subtotal (si solo hay descuentos); determinismo para el mismo input.
Nota: si más adelante aparecen recargos/impuestos, la postcondición “total <= subtotal” deja de ser válida; eso es una señal de que el contrato debe reflejar el dominio real (por ejemplo, “total >= 0” y “descuento aplicado no excede el subtotal”).
Paso 3: convertir variaciones en puntos de extensión (reglas como componentes)
Cuando el cambio se concentra en “nuevas reglas”, abstrae ese eje: una regla de descuento. Mantén el motor de cálculo estable y deja que las reglas cambien.
// C# (contrato de regla) public interface IDiscountRule { decimal CalculateDiscountAmount(Purchase purchase); // Contrato: devuelve un monto entre 0 y purchase.Subtotal } public sealed class Purchase { public decimal Subtotal { get; } public CustomerType CustomerType { get; } public bool HasCoupon { get; } public Purchase(decimal subtotal, CustomerType customerType, bool hasCoupon) { if (subtotal < 0) throw new ArgumentOutOfRangeException(nameof(subtotal)); Subtotal = subtotal; CustomerType = customerType; HasCoupon = hasCoupon; } } public enum CustomerType { Regular, Vip }Ahora el motor aplica una lista de reglas. Aquí es donde DbC ayuda: cada regla promete un monto válido, y el motor garantiza un total consistente.
// C# (motor cohesivo: solo orquesta reglas) public sealed class DiscountEngine { private readonly IReadOnlyList<IDiscountRule> _rules; public DiscountEngine(IReadOnlyList<IDiscountRule> rules) { _rules = rules ?? throw new ArgumentNullException(nameof(rules)); } public decimal CalculateTotal(Purchase purchase) { if (purchase == null) throw new ArgumentNullException(nameof(purchase)); decimal discount = 0m; foreach (var rule in _rules) { var amount = rule.CalculateDiscountAmount(purchase); if (amount < 0m) throw new InvalidOperationException("Rule returned negative discount"); discount += amount; } if (discount > purchase.Subtotal) discount = purchase.Subtotal; // postcondición defensiva return purchase.Subtotal - discount; } }Paso 4: implementar reglas con alta cohesión (una razón de cambio cada una)
Cada regla debe ser pequeña y enfocada. Si una regla empieza a consultar bases de datos, leer archivos o llamar APIs, probablemente estás mezclando preocupaciones (cálculo vs obtención de datos).
// Java (reglas simples) public interface DiscountRule { double discountAmount(Purchase purchase); } public final class VipDiscountRule implements DiscountRule { public double discountAmount(Purchase purchase) { return purchase.customerType() == CustomerType.VIP ? purchase.subtotal() * 0.15 : 0.0; } } public final class CouponDiscountRule implements DiscountRule { public double discountAmount(Purchase purchase) { return purchase.hasCoupon() ? purchase.subtotal() * 0.10 : 0.0; } }Si aparece un requisito como “máximo 50 de descuento por cupón”, esa variación vive dentro de CouponDiscountRule, no en el motor.
Paso 5: hacer explícito el contrato de composición (orden, acumulación, límites)
Los requisitos cambiantes suelen atacar la composición: “los descuentos no se suman, se aplica el mayor”, “primero VIP y luego cupón sobre el remanente”, “no combinar con promoción X”. No lo escondas en reglas dispersas sin un criterio; define una política de combinación.
Dos enfoques comunes:
- Acumulación (suma de montos): simple, pero puede exceder el subtotal; requiere límite.
- Selección (máximo descuento): útil cuando las promociones son excluyentes.
// C# (política de combinación como extensión) public interface IDiscountCombinationPolicy { decimal Combine(Purchase purchase, IEnumerable<decimal> discounts); } public sealed class SumWithCapPolicy : IDiscountCombinationPolicy { public decimal Combine(Purchase purchase, IEnumerable<decimal> discounts) { var sum = discounts.Sum(); if (sum < 0m) throw new InvalidOperationException("Negative sum"); return Math.Min(sum, purchase.Subtotal); } } public sealed class MaxOnlyPolicy : IDiscountCombinationPolicy { public decimal Combine(Purchase purchase, IEnumerable<decimal> discounts) { var max = discounts.DefaultIfEmpty(0m).Max(); if (max < 0m) throw new InvalidOperationException("Negative max"); return Math.Min(max, purchase.Subtotal); } }Y el motor queda estable, cambiando solo la política cuando cambian reglas de combinación.
// C# (motor con política) public sealed class DiscountEngine { private readonly IReadOnlyList<IDiscountRule> _rules; private readonly IDiscountCombinationPolicy _policy; public DiscountEngine(IReadOnlyList<IDiscountRule> rules, IDiscountCombinationPolicy policy) { _rules = rules ?? throw new ArgumentNullException(nameof(rules)); _policy = policy ?? throw new ArgumentNullException(nameof(policy)); } public decimal CalculateTotal(Purchase purchase) { if (purchase == null) throw new ArgumentNullException(nameof(purchase)); var amounts = _rules.Select(r => r.CalculateDiscountAmount(purchase)).ToList(); foreach (var a in amounts) if (a < 0m) throw new InvalidOperationException("Negative discount"); var discount = _policy.Combine(purchase, amounts); return purchase.Subtotal - discount; } }Qué abstraer y cuándo no hacerlo (diseño orientado al cambio)
Guía práctica para decidir abstracciones
| Pregunta | Si la respuesta es “sí” | Si la respuesta es “no” |
|---|---|---|
| ¿Este aspecto cambia por razones distintas al resto? | Sepáralo (módulo/regla/política). | Mantén junto; evita capas. |
| ¿Hay al menos 2 variantes reales hoy (o una inminente y confirmada)? | Introduce un punto de extensión (interfaz/estrategia). | Espera; usa una implementación directa. |
| ¿El consumidor necesita estabilidad y simplicidad? | Define un contrato pequeño y explícito. | No sobre-diseñes; expón lo mínimo. |
| ¿Puedes describir el contrato con pre/postcondiciones claras? | Buen candidato a abstracción. | Probablemente aún no entiendes el dominio; refina primero. |
Transformar requisitos cambiantes en puntos de extensión (ejemplos)
- “Aparecen nuevas reglas de descuento” → abstrae
DiscountRule(variación por regla). - “Cambian las reglas de combinación” → abstrae
CombinationPolicy(variación por composición). - “El descuento depende de datos externos” → separa obtención de datos (repositorio/servicio) del cálculo (regla). No metas IO dentro de la regla si puedes inyectar datos ya resueltos en el
Purchaseo en un contexto. - “Necesitamos auditar qué descuentos se aplicaron” → agrega un resultado enriquecido solo cuando sea necesario (por ejemplo,
DiscountBreakdown), sin romper el contrato actual: puedes crear un método adicional o un DTO opcional.
Señales de sobre-abstracción (y cómo corregirlas)
1) Interfaces innecesarias (una sola implementación sin variación real)
Si tienes IDiscountEngine y solo existe DiscountEngine, y no hay pruebas que requieran un doble ni una variante real, probablemente es ruido. En muchos casos, basta con depender de la clase concreta o extraer la interfaz cuando aparezca la segunda implementación.
- Corrección: elimina la interfaz prematura; conserva la composición de reglas/políticas, que sí representa variación.
2) Capas artificiales que no agregan valor
Ejemplo: DiscountService llama a DiscountManager llama a DiscountProcessor, todos con un método que solo delega. Eso reduce legibilidad y aumenta puntos de fallo.
- Corrección: colapsa capas; deja un módulo cohesivo con nombres claros. Mantén capas solo si hay una preocupación distinta (p. ej., orquestación vs cálculo vs persistencia).
3) Abstracciones “por si acaso” (contratos demasiado genéricos)
Señal: métodos tipo Execute(object input), Process(Map<String,Object> data), o “rules engine” genérico sin necesidad. Eso desplaza el problema a runtime y debilita contratos.
- Corrección: vuelve a tipos explícitos (por ejemplo,
Purchase), y contratos verificables con pre/postcondiciones.
4) Configuración excesiva para cambios simples
Si agregar una regla requiere tocar 6 archivos, registrar en un contenedor, actualizar factories, y editar configuraciones, el punto de extensión está sobrediseñado para el tamaño del problema.
- Corrección: empieza con una lista explícita de reglas en composición (por ejemplo, en el composition root). Solo introduce descubrimiento dinámico/config cuando el producto lo pida.
Refactorización incremental sugerida (checklist)
- 1. Aísla el cálculo: mueve la lógica de descuentos a un módulo dedicado (sin IO, sin UI).
- 2. Define el contrato mínimo: entrada clara (p. ej.,
Purchase) y salida clara (total o descuento). - 3. Extrae variaciones: crea reglas pequeñas (
VipDiscountRule,CouponDiscountRule). - 4. Formaliza composición: si el orden o la combinación cambia, introduce una política.
- 5. Refuerza contratos: valida precondiciones en bordes; asegura postcondiciones en el motor; agrega pruebas por regla y por política.
- 6. Revisa sobre-abstracción: elimina interfaces/capas que no representen variación real o no aporten claridad.