Orientação a objetos

Container de injeção de dependência (DI Container)

No artigo Entendendo Injeção de Dependência vimos sobre o que é injeção de dependência, seu funcionamento e como se dá a sua aplicação.

Injetar dependências pode se tornar uma tarefa tediosa quando se têm muitas classes envolvidas. Antes de injetar uma dependência ela precisa ser instanciada. Portanto, não cuidamos apenas da “injeção”, precisamos também ter o conhecimento de quais objetos ela precisa para funcionar.

Um container de injeção de dependência (DI Container) gerencia e automatiza as instanciações. Dizemos pra ele como um objeto deve ser criado (essa é a parte que nos toca, o nosso conhecimento sobre ele) e então sempre que o precisarmos, basta que usemos o container para obtê-lo.

Esse artigo utilizará PHP como linguagem base para os exemplos, no entanto, há de se destacar, o conceito é agnóstico à linguagem. Se PHP não é a sua “praia”, não tem problema, você pode pesquisar por “dependency injection container C#” ou por qualquer outra linguagem que encontrará importantes referências e implementações.

PHP Básico
Curso de PHP Básico
CONHEÇA O CURSO

No cenário dos frameworks PHP, os mais utilizados pelo mercado (Symfony, Laravel etc) implementam, cada um, o seu próprio container e, assim o fazem, pois seria impraticável manter os objetos “conversando” pelo detrimento da enorme quantidade de instanciações repetidas que precisariam ser feitas no ciclo de uma simples requisição. Esses frameworks possuem centenas de classes e não ter por onde resolver as dependências e reutilizá-las sob demanda, é impensável.

Um container nada mais é do que um “mapa” das dependências que o projeto usa, em termos práticos, é uma classe que armazena o conhecimento sobre seus objetos e suas dependências.

Uma implementação genérica de um container (para que possamos assimilar melhor):

<?php
declare(strict_types=1);

use Closure;

final class Container
{
    private $instances = [];

    public function set(string $id, Closure $closure) : void
    {
        $this->instances[$id] = $closure;
    }

    public function get($id) : object
    {
        return $this->instances[$id]($this);
    }
}

O método set() armazena no array $instances a identificação/nome de uma dependência e a lógica por trás da sua instanciação.

Por exemplo:

$container = new Container();

$container->set('db', function() {
    return new DatabaseAdapter('mysql:dbname=test;host=127.0.0.1', 'root', '');
});

O método get() é utilizado para resolver e retornar a instância do objeto. Observe que a instanciação não se dá no set() e sim sob demanda, na hora que precisamos daquele objeto, ou seja, na hora que usamos get().

Observe essa linha:

return $this->instances[$id]($this);

Está executando a função anônima que definimos (a que resolve a dependência) e está passando para ela como único parâmetro a instância da classe Container ($this dentro daquele contexto refere-se à instância da classe em operação). Quando a função anônima é executada temos como retorno um novo objeto.

Lembra o que a nossa função anônima retorna?

$container->set('db', function() {
    return new DatabaseAdapter('mysql:dbname=test;host=127.0.0.1', 'root', '');
});

Pois bem, saindo um pouco dessas nuances relacionadas à implementação, na prática temos:

<?php

$container = new Container();

$container->set('db', function() {
    return new DatabaseAdapter('mysql:dbname=test;host=127.0.0.1', 'root', '');
});

// Imprime a instância de um objeto do tipo 'DatabaseAdapter'
var_dump($container->get('db'));

Vamos aumentar o nosso leque de objetos e suas dependências?

<?php

// Container
$container = new Container();

// Objeto que recupera configurações salvas em algum tipo de arquivo.
$container->set('config', function() {
    return new Config();
});

// Veja que agora *db* têm como dependência um objeto da classe Config
// e o utiliza para obter os dados de acesso ao BD.
$container->set('db', function($container) {
    $config = $container->get('config')->getConfig('db');

    return new DatabaseAdapter($config['dsn'], $config['user'], $config['password']);
});

// A classe UserRepository precisa de uma instância de *db*
// para fazer consultas ao banco de dados.
$container->set('user.repository', function($container) {
    return new UserRepository($container->get('db'));
});

Veja que temos uma cadeia de objetos interdependentes. Imagine agora a situação de termos três diferentes controladores, sendo instanciados em momentos diferentes no ciclo de execução da aplicação e todos eles necessitando da instância de UserRepository para recuperar informações sobre um usuário?

Com o container definido tudo o que teríamos que fazer:

// ... 

$indexController = new IndexController(
    $container->get('user.repository')
);

$userController = new UserController(
    $container->get('user.repository')
);

$registerController = new RegisterController(
    $container->get('user.repository')
);

Observe que estamos passando para o construtor dos controladores uma instância de user.repository. O container lidará de nos retornar o objeto que queremos injetar a partir dessa identificação.

Um container pode implementar ainda mais comportamentos. Por exemplo, você deve ter percebido que a execução de $container->get('user.repository') vai sempre instanciar um novo objeto. Se executarmos 100 vezes, serão 100 novos objetos criados.

