🚨 ¡Nueva review! ✨ Mi ratón favorito para programar: el Logitech MX Master 3S . ¡Échale un ojo! 👀

¿Qué es el Principio de Inversión de Dependencias o Dependency Inversion Principle (DIP)?

Depende de abstracciones, no de implementaciones concretas. La base de un código flexible.

Escrito por domin el 5 de octubre de 2024 · Actualizado el 8 de febrero de 2026

🏗️ Principio de Inversión de Dependencias (DIP)

El principio de Inversión de Dependencias (Dependency Inversion Principle - DIP) es el quinto y último de los principios SOLID. Y para mí, uno de los más importantes. Este principio nos ayuda a crear sistemas más flexibles y fáciles de mantener al desacoplar dependencias entre clases de alto nivel y clases de bajo nivel.

Gato negro representando el principio de inversión de dependencias DIP de los principios SOLID.

¿Qué es?

El principio de Inversión de Dependencias, que a partir de ahora lo llamaremos DIP, tiene dos reglas fundamentales:

1. Los módulos de alto nivel no deben depender de los módulos de bajo nivel. Ambos deben depender de abstracciones.

2. Las abstracciones no deben depender de los detalles. Los detalles deben depender de las abstracciones.

Traducido a cristiano: las clases más importantes de tu aplicación (las que contienen la lógica de negocio) no deben tener dependencias directas de clases concretas (bases de datos, servicios de email, APIs externas…), sino que ambas deben depender de una interfaz o abstracción común. Así puedes cambiar las implementaciones concretas sin afectar al funcionamiento de la lógica principal.

¿De dónde sale este principio?

El DIP fue formulado por Robert C. Martin (Uncle Bob) en su artículo “The Dependency Inversion Principle” publicado en 1996 en la revista C++ Report. Más tarde lo integró como la D de SOLID en su libro “Agile Software Development, Principles, Patterns, and Practices” (2002).

La idea de Uncle Bob surgió de observar un problema muy común en los proyectos de software: la lógica de negocio (lo más valioso del sistema) estaba atada a detalles de infraestructura (bases de datos, frameworks, librerías de terceros). Cada vez que cambiaba un detalle técnico, había que tocar la lógica de negocio. Eso es como tener que reformar los cimientos de tu casa cada vez que cambias una bombilla.

La inspiración viene del mundo de la electrónica y la ingeniería de sistemas, donde los módulos de alto nivel siempre trabajan con interfaces estándar (enchufes, puertos USB, conectores) en lugar de depender del cable concreto que les conecta.

¿Qué es “alto nivel” y “bajo nivel”?

Alto nivel

La lógica de negocio. Lo que decide qué se hace. Ejemplo: "enviar una notificación al usuario", "procesar un pedido", "calcular el descuento".

Bajo nivel

Los detalles de implementación. Lo que decide cómo se hace. Ejemplo: "enviar por SMTP", "guardar en MySQL", "llamar a la API de Stripe".

Si tu clase de alto nivel instancia directamente una clase de bajo nivel, estás creando un acoplamiento fuerte. Si mañana cambias la base de datos de MySQL a PostgreSQL, tienes que modificar la clase de negocio. Eso no mola nada nene.


¿Por qué es tan importante?

Aplicar el DIP te da ventajas muy claras:


Ejemplo

Supongamos que tenemos un sistema de notificaciones y de momento vamos a enviar estas notificaciones vía email, pero hay planes para implementar en el futuro notificaciones vía SMS.

class EmailService
{
    public function sendEmail(string $message): void
    {
        // Configurar SMTP, conectar, enviar...
        echo "Enviando email: $message";
    }
}

class Notification
{
    private EmailService $emailService;

    public function __construct(EmailService $emailService)
    {
        $this->emailService = $emailService;
    }

    public function send(string $message): void
    {
        $this->emailService->sendEmail($message);
    }
}

Hasta aquí, ¿qué tal? Todo mal, ¿verdad?

El problema es que Notification (clase de alto nivel, la lógica de negocio) depende directamente de EmailService (clase de bajo nivel, un detalle de implementación). Si más adelante queremos enviar mensajes por SMS, vamos a tener que modificar la clase Notification, rompiendo el DIP y de paso el OCP (Open/Closed).

