Arquitectura de Software

El arte de soltar: una guía humana sobre la inyección de dependencias

Publicado el 2026-06-14•5 min•0 vistas

El arte de soltar el control: una guía humana sobre inyección de dependencias en C# trata de desaprender el instinto de controlar todo y confiar en que las herramientas correctas te serán entregadas en el momento adecuado. Este artículo recorre la filosofía, el desafío y la implementación práctica de DI en aplicaciones .NET modernas.

El Desafío

Todo desarrollador ha escrito código así en algún momento: una clase que crea sus propias dependencias, decide sus propios colaboradores y termina convirtiéndose en un monolito de lógica fuertemente acoplada. Se siente natural — necesitas un logger, creas un logger. Necesitas una conexión a la base de datos, instancias una ahí mismo, en línea, con la palabra clave new y una sensación de control total.

El problema no es la palabra clave new. El problema es el supuesto enterrado dentro de ella: que esta clase sabe mejor que nadie lo que necesita y cómo obtenerlo.

Cuando codificas las dependencias directamente, hacer pruebas se vuelve casi imposible. Cambiar implementaciones se convierte en una operación quirúrgica. Tu código se vuelve frágil exactamente en los lugares donde necesita ser flexible — en las costuras entre componentes.

La Inyección de Dependencias no es un patrón sobre frameworks o contenedores. Es un cambio de mentalidad: deja de construir cosas que saben cómo adquirir lo que necesitan, y empieza a construir cosas que simplemente declaran lo que necesitan. Deja que algo más — un contenedor, una fábrica, un harness de pruebas — lo entregue.

Implementación Técnica

Definir la Abstracción

El primer paso es dejar de depender de tipos concretos y comenzar a depender de interfaces. Este es el contrato que tu clase anuncia al mundo.

C# Debug Console
public interface ITransactionRepository
{
    Task<TransactionStatus> GetStatusAsync(Guid transactionId);
    Task<bool> CommitAsync(Guid transactionId);
}

La lógica de negocio nunca debería importarle si los datos vienen de SQL Server, de un almacenamiento en memoria o de un mock en una prueba unitaria. Solo le importa la forma de los datos.

Inyectar a Través del Constructor

La inyección por constructor es la forma más explícita y honesta de DI. Hace que las dependencias sean visibles, no opcionales y fáciles de razonar.

C# Debug Console
public class PaymentProcessor
{
    private readonly ITransactionRepository _repository;
    private readonly ILogger<PaymentProcessor> _logger;

    public PaymentProcessor(
        ITransactionRepository repository,
        ILogger<PaymentProcessor> logger)
    {
        _repository = repository;
        _logger = logger;
    }

    public async Task<bool> ProcessAsync(Guid transactionId)
    {
        _logger.LogInformation("Procesando transacción {Id}", transactionId);

        var status = await _repository.GetStatusAsync(transactionId);

        if (status != TransactionStatus.Pending)
        {
            _logger.LogWarning("La transacción {Id} no está en un estado procesable.", transactionId);
            return false;
        }

        return await _repository.CommitAsync(transactionId);
    }
}

Nota lo que PaymentProcessor NO hace: no crea una conexión a la base de datos, no sabe qué framework de logging se está usando y no decide su propio ciclo de vida. Simplemente hace su trabajo.

Registrar Dependencias en el Contenedor

En .NET, el contenedor de DI integrado vive en Microsoft.Extensions.DependencyInjection. Registras tus dependencias una vez, al inicio, y el contenedor se encarga de la resolución en todos los demás lugares.

C# Debug Console
var builder = WebApplication.CreateBuilder(args);

// Registrar el repositorio con tiempo de vida scoped (una vez por petición HTTP)
builder.Services.AddScoped<ITransactionRepository, SqlTransactionRepository>();

// Registrar el procesador — el contenedor inyectará el repositorio automáticamente
builder.Services.AddScoped<PaymentProcessor>();

var app = builder.Build();

Entender los tiempos de vida es fundamental:

  • •Transient — una nueva instancia cada vez que se solicita. Ideal para servicios ligeros y sin estado.
  • •Scoped — una instancia por petición (o scope). La elección correcta para la mayoría de servicios con base de datos.
  • •Singleton — una instancia para todo el tiempo de vida de la aplicación. Usar con cuidado; el estado compartido puede introducir errores sutiles.

Escribir una Prueba Unitaria Comprobable

El verdadero beneficio del DI se revela en el momento de las pruebas. Con dependencias inyectadas, puedes sustituir implementaciones reales por fakes controlados.

C# Debug Console
public class PaymentProcessorTests
{
    [Fact]
    public async Task ProcessAsync_RetornaFalso_CuandoTransaccionNoEstaPendiente()
    {
        // Arrange
        var mockRepo = new Mock<ITransactionRepository>();
        mockRepo
            .Setup(r => r.GetStatusAsync(It.IsAny<Guid>()))
            .ReturnsAsync(TransactionStatus.Completed);

        var logger = NullLogger<PaymentProcessor>.Instance;
        var processor = new PaymentProcessor(mockRepo.Object, logger);

        // Act
        var result = await processor.ProcessAsync(Guid.NewGuid());

        // Assert
        Assert.False(result);
    }
}

Sin base de datos. Sin archivos de configuración. Sin variables de entorno. Solo lógica pura, probada en aislamiento.

Payload de Ejemplo

Cuando este procesador se expone a través de un endpoint de API, la petición entrante debe transportar la siguiente estructura. El controlador la recibe, extrae el ID de transacción y delega al PaymentProcessor inyectado.

JSON Response Payload
{
    "transaction_id": "f3a2d1b0-4e5f-6789-abcd-ef0123456789",
    "requested_by": "user-service",
    "idempotency_key": "pay-20260615-00042",
    "metadata": {
        "source": "mobile-checkout",
        "retry_attempt": 0,
        "currency": "EUR"
    }
}

La idempotency_key es especialmente importante en contextos financieros: garantiza que aunque la misma petición se envíe dos veces por un reintento de red, la operación solo se ejecute una vez en el backend.

Conclusión

La Inyección de Dependencias no es sobre frameworks. No es sobre anotaciones, contenedores ni el atributo [Inject]. Es sobre la disciplina de construir componentes que declaran sus necesidades en lugar de satisfacerlas unilateralmente.

Cuando sueltas la palabra clave new — cuando tus clases dejan de adquirir sus propias dependencias — algo interesante ocurre. Tu código se vuelve más fácil de leer, porque las dependencias son explícitas. Se vuelve más fácil de probar, porque puedes sustituir cualquier cosa. Se vuelve más fácil de mantener, porque cambiar una implementación no se propaga por todo el código.

Jesús Jahaziel Higareda López

Jesús Jahaziel Higareda López

SOFTWARE ARCHITECT

Arquitecto de Software especializado en fintech, sistemas europeos de alta disponibilidad y migraciones progresivas de monolitos a microservicios.

Módulo de Comentarios0

AÑADIR PEER FEEDBACK

✓ Persistencia de sesión habilitada localmente.