No entanto, algumas dependências são definitivas o suficiente para que não haja a necessidade de sempre instanciarmos um novo objeto delas. Nesses casos podemos ter um novo método no container para definir uma dependência compartilhada (singleton), onde uma única instância é gerada e retornada durante todo o ciclo de execução da aplicação.

O objetivo primário desse artigo não é se preocupar tanto com a implementação, mas com o conceito. No entanto, é importante que desenvolvamos alguns “protótipos” para uma melhor assimilação.

Vejamos então a implementação do nosso container com o novo método singleton():

<?php
declare(strict_types=1);

use Closure;

final class Container
{
    private $instances = [];

    public function set(string $id, Closure $closure) : void
    {
        $this->instances[$id] = $closure;
    }

    public function get($id) : object
    {
        return $this->instances[$id]($this);
    }

    public function singleton(string $id, Closure $closure) : void
    {
        $this->instances[$id] = function() use($closure) {
            static $resolvedInstance;

            if(null !== $resolvedInstance) {
                $resolvedInstance = $closure($this);
            }

            return $resolvedInstance;
        };
    }
}

Nesse método a lógica de resolução tem uma camada a mais, nela verificamos:

  1. Essa dependência já foi resolvida (devidamente instanciada) anteriormente?
    1.1. Não? Então assim o faremos.
    1.2. Já foi? Então vamos retorná-la do “cache” da variável estática (tipo de variável que não perde o valor mesmo quando o nível de execução do programa deixa o escopo).

Voltando ao contexto do nosso exemplo das classes controladoras que recebem a injeção do objeto UserRepository, podemos agora otimizar a resolução dessa dependência usando o método singleton() ao invés do set():

$container->singleton('user.repository', function($container) {
    return new UserRepository($container->get('db'));
});

Agora, na injeção dessa dependência nas classes dos controladores teremos sempre o mesmo objeto sendo compartilhado entre as diferentes instâncias:

// ... 

$indexController = new IndexController(
    $container->get('user.repository')
);

$userController = new UserController(
    $container->get('user.repository')
);

$registerController = new RegisterController(
    $container->get('user.repository')
);

É comum referências a DI Container (Dependency Injection Container) ou a IoC Container (Inversion of Control Container). São a mesma coisa: containers de injeção de dependência. A diferença é que um IoC Container precisa conseguir (inclusive) resolver as dependências a partir do mapeamento de interfaces (ele as resolve a partir de abstrações em detrimento às implementações concretas). É possível programar um IoC Container para resolver determinado objeto se uma determinada interface for requerida.

O container do Laravel Framework é um bom caso de uso. Ele é bastante encorpado e consegue resolver de diferentes formas. Ele é considerado um IoC Container, mas nada de errado se o referirmos como sendo um DI Container.

Veja esse trecho do core do Laravel Framework:

// The database manager is used to resolve various connections, since multiple
// connections might be managed. It also implements the connection resolver
// interface which may be used by other components requiring connections.
$this->app->singleton('db', function ($app) {
    return new DatabaseManager($app, $app['db.factory']);
});

Ele possui diversos services providers que configuram dezenas de dependências. E o container é compartilhado e utilizado por quase todas as classes do Framework.

(Para visualizar o Código-fonte do IoC Container do Laravel, clique aqui).

Laravel 5.1 - Framework PHP
Curso de Laravel 5.1 - Framework PHP
CONHEÇA O CURSO

Outras importantes implementações de containers para PHP:

Recomendação de leitura:

Novo artigo da série, sobre resolução automática de dependências. Nele , vamos incrementar o container criado aqui nesse artigo. Portanto, recomendo a leitura:

Até a próxima!

PHP Avançado
Curso de PHP Avançado
CONHEÇA O CURSO

Padrões de projeto: o que são e o que resolvem

Design Patterns (comumente relacionados na literatura de TI como “Padrões de Projeto”) são soluções para problemas comuns que encontramos no desenvolvimento ou manutenção de um software orientado a objetos (não são tão bem aplicáveis em outros paradigmas). Olhando assim parece ser algo realmente bem “requintado”, não é?

Vamos tentar entender sob outra perspectiva: imagine você no futuro, daqui 15 anos, certamente terá passado por N problemas, repetidamente e em diferentes projetos. A tendência natural é que você crie soluções comuns (mas elegantes, assim esperamos, né? :P) para resolvê-los de tal forma que você consiga aplicá-las no futuro em outros projetos. Essas soluções, criadas por você, mesmo que não compartilhadas à literatura (livros ou artigos acadêmicos), podem ser consideradas como sendo Design Patterns, sim, padrões de projeto criados por você para resolver os seus problemas. Nada mais justo, não?

Sorte do dia: Não precisamos esperar uma dezena ou mais de anos para acumularmos experiência (e, consequentemente, perda de cabelo) para que tenhamos desenvolvido eficientes soluções para os nossos softwares.

Isaac Newton certa vez dissera:

Se cheguei até aqui foi porque me apoiei no ombro de gigantes.

Lhe convido a absorver essa ideia que nos foi “presenteada” séculos atrás por esse brilhante cientista (alguns diriam que ele foi um “full stack” por ter sido físico, matemático, astrônomo, filósofo, teólogo, alquimista e coisas mais. :P).