Además, para testear Notification necesitas un EmailService real (o al menos un mock específico para esa clase), lo que complica los tests.

Refactor

Vamos a aplicar el DIP. Creamos una abstracción (interfaz) de la que ambas clases dependan:

interface NotificationService
{
    public function send(string $message): void;
}

class EmailService implements NotificationService
{
    public function send(string $message): void
    {
        // Configurar SMTP, conectar, enviar...
        echo "Enviando email: $message";
    }
}

class Notification
{
    private NotificationService $notificationService;

    public function __construct(NotificationService $notificationService)
    {
        $this->notificationService = $notificationService;
    }

    public function send(string $message): void
    {
        $this->notificationService->send($message);
    }
}

Ahora Notification no sabe ni le importa si le llega un servicio de email, de SMS o de paloma mensajera. Solo sabe que tiene algo que implementa NotificationService y que puede llamar a send(). De esta forma podemos usarlo así:

$emailService = new EmailService();
$notification = new Notification($emailService);
$notification->send("¡Tienes una notificación por email!");

Y añadir nuevos canales es tan fácil como crear una clase nueva:

class SmsService implements NotificationService
{
    public function send(string $message): void
    {
        // Conectar con API de SMS...
        echo "Enviando SMS: $message";
    }
}

class TelegramService implements NotificationService
{
    public function send(string $message): void
    {
        // Conectar con Bot API de Telegram...
        echo "Enviando por Telegram: $message";
    }
}

// Cambiar de canal es cambiar una línea
$notification = new Notification(new SmsService());
$notification->send("¡Tienes una notificación por SMS!");

$notification = new Notification(new TelegramService());
$notification->send("¡Tienes una notificación por Telegram!");

Cero modificaciones en la clase Notification. La lógica de negocio permanece intacta. Solo creamos clases nuevas y las inyectamos. Eso es la inversión de dependencias.


Ejemplo más real: sistema de persistencia

Este caso te lo vas a encontrar en prácticamente todos los proyectos. Imagina un servicio que gestiona usuarios:

// MAL: acoplado a MySQL directamente
class UserService
{
    private MySqlConnection $db;

    public function __construct()
    {
        $this->db = new MySqlConnection('localhost', 'root', 'password');
    }

    public function getUser(int $id): array
    {
        return $this->db->query("SELECT * FROM users WHERE id = $id");
    }

    public function saveUser(array $data): void
    {
        $this->db->execute("INSERT INTO users ...", $data);
    }
}

¿Ves los problemas?

El refactor aplicando DIP:

interface UserRepository
{
    public function findById(int $id): array;
    public function save(array $data): void;
}

class MySqlUserRepository implements UserRepository
{
    public function __construct(
        private PDO $connection
    ) {}

    public function findById(int $id): array
    {
        $stmt = $this->connection->prepare("SELECT * FROM users WHERE id = :id");
        $stmt->execute(['id' => $id]);
        return $stmt->fetch();
    }

    public function save(array $data): void
    {
        // INSERT con PDO...
    }
}

class MongoUserRepository implements UserRepository
{
    public function __construct(
        private MongoCollection $collection
    ) {}

    public function findById(int $id): array
    {
        return $this->collection->findOne(['id' => $id]);
    }

    public function save(array $data): void
    {
        $this->collection->insertOne($data);
    }
}

class UserService
{
    public function __construct(
        private UserRepository $repository
    ) {}

    public function getUser(int $id): array
    {
        return $this->repository->findById($id);
    }

    public function createUser(array $data): void
    {
        // Lógica de negocio: validaciones, reglas...
        $this->repository->save($data);
    }
}

Ahora UserService no sabe si los datos viven en MySQL, MongoDB, un fichero JSON o una API REST. Solo sabe que tiene un UserRepository que puede buscar y guardar usuarios. Cambiar de base de datos es cambiar una línea en la configuración del contenedor de dependencias:

// Con MySQL
$service = new UserService(new MySqlUserRepository($pdoConnection));

// Con MongoDB
$service = new UserService(new MongoUserRepository($mongoCollection));

// En tests, con un repositorio en memoria
$service = new UserService(new InMemoryUserRepository());

La lógica de negocio queda blindada frente a cambios en la infraestructura.


Otro ejemplo: pasarela de pago

Imagina un servicio de checkout en una tienda online:

// MAL: acoplado a Stripe
class CheckoutService
{
    public function processPayment(float $amount): void
    {
        $stripe = new StripeClient('sk_live_xxxx');
        $stripe->charges->create([
            'amount' => $amount * 100,
            'currency' => 'eur',
        ]);
    }
}

Si mañana el negocio decide cambiar de Stripe a PayPal o Redsys, hay que reescribir CheckoutService. Y esto es lógica de negocio crítica, cada cambio es un riesgo.

Con DIP:

interface PaymentGateway
{
    public function charge(float $amount, string $currency): PaymentResult;
}

class StripeGateway implements PaymentGateway
{
    public function __construct(private StripeClient $client) {}

    public function charge(float $amount, string $currency): PaymentResult
    {
        $result = $this->client->charges->create([
            'amount' => $amount * 100,
            'currency' => $currency,
        ]);
        return new PaymentResult($result->id, $result->status);
    }
}

class PayPalGateway implements PaymentGateway
{
    public function __construct(private PayPalClient $client) {}

    public function charge(float $amount, string $currency): PaymentResult
    {
        // Lógica de PayPal...
        return new PaymentResult($orderId, 'completed');
    }
}

class CheckoutService
{
    public function __construct(
        private PaymentGateway $gateway
    ) {}

    public function processPayment(float $amount): PaymentResult
    {
        // Lógica de negocio: validar carrito, aplicar descuentos...
        return $this->gateway->charge($amount, 'eur');
    }
}

Ahora cambiar de pasarela de pago es cambiar qué clase se inyecta, no tocar la lógica de checkout. Y para los tests puedes crear un FakePaymentGateway que siempre devuelva éxito sin conectarse a ningún sitio.


DIP vs Inyección de Dependencias: no es lo mismo

Un error muy común es confundir el Principio de Inversión de Dependencias (DIP) con la Inyección de Dependencias (Dependency Injection - DI). Son cosas diferentes aunque están relacionadas:

DIP (Principio)

Es un principio de diseño. Dice qué debes hacer: depender de abstracciones, no de implementaciones concretas. Es la regla, la guía.

DI (Técnica)

Es una técnica de implementación. Dice cómo lo haces: inyectando las dependencias desde fuera (por constructor, setter o interfaz) en lugar de que la clase las cree ella misma.

Puedes usar inyección de dependencias sin cumplir el DIP (por ejemplo, inyectando una clase concreta en lugar de una interfaz). Y puedes cumplir el DIP teóricamente sin usar inyección de dependencias (aunque en la práctica casi siempre van juntos).

Lo ideal es usar ambos: el DIP te dice que dependas de abstracciones, y la DI te da el mecanismo para inyectar la implementación concreta desde fuera.

Formas de inyectar dependencias

// 1. Por constructor (la más común y recomendada)
class UserService
{
    public function __construct(
        private UserRepository $repository
    ) {}
}

// 2. Por setter
class UserService
{
    private UserRepository $repository;

    public function setRepository(UserRepository $repository): void
    {
        $this->repository = $repository;
    }
}

// 3. Por interfaz
interface RepositoryAware
{
    public function setRepository(UserRepository $repository): void;
}

La inyección por constructor es la más recomendada porque garantiza que la dependencia siempre está disponible desde el momento en que el objeto se crea. No hay riesgo de llamar a un método antes de haber inyectado la dependencia.


El papel de los contenedores de dependencias

En proyectos reales, no vas creando objetos a mano con new. Los frameworks modernos como Symfony, Laravel, Spring o .NET traen un contenedor de dependencias (también llamado contenedor IoC - Inversion of Control) que se encarga de:

  1. Registrar qué implementación concreta corresponde a cada interfaz
  2. Resolver automáticamente las dependencias cuando creas un objeto
  3. Gestionar el ciclo de vida de los objetos (singleton, request-scoped, etc.)

Por ejemplo, en Laravel:

// Registro: "cuando alguien pida un PaymentGateway, dale un StripeGateway"
$this->app->bind(PaymentGateway::class, StripeGateway::class);

// Ahora Laravel inyecta automáticamente StripeGateway
// en cualquier clase que pida PaymentGateway en su constructor

Y en Symfony:

# services.yaml
services:
    App\Domain\PaymentGateway:
        class: App\Infrastructure\StripeGateway

