Olá!

$whoami

  • Arquiteto de software na Talentify.io
  • Ex sysadmin.
  • Usuário GNU/Linux há 11 anos e amante do open source desde o tempo em que isso era ruim.
  • Atualmente dedicado a criar softwares e arquiteturas que persistam ao tempo e estudando aprendizado de máquina.
  • @gnumoksha

atualmente trabalho como arquiteto de software buscando criar softwares que persistam ao tempo. Utilizo GNU/Linux e desenvolvo software há cerca de 11 anos.

Tópicos

  • Injeção de dependência
  • Containers de injeção de dependência
  • Bibliotecas de injeção de dependência

Dependency Injection (DI)

Dependency Injection é um termo criado por Martin Fowler e defensores do princípio da inversão de controle (IoC) para caracterizar como classes obtém suas depedências.

Entao antes de surgir o termo Dependency Injection as pessoas se referiam a esta prática como inversão de controle, o que é um termo muito genérico. Qual controle está sendo invertido? Entao Martin Fowler criou este termo para descrever a inversão na maneira como classes obtem suas dependencias. E este pattern por si só é bem simples. Vamos ver um exemplo do que este pattern visa resolver.

class RealBillingService implements BillingService
{
    public function chargeOrder(Order $order, PaymentMethod $paymentMethod) : Receipt
    {
        $logger = new Logger();
        $processor = new RealPaymentProcessor();
        try {
            $result = $processor->charge($order, $paymentMethod);

            return $result->wasSuccessful()
                ? Receipt::forSuccessfulCharge($order->getAmount())
                : Receipt::forDeclinedCharge($result->getDeclineMessage());
        } catch (ConnectionError $exception) {
            $logger->error($exception->getMessage(), ['exception' => $exception]);

            return Receipt::forSystemFailure($exception->getMessage());
        }
    }
}

Descrever funcionamento básico desta classe.

Problemas

  • Alto acoplamento (viola os princípios de responsabilidade única e open/closed)
  • Esconde as dependências
  • Difícil de testar
  • Difícil de reutilizar

Problemas

Setter injection

class RealBillingService implements BillingService
{
    private $logger;
    private $paymentProcessor;

    public function setLogger(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function setPaymentProcessor(PaymentProcessor $paymentProcessor)
    {
        $this->paymentProcessor = $paymentProcessor;
    }
    public function chargeOrder(Order $order, PaymentMethod $paymentMethod) : Receipt
    {
        try {
            $result = $this->paymentProcessor->charge($order, $paymentMethod);

            return $result->wasSuccessful()
                ? Receipt::forSuccessfulCharge($order->getAmount())
                : Receipt::forDeclinedCharge($result->getDeclineMessage());
        } catch (ConnectionError $exception) {
            $this->logger->error($exception->getMessage(), ['exception' => $exception]);

            return Receipt::forSystemFailure($exception->getMessage());
        }
    }
}

Ainda há algo de errado: a classe se torna mutável, as dependencias da minha classe nao estao explicitas, é complicado informar para o cliente o que é necessário para o funcionamento da classe. E vai de contrario ao recomendado pelos exercícios de Object Calisthenics e DDD que recomendam adicionar comportamento a classes ao inves de apenas definir valores como um simples saco de dados.

Problemas

  • Difícil de testar
  • Alto acoplamento (viola os princípios de responsabilidade única e open/closed)
  • Esconde as dependências
  • Difícil de reutilizar
  • Mutabilidade

Problemas

Property injection

class RealBillingService implements BillingService
{
    public $logger;
    public $paymentProcessor;

