Comunidad de diseño web y desarrollo en internet online

POO: Inyección de dependencias (II)

En mi artículo anterior aprendimos inyección de dependencias en Laravel, un concepto básico de la POO, que permite a unos objetos interactuar con otros sin crear una dependecia como tal, la cual se inyecta a través del constructor o de un “método setter”. Veamos un ejemplo:

Código :

<?php

$db = new MysqlDatabase($host, $user, $password, $options);
$user = new User($db);

$users = $user->selectAll();


Usamos un objeto para conectarnos a una base de datos MySQL y luego lo “inyectamos” a un objeto User. Si luego quisiéramos usar Oracle en teoría sólo bastaría inyectar un objeto OracleDatabase (con la misma interface de MysqlDatabase). Por lo tanto el manejo de usuarios no está "atado" a MySQL sino que en teoría funcionaría con otras bases de datos.

Si no has leído el artículo anterior hazlo ahora, no te preocupes que el equipo secreto :swat: de Cristalab mantendrá el servidor funcionando mientras vuelves


El Problema de la inyección de dependencias


Lo anterior parece genial, pero ¿Cuál es el truco? Imagina un sistema con 50 tablas distintas y tener que hacer esto cada vez que quieras usar un objeto del ORM:

Código :

// Muy dificil instanciar un objeto
$db = new MySQLDatabase($host, $user, $password, $options);
$posts = new Post($db);


Contenedores de inyección de dependencia o DIC (siglas en inglés)


Si ya el nombre “inyección de dependencias” da miedo, es probable que “contenedor de inyección de dependencias” te haga saltar de la silla y huir del susto, pero en realidad es muy sencillo, tan sencillo que escribiré uno en cinco minutos.

Treinta minutos después...

En un contenedor, usualmente cada clase/objeto es representado por un alias o nombre que es diferente del nombre de la clase original. Por ejemplo el alias de MysqlDatabase u OracleDatabase sería sólo “db”. La clase “User” podría ser “user” (en minúsculas) y así…

Para instanciar una clase usando el contenedor haremos algo como esto


Código :

$app->make('db'); // O esto:
$app->make('user'); 


En vez de esto:

Código :

$db = new MysqlDatabase($host, $user, $password, $options);
$user = new User($db);


El contenedor se encargará de resolver el objeto por nosotros (en este caso instanciar -si hace falta- y pasar el objeto de base de datos al usuario).

Por supuesto hay que “enseñar” a nuestro contenedor cómo hacerlo, pero la ventaja es que sólo le enseñas una vez.

Enseñar al contenedor cómo resolver un objeto



A través de una closure


Esta opción me parece la más flexible, tienes un bloque de código para indicarle a PHP cómo debe instanciar la clase (configurar parámetros, etc.) El código dentro de la closure sólo será ejecutado si llamas a $app->make para el alias en cuestión.

Código :

$app->bind('db', function (Container $app, $options) {
    return new MysqlDatabase('local', 'user', 'password', array('engine' => 'innodb'));
});

// Si no se llama a app->make('db') MysqlDatabase nunca se instanciaría:
$db = $app->make('db'); 


Indicando directamente el nombre de la clase


En este caso, al momento de “hacer” la clase con $app->make tendríamos que pasar el array con los parámetros del constructor, útil si cada instanciación es diferente (aunque en mi contenedor la opción del closure también acepta parámetros).

Código :

$app->bind('db', 'MysqlDatabase');
//Crea una instancia de la base de datos, el array seran los parametros pasados al constructor de MysqlDatabase en este caso:
$db = $app->make('db', array('local', 'user', 'password', array('engine' => 'innodb')));
var_dump($db);


Construyendo un objeto fuera del constructor


Menos óptima ya que tenemos que instanciar el objeto sólo para pasarlo al contenedor, haga falta o no en la aplicación como tal:

Código :

$app->instance('db', new MysqlDatabase('local', 'user', 'password', array('engine' => 'innodb')));
$db = $app->make('db');
var_dump($db);


Una instancia “singleton”


Si necesitamos asegurarnos que un objeto se instancie una sola vez, entonces en vez del método “bind” usamos el método “singleton” con los mismos parámetros, ejemplo:

Código :

$app->singleton('db', function (Container $app, $options) {
    return new MysqlDatabase('local', 'user', 'password', array('engine' => 'innodb'));
});


En resumen tenemos 4 métodos. “bind” “instance” y “singleton” que permiten “atar” un objeto al contenedor, y “make” que permite “resolverlo” y que usaremos cada vez que necesitemos dicho objeto. Además recuerden que en el contenedor todos los objetos tendrán un “alias”, aunque usemos otra clase los alias siempre serán los mismos, lo cual es muy conveniente.

