O que é Injeção de Dependência?

Este capítulo apresenta as práticas básicas de programação que você deve seguir ao escrever qualquer solicitação. Estas são as bases necessárias para escrever um código limpo, compreensível e de fácil manutenção.

Se você aprender e seguir estas regras, Nette estará lá para você a cada passo do caminho. Ela cuidará das tarefas de rotina para você e o deixará o mais confortável possível para que você possa se concentrar na própria lógica.

Os princípios que vamos mostrar aqui são bastante simples. Você não tem nada com que se preocupar.

Lembra-se de seu primeiro programa?

Não temos idéia em que linguagem você a escreveu, mas se fosse PHP, provavelmente seria algo parecido com isto:

function addition(float $a, float $b): float
{
	return $a + $b;
}

echo addition(23, 1); // impressões 24

Algumas linhas triviais de código, mas tantos conceitos-chave escondidos nelas. Que existem variáveis. Que esse código é dividido em unidades menores, que são funções, por exemplo. Que nós as passamos argumentos de entrada e elas retornam resultados. Tudo o que está faltando são condições e loops.

O fato de passarmos a entrada para uma função e ela retornar um resultado é um conceito perfeitamente compreensível que é usado em outros campos, como a matemática.

Uma função tem uma assinatura, que consiste em seu nome, uma lista de parâmetros e seus tipos e, finalmente, o tipo de valor de retorno. Como usuários, estamos interessados na assinatura; normalmente não precisamos saber nada sobre a implementação interna.

Agora imagine que a assinatura de uma função se parece com isto:

function addition(float $x): float

Uma adição com um parâmetro? Isso é estranho… Que tal isto?

function addition(): float

Isso é realmente estranho, não é? Como você acha que a função é usada?

echo addition(); // o que imprime?

Olhando para tal código, ficamos confusos. Não só um iniciante não o entenderia, nem mesmo um programador hábil entenderia tal código.

Você se pergunta como seria realmente uma função desse tipo por dentro? Onde obteria as víboras? Provavelmente os conseguiria somente por si só, assim:

function addition(): float
{
	$a = Input::get('a');
	$b = Input::get('b');
	return $a + $b;
}

Acontece que existem ligações ocultas a outras funções (ou métodos estáticos) no corpo da função, e para descobrir de onde vêm os adendos de fato, temos que cavar mais.

Não desta maneira!

O projeto que acabamos de mostrar é a essência de muitas características negativas:

  • a assinatura da função fingia que não precisava de adendos, o que nos confundia
  • não temos idéia de como fazer o cálculo da função com dois outros números
  • tivemos que olhar para o código para ver onde ele leva os adendos
  • descobrimos encadernações ocultas
  • para compreender plenamente, precisamos explorar também estas ligações

E é mesmo o trabalho da função de adição a aquisição de insumos? Claro que não é. Sua responsabilidade é apenas a de acrescentar.

Não queremos encontrar tal código, e certamente não queremos escrevê-lo. O remédio é simples: voltar ao básico e usar apenas parâmetros:

function addition(float $a, float $b): float
{
	return $a + $b;
}

Regra nº 1: Deixe que seja passado para você

A regra mais importante é: **todos os dados que funcionam ou classes precisam ser passados a eles***.

Em vez de inventar mecanismos ocultos para ajudá-los de alguma forma a chegar até eles mesmos, basta passar os parâmetros para dentro. Você economizará o tempo necessário para inventar mecanismos ocultos, o que definitivamente não irá melhorar seu código.

Se você segue esta regra sempre e em qualquer lugar, você está a caminho de codificar sem amarrações ocultas. Para um código que seja compreensível não só para o autor, mas também para qualquer pessoa que o leia depois. Onde tudo é compreensível a partir das assinaturas de funções e classes e não há necessidade de buscar segredos ocultos na implementação.

Esta técnica é denominada habilmente injeção de dependência***. E os dados são chamados de **dependências. Mas é um parâmetro simples de passagem, nada mais.

Por favor, não confunda injeção de dependência, que é um padrão de projeto, com “recipiente de injeção de dependência”, que é uma ferramenta, algo completamente diferente. Discutiremos os recipientes mais tarde.

Das funções às aulas

