🏗️ 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.

¿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”?
La lógica de negocio. Lo que decide qué se hace. Ejemplo: "enviar una notificación al usuario", "procesar un pedido", "calcular el descuento".
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:
- Flexibilidad: Puedes cambiar implementaciones (base de datos, proveedor de email, pasarela de pago…) sin tocar la lógica de negocio. Solo cambias la clase concreta que inyectas.
- Testabilidad: Puedes mockear las dependencias fácilmente en los tests. En lugar de necesitar una base de datos real, inyectas un mock que implementa la interfaz.
- Mantenibilidad: La lógica de negocio no se ensucia con detalles de infraestructura. Cada capa tiene su responsabilidad.
- Reutilización: Una clase de alto nivel que depende de una interfaz se puede reutilizar con cualquier implementación concreta.
- Escalabilidad del equipo: Un desarrollador puede trabajar en la lógica de negocio y otro en la implementación concreta del servicio de email, porque ambos trabajan contra la misma interfaz.
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?
UserServicecrea su propia dependencia (hacenew MySqlConnection). Eso es un acoplamiento brutal.- Si mañana quieres cambiar a PostgreSQL, Redis o MongoDB, tienes que abrir y modificar
UserService. - Para testear necesitas una base de datos MySQL real. No puedes mockear nada.
- Las credenciales de la base de datos están en la clase de negocio. Menudo estropicio.
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:
Es un principio de diseño. Dice qué debes hacer: depender de abstracciones, no de implementaciones concretas. Es la regla, la guía.
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:
- Registrar qué implementación concreta corresponde a cada interfaz
- Resolver automáticamente las dependencias cuando creas un objeto
- 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
- Crear interfaces innecesarias: Si una implementación concreta solo va a tener una implementación y no hay planes de cambiarla nunca, crear una interfaz puede ser over-engineering. El DIP es para cuando necesitas flexibilidad real, no para cumplir un checklist.
- Confundir DIP con DI: Como vimos antes, son cosas diferentes. Inyectar una clase concreta por constructor es DI pero no cumple el DIP. Necesitas la abstracción (interfaz) para que el DIP se cumpla.
- Abstracciones que copian la implementación: Si tu interfaz
UserRepositorytiene métodos comoexecuteSqlQuery()ofindByMongoFilter(), la abstracción está contaminada por los detalles de la implementación. Una buena abstracción debe ser agnóstica a la tecnología. - No invertir en la dirección correcta: El DIP dice que los detalles dependen de las abstracciones, no al revés. Si tu interfaz cambia cada vez que cambia la implementación, la inversión está al revés.
- Crear capas de abstracción demasiado profundas: Si para llegar de tu controlador a la base de datos hay que pasar por 7 interfaces y 7 implementaciones, probablemente te has pasado. La abstracción debe aportar valor, no burocracia.
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:
Clases con una sola responsabilidad son más fáciles de abstraer. Si una clase hace 5 cosas, ¿qué interfaz le pones?
El DIP es la base técnica del OCP. Dependiendo de abstracciones puedes añadir implementaciones nuevas sin modificar nada.
Para que el DIP funcione, las implementaciones concretas deben respetar el contrato de la interfaz. Sin LSP, el DIP se desmorona.
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:
- Scripts pequeños y utilidades: Si es un script de consola que se ejecuta una vez, no necesitas interfaces ni abstracciones. Acopla con alegría y ya está.
- Prototipos y MVPs: Cuando estás validando una idea, el acoplamiento directo es más rápido. Ya refactorizarás si la idea funciona.
- Dependencias estables que nunca van a cambiar: Si llevas 5 años usando la misma librería de logs y no hay planes de cambiarla, crear una interfaz intermedia puede ser ruido innecesario.
- Cuando la abstracción añade más complejidad que la que resuelve: Si para inyectar un
DateFormatternecesitas una interfaz, un binding en el contenedor y una factory, quizá unnew DateFormatter()directo es más sensato.
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?