Saltar al contenido principal

Depende de lo que no cambia: Practical Object Oriented Design (III)

2026-02-268 min
ArquitecturaPractical Object Oriented DesignClean Code.NET

Seguimos con la serie sobre Practical Object-Oriented Design de Sandi Metz. En el anterior artículo vimos por qué el diseño importa y cómo conseguir que una clase tenga una única responsabilidad. Ahora toca hablar de algo que probablemente sea la fuente número uno de dolor en el mantenimiento de software: las dependencias.

Porque aquí está la cosa: puedes tener clases con una responsabilidad perfectamente definida, y aun así acabar con un código que se rompe en cascada cada vez que tocas algo. El problema no es lo que hace cada clase, sino lo que sabe de las demás.

Cuando los objetos saben demasiado

Los objetos no viven aislados. Para resolver problemas complejos, necesitan colaborar entre sí. Y esa colaboración tiene un precio: cada vez que un objeto conoce detalles de otro, se crea una dependencia.

Metz es muy concreta al definir cuándo existe una dependencia. Un objeto depende de otro cuando conoce:

  1. El nombre de otra claseGear sabe que existe Wheel.

  2. El nombre de un mensajeGear espera llamar a Diameter.

  3. Los argumentos que necesitaGear sabe que Wheel requiere rim y tire.

  4. El orden de esos argumentos — primero la llanta, luego el neumático.

Cada uno de estos puntos es un hilo invisible que ata una clase a otra. Cuantos más hilos, más acoplamiento. Y cuanto más acoplamiento, más frágil es el sistema: cambios pequeños en un lugar provocan roturas en lugares inesperados.

El problema del new

Veamos un ejemplo clásico. Imagina una clase Gear que necesita calcular las "gear inches" (una medida que combina la relación de engranajes con el diámetro de la rueda):

c#
// MAL DISEÑO: Acoplamiento fuerte
public class Gear
{
    private readonly double rim;
    private readonly double tire;

    public Gear(double rim, double tire)
    {
        this.rim = rim;
        this.tire = tire;
    }

    public double GearInches()
    {
        // Gear crea la instancia explícitamente.
        // Sabe el nombre de la clase, sus argumentos y su orden.
        return Ratio() * new Wheel(rim, tire).Diameter;
    }

    private double Ratio() => 52.0 / 11.0;
}

Parece inocente, pero esa línea con new Wheel(rim, tire) es una declaración de intenciones: Gear está diciendo "yo solo trabajo con Wheel, y no me hables de nada más". Si mañana necesitas calcular gear inches con un rodillo de entrenamiento, un tambor o cualquier otra cosa que tenga un diámetro, no puedes. Gear se niega a colaborar.

Y no solo eso: Gear conoce los cuatro tipos de dependencia que mencionamos. Sabe que la clase se llama Wheel, que tiene una propiedad Diameter, que necesita dos argumentos, y que van en un orden concreto. Si cualquiera de esas cosas cambia, Gear se rompe.

Inyección de dependencias: que no te importe el "qué", sino el "cómo"

La solución que propone Metz es elegante en su simplicidad: a Gear no le importa qué es el objeto, solo le importa que tenga un diámetro. Así que en lugar de crear la dependencia internamente, la recibe desde fuera:

c#
public class Gear
{
    private readonly Wheel wheel;

    // La dependencia se inyecta desde fuera
    public Gear(Wheel wheel)
    {
        this.wheel = wheel;
    }

    public double GearInches()
    {
        // Gear colabora con cualquier cosa que tenga Diameter
        return Ratio() * wheel.Diameter;
    }

    private double Ratio() => 52.0 / 11.0;
}

Con este cambio, Gear pasa de conocer cuatro dependencias (nombre de clase, propiedad, argumentos, orden) a conocer solo una (que el objeto tiene Diameter). La creación de Wheel ya no es responsabilidad de Gear, sino de quien lo use:

c#
var wheel = new Wheel(26, 1.5);
var gear = new Gear(wheel);
var inches = gear.GearInches();

Esto te sonará al principio de inversion de dependencias de SOLID. Y es exactamente eso. La diferencia es que Metz no te lo presenta como un principio abstracto que memorizar, sino como una solución natural a un problema concreto. Ese es uno de los puntos fuertes del libro: el "por qué" siempre llega antes que el "cómo".

Cuando no puedes inyectar: aísla

En un mundo ideal, siempre inyectarías las dependencias. Pero la realidad tiene restricciones: código legado, frameworks que controlan la creación de instancias, o simplemente código que heredaste y no puedes refactorizar de golpe. Para estos casos, Metz propone aislar la dependencia.

La idea es sencilla: si no puedes eliminar el new, al menos haz que sea evidente y fácil de cambiar.

Técnica 1: Aislar la creación. Mueve el new a un método propio:

c#
public class Gear
{
    private Wheel? wheel;
    private readonly double rim;
    private readonly double tire;

    public Gear(double rim, double tire)
    {
        this.rim = rim;
        this.tire = tire;
    }

    public double GearInches()
    {
        // El cálculo queda limpio
        return Ratio() * GetWheel().Diameter;
    }

    // Todo el conocimiento de la creación está aislado aquí.
    // Si el constructor de Wheel cambia, solo tocamos este método.
    private Wheel GetWheel() => wheel ??= new Wheel(rim, tire);

    private double Ratio() => 52.0 / 11.0;
}

Técnica 2: Aislar mensajes externos. Si la llamada a wheel.Diameter es propensa a cambios (por ejemplo, si es una cadena de llamadas profunda tipo wheel.Tire.OuterDiameter), envuélvela:

c#
public class Gear
{
    private readonly Wheel wheel;

    public Gear(Wheel wheel) => this.wheel = wheel;

    public double GearInches()
    {
        // Gear habla consigo mismo, no con un extraño
        return Ratio() * Diameter();
    }

    // Si la API de Wheel cambia (de .Diameter a .TotalWidth),
    // solo rompemos este método wrapper.
    private double Diameter() => wheel.Diameter;

    private double Ratio() => 52.0 / 11.0;
}

Ambas técnicas comparten la misma filosofía: no eliminas la dependencia, pero la conviertes en un punto de cambio único y visible. Cuando Wheel cambie (y cambiará), sabrás exactamente dónde ir.

El orden de los argumentos es una dependencia oculta

Hay un tipo de dependencia que a menudo se nos pasa por alto: el orden de los argumentos. Cuando ves algo como new Gear(52, 11, 26, 1.5), cada posición es una dependencia. ¿El 52 es el piñón o el plato? ¿El 26 es el aro o el neumático? Si el orden cambia, todo el código que instancia Gear se rompe.

La solución de Metz es usar lo que en Ruby sería un hash de opciones. En C# tenemos varias formas de conseguir lo mismo:

c#
public record GearOptions
{
    public int Chainring { get; init; } = 40;
    public int Cog { get; init; } = 18;
    public required Wheel Wheel { get; init; }
}

public class Gear
{
    public int Chainring { get; }
    public int Cog { get; }
    public Wheel Wheel { get; }

    public Gear(GearOptions options)
    {
        Chainring = options.Chainring;
        Cog = options.Cog;
        Wheel = options.Wheel;
    }
}

// El orden ya no importa, y los nombres documentan cada argumento
var gear = new Gear(new GearOptions
{
    Cog = 11,
    Wheel = new Wheel(26, 1.5)
});

La dependencia pasa del "orden" a los "nombres de propiedades". Y los nombres son mucho más estables que las posiciones.

Wrappers: protégete de lo que no controlas

Cuando la dependencia es una librería externa cuya API no puedes modificar, Metz propone crear un wrapper o factory que aísle ese conocimiento:

c#
// Clase de una librería externa que NO podemos tocar
public class ExternalFrameworkGear
{
    public ExternalFrameworkGear(int chainring, int cog, Wheel wheel)
    {
        // ...
    }
}

// Nuestro wrapper ofrece una interfaz limpia
public static class GearFactory
{
    public static ExternalFrameworkGear Create(GearOptions options) =>
        new(options.Chainring, options.Cog, options.Wheel);
}

El resto de tu aplicación usa GearFactory.Create(opciones) y no sabe nada del constructor posicional de la librería externa. Si la librería cambia su API en una actualización, solo tocas el wrapper.

Este patrón es especialmente relevante cuando trabajas con SDKs de terceros (proveedores de pago, servicios cloud, clientes HTTP...) que pueden cambiar entre versiones.

La decisión más importante: la dirección

Hasta aquí hemos visto cómo reducir el acoplamiento. Pero hay una decisión de diseño aún más fundamental: la dirección de la dependencia. Que A dependa de B o que B dependa de A no es lo mismo, y elegir mal puede ser muy costoso.

Metz propone una regla simple: depende de cosas que cambien con menos frecuencia que tú.

image

Las clases varían en tres dimensiones: su probabilidad de cambio, su nivel de abstracción, y el número de clases que dependen de ellas. Con estas tres dimensiones, puedes clasificar cualquier pieza de código en cuatro zonas:

Zona A — La zona abstracta. Cambian poco, muchas clases dependen de ellas. Son las interfaces, las clases abstractas, los tipos del lenguaje como string o IEnumerable<T>. Es la zona ideal para depender de ella: estable y ampliamente usada.

Zona B — Estable y solitaria. Cambian poco y pocas clases dependen de ellas. Código utilitario, helpers internos. Inofensiva.

Zona C — Volátil pero aislada. Cambian mucho pero nadie depende de ellas. Controladores, scripts de migración, clases de UI. Aunque el código es inestable, los cambios no generan efectos secundarios porque nadie más los referencia.

Zona D — La zona de peligro. Cambia mucho y muchas clases dependen de ella. Es una clase concreta que todo el mundo usa pero que necesita modificarse constantemente. Esta es la fuente del dolor en el mantenimiento. El objetivo del diseño es evitar que cualquier clase caiga aquí.

Lo interesante de este análisis es que revela una consecuencia sutil: una clase con muchos dependientes estará bajo una enorme presión para no cambiar nunca. Si esa clase además es volátil (porque los requisitos de negocio la empujan a cambiar), tienes un conflicto grave. Necesitas cambiarla, pero no puedes sin romper medio sistema.

Lo que me llevo de este capítulo

Si el capítulo de responsabilidad única me enseñó a pensar en lo que hace cada clase, el de dependencias me enseñó a pensar en lo que cada clase sabe de las demás. Y esa distinción cambia completamente la forma de diseñar.

La inyección de dependencias no es un patrón de framework, es una decisión de diseño. Antes de llegar a cualquier contenedor de DI, la pregunta fundamental es: ¿esta clase necesita saber cómo se crea su colaborador, o solo necesita usarlo?

El aislamiento es tu plan B. Cuando no puedes inyectar (y en el mundo real, muchas veces no puedes), aislar la dependencia en un único punto visible es la siguiente mejor opción.

La dirección importa más que la cantidad. Puedes tener muchas dependencias y que no sea un problema, si todas apuntan hacia cosas más estables que tú. El problema real aparece cuando algo inestable tiene muchos dependientes.

Nos vemos en el siguiente post!