E como as aulas se relacionam com isso? Uma classe é uma entidade mais complexa do que uma simples função, mas a regra nº 1 também se aplica aqui. Há apenas mais maneiras de passar argumentos. Por exemplo, bem parecido com o caso de uma função:

class Math
{
	public function addition(float $a, float $b): float
	{
		return $a + $b;
	}
}

$math = new Math;
echo $math->addition(23, 1); // 24

Ou através de outros métodos, ou diretamente pelo construtor:

class Addition
{
	public function __construct(
		private float $a,
		private float $b,
	) {
	}

	public function calculate(): float
	{
		return $this->a + $this->b;
	}

}

$addition = new Addition(23, 1);
echo $addition->calculate(); // 24

Ambos os exemplos estão completamente de acordo com a injeção de dependência.

Exemplos da vida real

No mundo real, você não vai escrever aulas para adicionar números. Passemos aos exemplos do mundo real.

Vamos ter uma aula Article representando um artigo de blog:

class Article
{
	public int $id;
	public string $title;
	public string $content;

	public function save(): void
	{
		// salvar o artigo no banco de dados
	}
}

e a utilização será a seguinte:

$article = new Article;
$article->title = '10 Things You Need to Know About Losing Weight';
$article->content = 'Every year millions of people in ...';
$article->save();

O método save() salva o artigo em uma tabela de banco de dados. Implementá-lo usando o Nette Database seria canja, se não fosse por um único engate: onde Article deveria obter a conexão com o banco de dados, ou seja, o objeto de classe Nette\Database\Connection?

Parece que temos muitas opções. Ela pode ser tirada de algum lugar em uma variável estática. Ou herdá-la de uma classe que fornecerá a conexão com o banco de dados. Ou tirar vantagem de um único botão. Ou as chamadas fachadas que são usadas em Laravel:

use Illuminate\Support\Facades\DB;

class Article
{
	public int $id;
	public string $title;
	public string $content;

	public function save(): void
	{
		DB::insert(
			'INSERT INTO articles (title, content) VALUES (?, ?)',
			[$this->title, $this->content],
		);
	}
}

Ótimo, nós resolvemos o problema.

Ou temos?

Vamos relembrar a regra nº 1: Deixe que seja passado para você: todas as dependências que a classe precisa devem ser passadas a ela. Porque se não o fizermos, e quebrarmos a regra, começamos pelo caminho do código sujo cheio de encadernações ocultas, incompreensibilidade, e o resultado será uma aplicação que é uma dor para manter e desenvolver.

O usuário da classe Article não tem idéia de onde o método save() armazena o artigo. Em uma tabela de banco de dados? Em qual delas, produção ou desenvolvimento? E como isso pode ser mudado?

O usuário tem que olhar como o método save() é implementado para encontrar o uso do método DB::insert(). Portanto, ele tem que pesquisar mais para descobrir como este método provê uma conexão de banco de dados. E as amarrações ocultas podem formar uma cadeia bastante longa.

Ligações ocultas, fachadas de Laravel ou variáveis estáticas nunca estão presentes em código limpo e bem desenhado. Em código limpo e bem desenhado, os argumentos são passados:

class Article
{
	public function save(Nette\Database\Connection $db): void
	{
		$db->query('INSERT INTO articles', [
			'title' => $this->title,
			'content' => $this->content,
		]);
	}
}

Ainda mais prático, como veremos a seguir, é usar um construtor:

class Article
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function save(): void
	{
		$this->db->query('INSERT INTO articles', [
			'title' => $this->title,
			'content' => $this->content,
		]);
	}
}

Se você é um programador experiente, você pode estar pensando que Article não deveria ter um método save(), deveria ser um componente de dados puro, e um repositório separado deveria cuidar do armazenamento. Isto faz sentido. Mas isso nos levaria muito além do tópico, que é a injeção de dependência, e tentando dar exemplos simples.

Se você vai escrever uma classe que requer um banco de dados para operar, por exemplo, não descubra de onde obtê-lo, mas faça-o passar para você. Talvez como um parâmetro para um construtor ou outro método. Declare as dependências. Exponha-as na API de sua classe. Você obterá um código compreensível e previsível.

Que tal esta classe que registra mensagens de erro:

class Logger
{
	public function log(string $message)
	{
		$file = LOG_DIR . '/log.txt';
		file_put_contents($file, $message . "\n", FILE_APPEND);
	}
}