La clase la hice con una interfaz similar a la de Laravel a propósito, aunque ésta es más sencilla y la intención no es que la usen en sus proyectos sino que vean cuán simple puede ser un contenedor, por otro lado si no la entienden del todo no hay problema, más abajo les dejo ejemplos de su implementación (que sí me interesa que aprendan):

Código :

<?php

class Container {

    protected $singleton = array();
    protected $bindings = array();

    /**
     * Bind an object to the container
     * @param $name string alias of the class inside the container
     * @param $resolver string or closure use to create a new class
     * @param bool $singleton whether you need only one instance of the object or several instances
     */
    public function bind($name, $resolver, $singleton = false)
    {
        $this->bindings[$name] = array(
            'resolver'  => $resolver,
            'singleton' => $singleton
        );
    }

    /**
     * Alias of the bind method using the singleton option
     * @param $name string alias of the class inside the container
     * @param $resolver string or closure use to create a new class
     */
    public function singleton($name, $resolver)
    {
        $this->bind($name, $resolver, true);
    }

    /**
     * Bind an instance of an object to our container
     * @param $name string alias of the class inside the container
     * @param $object object to be bound
     */
    public function instance($name, $object)
    {
        $this->singleton[$name] = $object;
    }

    /**
     * Create or get an object from the container
     * It may be resolved or retrieved from the singleton array
     * @param $name string alias of the class inside the container
     * @param array $options options to instantiate a new class
     * @return object or null
     */
    public function make($name, $options = array())
    {
        if (isset ($this->singleton[$name]))
        {
                return $this->singleton[$name];
        }

        if ( ! isset ($this->bindings[$name]))
        {
                return null;
        }

        $resolver = $this->bindings[$name]['resolver'];

        if (is_string($resolver))
        {
            $reflection = new ReflectionClass($resolver);
            $object = $reflection->newInstanceArgs($options);
        }
        elseif ($resolver instanceof Closure)
        {
            $object = $resolver($this, $options);
        }

        if ($this->bindings[$name]['singleton'])
        {
            $this->singleton[$name] = $object;
        }

        return $object;
    }

}


Ahora creemos algunos objetos tontos sólo para poder hacer uso del contenedor:

Código :

class MysqlDatabase {

    protected $connection;

    public function __construct($host, $user, $password, $options)
    {
        $this->connection = compact('host', 'user', 'password', 'options');
    }
}

class User {
    protected $db;
    public function __construct($db)
    {
        $this->db = $db;
    }
}


Ok, ahora hagamos el ejemplo del inicio de este post pero usando nuestro contenedor.

1. Instanciamos:

Código :

$app = new Container();


2. Le explicamos al contenedor cómo resolver el alias “db”:

Código :

$app->singleton('db', function ($app) {
    return new MysqlDatabase('local', 'me', 'pony123456', array('engine' => 'innodb'));
});


3. Le explicamos al contenedor cómo resolver el alias “user”. Noten que dentro del closure hacemos uso del mismo Container para resolver la DB (dado que “user” necesita “db”):

Código :

$app->bind('user', function ($app) {
   $db = $app->make('db'); //devolver la DB almacenada en el contenedor
   return new User($db);
});


4. Por último haremos (make) un usuario (user) y lanzamos un var_dump:

Código :

$user = $app->make('user');
var_dump($user);


¡Si corren el ejemplo verán que el objeto User fue creado y que dentro de éste se encuentra el objeto MysqlDatabase!

Al usar un contenedor todas las dependencias de un objeto se definen una vez y son reusables y si necesitas cambiar la forma como es instanciado un objeto sólo cambias en un lugar de tu aplicación y listo.

¿Cuando no usar un contenedor ni inyección de dependencias?


En el caso de que tu aplicación sea muy sencilla es muy posible que no te haga falta.

¿Al usar un contenedor mi aplicación no depende ahora de éste?


Es irónico, la idea es eliminar dependencias y creamos otra, pero es el menor de los males, con él todas las demás clases de nuestra aplicación quedan independientes y pueden ser reemplazadas fácilmente.

Otras preguntas en los comentarios o sígueme en Twitter. Si te gustó o no, entendiste o no, me gustaría saberlo.

En el próximo artículo ahora sí me toca hablarles de cómo funciona esto dentro de Laravel, además de cómo crear una Facade, un ServiceProvider y otros conceptos propios de Laravel.

¡Saludos a todos!

¿Sabes SQL? ¿No-SQL? Aprende MySQL, PostgreSQL, MongoDB, Redis y más con el Curso Profesional de Bases de Datos que empieza el martes, en vivo.

Publica tu comentario

o puedes...

¿Estás registrado en Cristalab y quieres
publicar tu URL y avatar?

¿No estás registrado aún pero quieres hacerlo antes de publicar tu comentario?

Registrate