    public function chargeOrder(Order $order, PaymentMethod $paymentMethod) : Receipt
    {
        try {
            $result = $this->paymentProcessor->charge($order, $paymentMethod);

            return $result->wasSuccessful()
                ? Receipt::forSuccessfulCharge($order->getAmount())
                : Receipt::forDeclinedCharge($result->getDeclineMessage());
        } catch (ConnectionError $exception) {
            $this->logger->error($exception->getMessage(), ['exception' => $exception]);

            return Receipt::forSystemFailure($exception->getMessage());
        }
    }
}

$billingService = new RealBillingService();
$billingService->logger = new Logger();
$billingService->paymentProcessor = new PaymentProcessor();

Problemas

  • Difícil de testar
  • Alto acoplamento (viola os princípios de responsabilidade única e open/closed)
  • Esconde as dependências
  • Difícil de reutilizar
  • Mutabilidade

Problemas

Nota sobre Monkey Patching

>>> from unittest.mock import patch
>>> @patch('module.ClassName2')
... @patch('module.ClassName1')
... def test(MockClass1, MockClass2):
...     module.ClassName1()
...     module.ClassName2()
...     assert MockClass1 is module.ClassName1
...     assert MockClass2 is module.ClassName2
...     assert MockClass1.called
...     assert MockClass2.called
...
>>> test()

e Dependency injection is not a virtue in Ruby

Problemas

  • Alto acoplamento (viola os princípios de responsabilidade única e open/closed)
  • Esconde as dependências
  • Difícil de testar
  • Difícil de reutilizar

Solução

Então, como resolver estes problemas? A resposta é simples e creio que muitos de vocês já a utilizam.

class RealBillingService implements BillingService
{
    private $logger;
    private $paymentProcessor;

    public function __construct(
        LoggerInterface $logger,
        PaymentProcessor $paymentProcessor
    ) {
        $this->logger = $logger;
        $this->paymentProcessor = $paymentProcessor;
    }
    public function chargeOrder(Order $order, PaymentMethod $paymentMethod) : Receipt
    {
        try {
            $result = $this->paymentProcessor->charge($order, $paymentMethod);

            return $result->wasSuccessful()
                ? Receipt::forSuccessfulCharge($order->getAmount())
                : Receipt::forDeclinedCharge($result->getDeclineMessage());
        } catch (ConnectionError $exception) {
            $this->logger->error($exception->getMessage(), ['exception' => $exception]);

            return Receipt::forSystemFailure($exception->getMessage());
        }
    }
}

Bastaria então eu passar as dependências da minha classe para o seu construtor. Assim elas estarão explíticas e deixo a minha classe livre do conhecimento adicional sobre as dependencias. Nao preciso saber para onde o log vai ou que o processamento do pagamento faz. A única coisa que eu preciso saber é da API pública destas dependencias.

Muito simples, não é? Bom, é simples até o momento em que saímos da teoria e vamos para o dia-a-dia. E no dia-a-dia nós nos deparamos com a necessidade de criar longos grafos de objetos durante o ciclo de vida das nossas aplicações.

O que são containers de injeção de dependência?

E para essa necessidade surgiram os containers de injeção de dependencia. Bom, durante a preparação desta talk eu conversei com diferentes programadores de diferentes linguagens e tive a impressão de que o termo causa uma certa confusão.

entao eu gostaria de primeiro explicar que estes containers não tem nada a ver com containers de aplicaçao como o docker, LXC, etc.

Dependency Injection Container

Um container de injeção de dependência é um objeto que sabe como instanciar e configurar objetos. Para saber como fazer este trabalho, ele precisa saber sobre os argumentos de construção e o relacionamento entre os objetos. Fabien Potencier

E para essa necessidade surgiram os containers de injeçao de dependencia que nada mais fazem, ou pelo menos não deveriam fazer, alem de criar e injetar dependencias para nós.

Existem aos montes

existem aos montes como bibliotecas separadas ou incorporados em frameworks

Java

  • http://weld.cdi-spec.org/
  • https://github.com/google/guice
  • https://github.com/google/dagger
  • https://javaee.github.io/hk2/
  • https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans
  • https://www.playframework.com/
  • https://ruediste.github.io/salta/

.NET