Engenheiros de softwares por décadas desenvolveram padrões de projeto para resolver problemas comuns. Por que não dar uma chance de conhecê-los e, quem sabe, utilizá-los?

Parafraseando o filósofo e professor da USP, Clóvis de Barros Filho, em uma palestra muito especial:

Os caras escreveram as soluções, véio. Eles tiveram que tirar o negócio do zero! Só precisamos entender e aplicar a bagaça!

Ruby on Rails Intermediário
Curso de Ruby on Rails Intermediário
CONHEÇA O CURSO

Como tudo começou

Toda a inspiração por trás do nosso conceito de “padrões de projeto” veio, na realidade, da arquitetura (sim, àquela área de conhecimento que projeta e arquiteta ambientes), de um livro chamado “A Patter Language”, escrito por Christopher Alexander, Sara Ishikawa e Murray Silverstein, foi o que primeiramente lançou a ideia de “linguagem de padrões”. O livro teve como propósito apresentar centenas de padrões sobre como cidades, bairros, casas e ambientes no geral poderiam ser projetados.

O primeiro grande trabalho da área de desenvolvimento de software que absorveu tal ideia coletando e desenvolvendo dezenas de padrões para softwares se deu em 1994 em um livro chamado “Design Patterns: Elements of Reusable Object-Oriented Software”, escrito por Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides. Originalmente o livro discutiu 23 padrões de projetos. Os autores e o livro causaram tanto impacto que começaram a ser chamados e reconhecidos por Gang of Four (GoF) (gangue dos quatro) e tais padrões começaram a ser intitulados de GoF Patterns (Padrões GoF).

Olhando assim parece até que em uma noite chuvosa a “gangue dos quatro” reunida na garagem de um deles se acometeu de uma sobrenatural inspiração e então gritaram ==”Eureka! Eureka!”== e logo já escreveram os 23 padrões, não é? É, só parece. Os padrões, na realidade, são soluções de problemas retirados de ==códigos reais==.

Categorias dos padrões GoF

Os padrões GoF foram divididos e categorizados de acordo com a natureza do problema que eles resolvem (ou ao menos tentam, uma vez que software, como bem você sabe, é algo com “vida própria” haha):

Padrões de Criação: Tem como objetivo abstrair a instanciação de objetos. Com eles, o sistema vai solicitar um objeto de um determinado tipo e o terá prontinho, sob demanda, sem nem se preocupar com as nuances da criação. Fazendo um paralelo com o mundo real, uma empresa automobilística quando precisa de amortecedores, ela terceiriza (solicita-os) e então os instala em seus carros, sem se preocupar com o todo envolvido na criação desse componente.

Padrões estruturais: Os padrões dessa categoria se preocupam em melhor organizar a estrutura das classes e os relacionamentos entre classes e objetos.

Padrões comportamentais: Os padrões dessa categoria atuam diretamente na delegação de responsabilidades, definindo como os objetos devem se comportar e se comunicar.

Padrões GoF

Abaixo a relação dos padrões GoF.

Nome do padrãoCategoria
Abstract FactoryCriacional
BuilderCriacional
Factory MethodCriacional
PrototypeCriacional
SingletonCriacional
AdapterEstrutural
BridgeEstrutural
CompositeEstrutural
DecoratorEstrutural
FacadeEstrutural
FlyweightEstrutural
ProxyEstrutural
Chain of ResponsibilityComportamental
CommandComportamental
InterpreterComportamental
IteratorComportamental
MediatorComportamental
MementoComportamental
ObserverComportamental
StateComportamental
StrategyComportamental
Template MethodComportamental
VisitorComportamental

Concluindo

Os padrões GoF foram descritos há décadas e, software é quase como um organismo vivo, ele passa por transformações para se adaptar às novas realidades do “ambiente”.

Nem todos os padrões GoF são bem aceitos nas comunidades de desenvolvimento, por exemplo, você já deve ter ouvido de algum colega desenvolvedor que você deveria passar longe de usar um Singleton por ser um “anti pattern”. Não tenho como propósito aqui fazer julgamento de valor mas, nem tudo é “preto no branco”. Conheço grandes softwares open sources (Frameworks de linguagens bem estabelecidas) que usam Singleton em alguma parte de seus códigos e nem por isso deixaram de ser bons.

Abusar no uso de padrões de projeto pode nos levar a caminhos tortuosos. Na realidade, supervalorizar qualquer coisa nessa nossa complexa vida pode ser perigoso, pois tendemos a criar determinamos “vícios” nas resoluções dos nossos problemas. O bom senso é sempre fundamental. E no final, como sempre, nunca teremos todas as respostas. Nossos softwares nunca serão “perfeitos e impecáveis” e nunca estaremos em plenitude satisfeitos com o código que escrevemos. O desenvolvimento intelectual tem que ser constante e a paranóia precisa ser moderada. Então, vai uma cerveja (ou coca) aí? 😛

Em futuros artigos veremos a aplicação de alguns desses padrões.

Ruby on Rails Intermediário
Curso de Ruby on Rails Intermediário
CONHEÇA O CURSO