O que você acha, nós seguimos a regra nº 1: Deixe que seja passado para você?

Nós não o fizemos.

A informação chave, o diretório de arquivos de log, é obtida pela classe da constante.

Veja o exemplo de uso:

$logger = new Logger;
$logger->log('The temperature is 23 °C');
$logger->log('The temperature is 10 °C');

Sem conhecer a implementação, você poderia responder à pergunta onde as mensagens são escritas? Sugeriria a você que a existência da constante LOG_DIR é necessária para que ela funcione? E você seria capaz de criar uma segunda instância que escreva para um local diferente? Certamente que não.

Vamos consertar a classe:

class Logger
{
	public function __construct(
		private string $file,
	) {
	}

	public function log(string $message): void
	{
		file_put_contents($this->file, $message . "\n", FILE_APPEND);
	}
}

A classe é agora muito mais clara, mais configurável e, portanto, mais útil.

$logger = new Logger('/path/to/log.txt');
$logger->log('The temperature is 15 °C');

Mas eu não me importo!

“Quando eu crio um objeto de Artigo e chamo salvar(), não quero lidar com o banco de dados, apenas quero que ele seja salvo para aquele que eu defini na configuração. ”

“Quando uso o Logger, só quero que a mensagem seja escrita, e não quero lidar com onde. Deixe que as configurações globais sejam usadas. ”

Estes são os comentários corretos.

Como exemplo, vamos dar uma aula que envia boletins informativos e registros de como isso foi feito:

class NewsletterDistributor
{
	public function distribute(): void
	{
		$logger = new Logger(/* ... */);
		try {
			$this->sendEmails();
			$logger->log('Emails have been sent out');

		} catch (Exception $e) {
			$logger->log('An error occurred during the sending');
			throw $e;
		}
	}
}

O Logger melhorado, que não usa mais a constante LOG_DIR, requer um caminho de arquivo no construtor. Como resolver isto? A classe NewsletterDistributor não se importa onde as mensagens são escritas, ela só quer escrevê-las.

A solução é novamente a regra nº 1: Deixe que seja passado para você: passe todos os dados que a classe precisa para ela.

Então passamos o caminho para o toro para o construtor, que depois usamos para criar o objeto Logger?

class NewsletterDistributor
{
	public function __construct(
		private string $file, // ⛔ NÃO DESTA FORMA!
	) {
	}