Cambiar de Stripe a PayPal es cambiar una línea de configuración. Ni una sola línea de lógica de negocio se toca.


¿Cómo detectar que estás violando el DIP?

Para detectar que no se está cumpliendo el DIP puedes fijarte en estas señales:

Uso de new dentro de clases de alto nivel

Si tu clase de negocio hace new EmailService() o new MySqlConnection() dentro de sus métodos o constructor, está creando sus propias dependencias. Eso es un acoplamiento directo con la implementación concreta.

El constructor recibe tipos concretos en lugar de interfaces

Si el type hint del constructor es EmailService en lugar de NotificationService, estás dependiendo de un detalle de implementación.

Cambiar una clase de bajo nivel obliga a cambiar clases de alto nivel

Si al cambiar el proveedor de email tienes que tocar la clase que gestiona los pedidos, algo está mal. La lógica de negocio no debería enterarse de esos cambios.

Tests difíciles de escribir

Si para testear una clase necesitas una base de datos real, un servidor SMTP funcionando o una API externa disponible, es señal de que las dependencias están acopladas. Con el DIP bien aplicado, mockearlo todo debería ser trivial.

Import de clases de infraestructura en la capa de dominio

Si tu clase de dominio (UserService, OrderProcessor) importa clases de infraestructura (MySqlConnection, SmtpMailer, StripeClient), la dependencia va en la dirección incorrecta.


Errores comunes al aplicar el DIP


Relación con otros principios SOLID

El DIP es, en muchos sentidos, el principio que cierra el círculo de SOLID. Está conectado con todos los demás:

DIP + SRP

Clases con una sola responsabilidad son más fáciles de abstraer. Si una clase hace 5 cosas, ¿qué interfaz le pones?

DIP + OCP

El DIP es la base técnica del OCP. Dependiendo de abstracciones puedes añadir implementaciones nuevas sin modificar nada.

DIP + LSP

Para que el DIP funcione, las implementaciones concretas deben respetar el contrato de la interfaz. Sin LSP, el DIP se desmorona.

DIP + ISP

Interfaces pequeñas y específicas hacen que las abstracciones del DIP sean más útiles. Nadie quiere depender de una interfaz gorda.

De hecho, si miras la serie completa de SOLID, el DIP es como el pegamento que une todo: usa abstracciones (ISP) que permiten extensión sin modificación (OCP), donde las implementaciones son sustituibles (LSP) y cada una tiene una responsabilidad clara (SRP).


¿Cuándo NO aplicar el DIP?

Como con el resto de principios SOLID, el DIP tiene sus matices:

Recuerda: El DIP es especialmente valioso en las fronteras del sistema (bases de datos, APIs externas, servicios de email, pasarelas de pago) donde los cambios de proveedor son reales. Para dependencias internas estables, evalúa si la abstracción aporta valor.


Conclusión

El Principio de Inversión de Dependencias cierra la serie SOLID con una idea muy potente: tu lógica de negocio no debe saber ni importarle cómo se hacen las cosas, solo qué se hace. Las bases de datos, los servicios de email, las pasarelas de pago… todo eso son detalles que pueden cambiar. Lo que no debería cambiar es tu lógica de negocio.

La clave técnica es sencilla: depende de interfaces, no de clases concretas. Y usa inyección de dependencias para que sea el mundo exterior quien decida qué implementación concreta se usa en cada momento.

Con el DIP bien aplicado, tu código se vuelve como un enchufe universal: le puedes conectar cualquier cosa que cumpla el estándar y funciona. Sin reformas, sin sustos, sin tocar nada de lo que ya funciona.

Espero que se haya entendido ejeejjeje EA nos beermos! 🍻


Pon a prueba lo aprendido

1. ¿Quién formuló el Principio de Inversión de Dependencias?

2. ¿Cuál es la primera regla del DIP?

3. En el ejemplo de notificaciones, ¿por qué Notification dependiendo de EmailService viola el DIP?

4. ¿Cuál es la diferencia entre DIP e Inyección de Dependencias (DI)?

5. ¿Cuál es la forma más recomendada de inyectar dependencias?

6. ¿Cuál de estas es una señal de que estás violando el DIP?

7. ¿Qué papel cumple el DIP respecto al resto de principios SOLID?

8. ¿Cuál de estos es un error común al aplicar el DIP?