Ejemplo de inyección de depencias en PHP

Un concepto muy simple y, a la vez, muy potente de la Programación Orientada a Objetos es la inyección de dependencias.

Diría que se trata de la piedra angular de cualquier sistema desacoplado y, por lo tanto, fácil de evolucionar y testear.

Y lo mejor de todo es que su implementación es realmente sencilla. En otras palabras: puras ventajas.

Acompañame a darle una mirada más de cerca.

Qué es una dependencia en POO

Como en muchas ocasiones en este mundo de la informática, un mismo término puede referirse a diferentes cosas y, para saber realmente de qué estamos hablando es necesario poner en claro el contexto.

Por ejemplo, si hablamos de las dependencias de una aplicación, nos estaremos refiriendo a aquellas librerías necesarias para que dicha aplicación funcione. Tema que está mejor tratado en este artículo.

En este contexto, cuando hablo de dependencia me refiero a aquellos objetos de los que otros dependen para realizar sus tareas.

Te pongo un ejemplo que me compartió un lector de mi newsletter y me viene muy bien para ilustrar este concepto:

<?php
namespace Controllers;

use ApplicationService\BillFinderService;
use ApplicationService\TotalDeductiblesByYearService;
use Dao\DeductibleDao;
use Infraestructure\Connection\ConnectionMySql;

class FrontController extends Controller{

    private $connection;

    public function __construct()
    {
        parent::__construct();
        $this->connection = new ConnectionMySql();
    }
    
    public function home($year = null){
        if (!$year) {
            $year = new \DateTime();
            $year = $year->format('Y');
        }
        $billFinderService = new BillFinderService($this->connection);
        $totalDeductibleByYear = new TotalDeductiblesByYearService($this->connection);
        $lastBillsRegistered = $billFinderService->findByYear($year);
        $totalByDeductible = $totalDeductibleByYear->getTotalByYear($year);
        echo $this->templates->render('index', [
            'title' => 'Main menu',
            'lastBillsRegistered' => $lastBillsRegistered,
            'totalByDeductible' => $totalByDeductible,
            'year' => $year,
            'years' => [2023, 2024, 2025]
        ]);

    }
    
    public function error404(){
        
        echo $this->templates->render('404');
    
    }

    public function getBillsByDeductibleIdAndYear($deductibleId, $year){

        $totalDeductibleByYear = new TotalDeductiblesByYearService($this->connection);
        $bills = $totalDeductibleByYear->getBillsByDeductibleIdAndYear($deductibleId, $year);
        $deductibleDao = new DeductibleDao($this->connection);
        $deductible = $deductibleDao->findById($deductibleId);
        echo $this->templates->render('bills-by-deductible-and-year', [
            'title' => "Bills by deductible by year",
            'deductibleName' => $deductible->getName(),
            'bills' => $bills,
            'year' => $year
        ]);
    }
}

Aquí tenemos una clase llamada FrontController que depende, entre otras, de:

  • ConnectionMySql
  • BillFinderService
  • TotalDeductiblesByYearService
  • DeductibleDao

¿Por qué digo que FrontController depende de estas clases? Porque, para lograr sus objetivos se sirve de métodos de ellas, ejemplo:

public function home($year = null){
        ...
        $lastBillsRegistered = $billFinderService->findByYear($year);
        $totalByDeductible = $totalDeductibleByYear->getTotalByYear($year);
        ...
    }

Para que este código pueda ejecutarse, será necesario que las variables que contienen referencias a dichos objetos ($billFinderService y $totalDeductibleByYear) hayan sido previamente asignadas a instancias de sus respectivas clases.

Algo que se está haciendo precisamente al comienzo del método:

public function home($year = null){
...
        $billFinderService = new BillFinderService($this->connection);
        $totalDeductibleByYear = new TotalDeductiblesByYearService($this->connection);
...
}

Entonces… ¿cuál es el problema?

Bueno, más que un problema es una oportunidad perdida.

Implementando inyección de dependencias

Empiezo por una pregunta que seguramente se pasó por alto al escribir este código:

¿Es necesario crear una nueva instancia de cada colaborador cada vez que se llama al método?

Ya dije que crear la instancia es inevitable pero la pregunta apunta a determinar si el mejor momento (O lugar, como quieras verlo) para hacerlo es aquí.

¿Qué pasaría si modificáramos ligeramente el código para que crear las dependencias fuera responsabilidad de alguien más?

Algo como:

class FrontController extends Controller{

    private $connection;
    private $billFinderService;
    private $totalDeductiblesByYearService;

    public function __construct(ConnectionMySql $connection, BillFinderService $billFinderService, TotalDeductiblesByYearService $totalDeductiblesByYearService)
    {
        parent::__construct();
        $this->connection = $connection;
        $this->billFinderService = $billFinderService;
        $this->totalDeductiblesByYearService = $totalDeductiblesByYearService;
    }
    
    public function home($year = null){
        if (!$year) {
            $year = new \DateTime();
            $year = $year->format('Y');
        }
        echo $this->templates->render('index', [
            'title' => 'Main menu',
            'lastBillsRegistered' => $this->billFinderService->findByYear($year),
            'totalByDeductible' => $this->totalDeductiblesByYearService->getTotalByYear($year),
            'year' => $year,
            'years' => [2023, 2024, 2025]
        ]);
    }
...
}

Con este pequeño cambio pateamos la responsabilidad hacia arriba. Ahora es problema del cliente de la clase FrontController conseguir instancias válidas para pasarle (Ya sea creándolas él mismo o pidiéndoselas a algún otro componente).

Esta forma de vincular una clase con sus colaboradores es lo que se conoce como inyección de dependencias.

Ventajas de la Inyección de Dependencias

Ahora que el tecnicismo está claro, veamos por qué vale la pena diseñar de esta manera.

Para empezar, porque el costo de hacerlo es muy bajo. Si mirás el código inicial y lo comparás con el actual diríamos que mucha diferencia no hay, ¿cierto?

Pero vamos a ver los aspectos más jugosos del tema:

Performance

Este punto depende mucho del contexto particular de tu aplicación. Si el método que crea los objetos no se ejecuta con mucha frecuencia la diferencia no va a ser mucha, pero si es al revés, te vas a estar ahorrando la creación de objetos idénticos, lo que implica menos gasto de memoria y de tiempo de procesamiento.

Es cierto que en PHP esta ventaja se diluye por el hecho de que cada request se procesa en su propio hilo pero aún así, algo se puede ganar.

Versatilidad

Este punto me parece el más interesante ya que es el que da lugar a los dos siguientes.

Pensalo así: al dejar de lado la decisión de qué instancia específica del colaborador va a usar tu clase dejás abierta la puerta a que un mismo método pueda usarse en diferentes contextos.

Imaginá un escenario en el que existe no una si no dos instancias de la clase BillFinderService y que, dependiendo de alguna configuración se necesita usar una o la otra.

Si el código está escrito usando Inyección de Dependencias, implementar ese cambio no supone ningún inconveniente, basta con cambiar el parámetro que se usa para construir FrontController y listo.

Es más, dado que estás trabajando con Programación Orientada a Objetos, bien podrías pasarle una instancia de una clase derivada de BillFinderService y todo seguiría funcionando como si tal cosa.

De hecho, este mecanismo es el que posibilita mucho de los principios SOLID.

Testabilidad

Un caso muy claro en el que viene bien poder cambiar la instancia de un colaborador por otra es a la hora de hacer tests unitarios.

Si no podés aislar de sus la clase que querés testear, las cosas se te van a complicar.

Una vez que lo viste…

… no podés des-verlo.

No te digo que vayas corriendo a desarmar todos tus proyectos que no usan Inyección de Dependencias pero sí te propongo que le des una segunda mirada teniendo en cuenta lo que podrías ganar si lo implementás y, de a poco, lo vayas incorporando a tu día a día.

mchojrin

Por mchojrin

Ayudo a desarrolladores PHP a afinar sus habilidades técnicas y avanzar en sus carreras

¿Te quedó alguna duda? Publica aca tu pregunta

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.