  • http://www.castleproject.org/
  • https://autofac.org/
  • https://github.com/unitycontainer/unity
  • http://www.ninject.org/
  • https://bitbucket.org/dadhi/dryioc
  • https://github.com/philiplaureano/LinFu

PHP

  • https://symfony.com/doc/current/components/dependency_injection.html
  • https://laravel.com/docs/master/container
  • https://zendframework.github.io/zend-servicemanager/
  • http://container.thephpleague.com/
  • https://github.com/bitExpert/disco
  • http://php-di.org/
  • https://pimple.symfony.com/
  • https://github.com/rdlowrey/auryn
  • https://github.com/woohoolabs/zen
  • https://github.com/jshannon63/cobalt

Problemas com a utilização de Dependency Injection Containers

Porém container de injeção de dependência é uma daquelas coisas que já vem no framework e geralmente nós não nos preocupamos em entender melhor os princípios por trás do código. O que acaba levando ao uso incorreto e, para alguns destes containers, até a uma implementação incorreta.

Problemas com containers

Service locator

Service locator is an anti-pattern

estes trechos de código eu peguei da documentação de três frameworks diferentes. um saco de dados, um register onde vamos e pegamos o que queremos. Viola encapsulamento, viola solid (The interface-segregation principle (ISP) states that no client should be forced to depend on methods it does not use.)

Problemas

  • Difícil de testar
  • Difícil de reutilizar
  • Alto acoplamento (viola os princípios de responsabilidade única e open/closed)
  • Esconde as dependências
  • Interface segregation

Problemas com Bibliotecas de injeção de dependência

Bibliotecas de injeção de dependência

Formatos específicos (YML)


parameters:
    # ...
    mailer.transport: sendmail

services:
    mailer:
        class:     Mailer
        arguments: ['%mailer.transport%']
    newsletter_manager:
        class:     NewsletterManager
        calls: [setMailer, ['@mailer']]
    serviceA:
        class: ServiceA
        arguments: ['Foo', 'Bar']
    serviceB:
        class: ServiceB
        arguments: ['Foo', 'Bar']
    serviceC:
        class: ServiceC
        arguments: ['Foo', 'Bar']
    serviceD:
       class: ServiceD
       arguments: ['Foo', 'Bar']
    serviceE:
       class: ServiceE
       arguments: ['Foo', 'Bar']
    serviceF:
       class: ServiceF
       arguments: ['Foo', 'Bar']

Bibliotecas de injeção de dependência

Formatos específicos (XML)

<!--?xml version="1.0" encoding="UTF-8"?-->
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="accountDao" class="org.springframework.samples.jpetstore.dao.jpa.JpaAccountDao">
        <!-- additional collaborators and configuration for this bean go here -->
    </bean>

    <bean id="itemDao" class="org.springframework.samples.jpetstore.dao.jpa.JpaItemDao">
        <!-- additional collaborators and configuration for this bean go here -->
    </bean>

    <!-- more bean definitions for data access objects go here -->

</beans>

Bibliotecas de injeção de dependência

Formatos específicos