	public function distribute(): void
	{
		$logger = new Logger($this->file);

Não dessa maneira! Porque o caminho ** não*** pertence aos dados que a classe NewsletterDistributor precisa; precisa Logger. A classe precisa do próprio madeireiro. E é isso que vamos passar adiante:

class NewsletterDistributor
{
	public function __construct(
		private Logger $logger, // ✅
	) {
	}

	public function distribute(): void
	{
		try {
			$this->sendEmails();
			$this->logger->log('Emails have been sent out');

		} catch (Exception $e) {
			$this->logger->log('An error occurred during the sending');
			throw $e;
		}
	}
}

Agora fica claro a partir das assinaturas da classe NewsletterDistributor que a extração de madeira é parte de sua funcionalidade. E a tarefa de substituir o madeireiro por outro, talvez para fins de teste, é bastante trivial. Além disso, se o construtor da classe Logger for alterado, isso não terá nenhum efeito sobre nossa classe.

Regra nº 2: Tome o que é seu

Não se deixe enganar e não deixe que os parâmetros de suas dependências lhe sejam passados. Transmita diretamente as dependências.

Isto tornará o código usando outros objetos completamente independente de mudanças em seus construtores. Sua API será mais verdadeira. E o mais importante, será trivial trocar essas dependências por outras.

Um novo membro da família

A equipe de desenvolvimento decidiu criar um segundo logger que escreva para o banco de dados. Por isso, criamos uma classe DatabaseLogger. Então temos duas classes, Logger e DatabaseLogger, uma escreve para um arquivo, a outra escreve para um banco de dados … você não acha que há algo de estranho nesse nome? Não seria melhor renomear Logger para FileLogger? Claro que sim.

Mas vamos fazer isso com inteligência. Vamos criar uma interface com o nome original:

interface Logger
{
	function log(string $message): void;
}

…que ambos os madeireiros implementarão:

class FileLogger implements Logger
// ...

class DatabaseLogger implements Logger
// ...

E desta forma, nada precisará ser alterado no resto do código onde o madeireiro é utilizado. Por exemplo, o construtor da classe NewsletterDistributor ainda ficará feliz em exigir Logger como parâmetro. E caberá a nós qual instância passaremos para ele.

** É por isso que nunca damos nomes de interface o sufixo Interface ou o prefixo I.** Caso contrário, seria impossível desenvolver código tão bem.

Houston, temos um problema

Enquanto em toda a aplicação podemos ficar satisfeitos com uma única instância de um registrador, seja de arquivo ou banco de dados, e simplesmente passá-la onde quer que algo esteja registrado, é bem diferente no caso da classe Article. Na verdade, criamos instâncias dela conforme a necessidade, possivelmente várias vezes. Como lidar com a encadernação do banco de dados em seu construtor?

Como exemplo, podemos usar um controlador que deve salvar um artigo no banco de dados após o envio de um formulário:

class EditController extends Controller
{
	public function formSubmitted($data)
	{
		$article = new Article(/* ... */);
		$article->title = $data->title;
		$article->content = $data->content;
		$article->save();
	}
}

Uma possível solução é oferecida diretamente: fazer passar o objeto do banco de dados pelo construtor para EditController e usar $article = new Article($this->db).

Como no caso anterior com Logger e o caminho do arquivo, esta não é a abordagem correta. O banco de dados não é uma dependência do EditController, mas do Article. Assim, a passagem do banco de dados vai contra a regra nº 2: pegue o que é seu. Quando o construtor da classe Article é modificado (um novo parâmetro é adicionado), o código em todos os lugares onde as instâncias são criadas também precisará ser modificado. Ufff.

Houston, o que você está sugerindo?

Regra nº 3: Deixe a Fábrica tratar disso

Ao remover as amarrações ocultas e passar todas as dependências como argumentos, obtemos classes mais configuráveis e flexíveis. E assim, precisamos de algo mais para criar e configurar essas classes mais flexíveis. Vamos chamá-lo de fábricas.

A regra básica é: se uma classe tem dependências, deixar a criação de suas instâncias para a fábrica.

As fábricas são um substituto mais inteligente para o operador new no mundo da injeção de dependência.

Fábrica

Uma fábrica é um método ou classe que produz e configura objetos. Chamamos Article produzindo a classe ArticleFactory e poderia parecer assim:

class ArticleFactory
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function create(): Article
	{
		return new Article($this->db);
	}
}

Sua utilização no controlador seria a seguinte:

class EditController extends Controller
{
	public function __construct(
		private ArticleFactory $articleFactory,
	) {
	}

	public function formSubmitted($data)
	{
		// deixar a fábrica criar um objeto
		$article = $this->articleFactory->create();
		$article->title = $data->title;
		$article->content = $data->content;
		$article->save();
	}
}

Neste ponto, quando a assinatura do construtor da classe Article muda, a única parte do código que precisa responder é a própria fábrica ArticleFactory. Qualquer outro código que funcione com objetos Article, tais como EditController, não será afetado.

Você pode estar batendo a testa agora mesmo se perguntando se nós nos ajudamos de alguma forma. A quantidade de código cresceu e tudo isso está começando a parecer suspeitamente complicado.

Não se preocupe, em breve chegaremos ao recipiente Nette DI. E ele tem uma série de ases na manga que tornarão as aplicações de construção utilizando a injeção de dependência extremamente simples. Por exemplo, em vez da classe ArticleFactory, será suficiente escrever uma interface simples:

interface ArticleFactory
{
	function create(): Article;
}

Mas estamos nos adiantando, segurem-se :-)

Sumário

No início deste capítulo, prometemos mostrar-lhe uma maneira de projetar um código limpo. Basta dar as aulas

Pode não parecer assim à primeira vista, mas estas três regras têm implicações de longo alcance. Elas levam a uma visão radicalmente diferente do projeto do código. Será que vale a pena? Programadores que expulsaram velhos hábitos e começaram a usar de forma consistente a injeção de dependência consideram isto um momento crucial em suas vidas profissionais. Isso abriu um mundo de aplicações claras e sustentáveis.

Mas e se o código não usar a injeção de dependência de forma consistente? E se ele for construído sobre métodos estáticos ou singletons? Isso traz algum problema? Traz, e é muito significativo.

versão: 3.x