  • Nova sintaxe e curva de aprendizado
  • Sem tipagem
  • Sem validação
  • Dificuldade aumenta a medida que cresce

nova sintaxe que exige que alguem novo na base de código saiba que tal configuração existe e como gerencia-la. Nao tem tipagem ou qualquer mecanismo de validação built-in. A medida que cresce fica mais difícil lidar com essa configuração: saber se um serviço está configurado ou não, entender a orvore de dependencias e possiveis breaking changes

Bibliotecas de injeção de dependência

Dependência de um framework

tecnologias vem e vão; e quando a gente passa a se preocupar em criar um código, uma aplicação, que persista ao teste do tempo, passa a ser vital desacoplar o máximo possível das camadas de infraestrutura, como o framework.

então até agora nós vimos o problema, vimos a solução simples com o uso de injeção de dependencia. e por fim vimos que o uso de bibliotecas de injeção de dependencia podem ser complicados e prejudiciais. Mas você pode estar se perguntando:

Eu preciso disso?

voltando ao exemplos que mostrei, retirados da documentação de alguns projetos. Será que é realmente necessário a utilização de uma biblioteca de injeção de dependencia?

Indo mais a fundo

bom, o que eu preciso fazer para utilizar injeção de dependencia sem precisar de um container? Eu preciso construir a minha aplicação desde o seu inicio.

Raiz de composição

(Composition Root)

A raiz de composição é, preferencialmente, o único lugar em uma aplicação onde módulos são compostos juntos.

Mark Seemann

ler a definição. Então, onde se encontra a raiz de composição da sua aplicação?

Vamos pegar como exemplo uma aplicação web. Qual

Bom, podem haver diferentes raizes de composição para diferentes arquiteturas, mas em uma aplicação web, por exemplo, o comum é que sejam nas...

Rotas

$router->post('/order', ['uses' => 'Domain\Order\Http\OrderController@create']);

Como?

agora que sabemos onde compor nossa aplicação, como podemos estruturar o código que irá criar toda a arvore de dependencias?

Injeção de Dependência Pura

(Pure DI)

DI é um conjunto de princípios e padrões; DI Containers são bibliotecas auxiliares opcionais.

Mark Seemann

Exemplo

$router->post('/order', function () {
    return new OrderController(
        new RealOrderService()
    );
});

Vantagens da injeção pura

  • Fortemente tipado
  • Pura composição de objetos
  • O feedback mais rápido que você pode ter
  • Sem curva de aprendizado
  • Sem mágica

Desvantagens da injeção pura

  • Manutenção aumenta conforme a assinatura dos construtores muda
  • Ciclo de vida simples

Raiz de composição + container simples

abstract class Dependencies {
    private $environment;

    function __construct(Environment $environment) {
        $this->environment = $environment;
    }

    public function OrderService(): OrderService {
        return new OrderService($this->OrderRepository());
    }

    protected function OrderRepository(): OrderRepository {
        return $this->MySQLOrderRepository();
    }

    protected function MySQLOrderRepository(): MySQLOrderRepository {
        return new MySQLOrderRepository($this->DBConnection());
    }

    protected function DBConnection(): DBConnection {
        $environment = $this->environment;
        return new DBConnection($environment->dbHost, $environment->dbUsername);
    }
}

Source

final class ProductionDependencies extends Dependencies
{
    // dependencias de produção
}

final class StagingDependencies extends Dependencies
{
    // dependencias de stage
}

final class TestingDependencies extends Dependencies
{
    // dependencias de teste
    protected function OrderRepository (): OrderRepository {
        return $this->InMemoryOrderRepository();
    }
}
final class Router
{
    private $dependencies;

    public function __construct(Dependencies $dependencies) {
        $this->dependencies = $dependencies;
    }

    public function route(Request $request)
    {
        if ($request->endpoint === '/order') {
            return $this->dependencies->OrderService()->createFromRequest($request);
        }
    }
}

Vantagens

  • Mínimo código nescessário
  • Tipagem
  • Sem curva de aprendizado, apenas a sua linguagem de programação
  • Sem mágica

variacoes desse método podem ser utilizadas caso seu framework nao possibilite esta extensao, ou voce queira aprimorar o container que vem no seu framework. Inclusive, se voce utilizar DDD, é interessante criar um container por Bounded Context da sua aplicação para evitar a exposição de dependencias entre os contextos.

Conclusão

Referências adicionais

  • http://blog.ploeh.dk/2014/06/10/pure-di/
  • http://blog.ploeh.dk/2012/11/06/WhentouseaDIContainer/
  • https://www.kenneth-truyers.net/2014/11/19/how-to-use-pure-di/

Perguntas?

  • @gnumoksha
  • contato@tobias.ws