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

Este capítulo apresentará os procedimentos básicos de programação que você deve seguir ao escrever todas as aplicações. São os fundamentos necessários para escrever código limpo, compreensível e sustentável.

Se você dominar e seguir estas regras, o Nette o apoiará em cada passo. Ele cuidará das tarefas rotineiras para você e fornecerá o máximo de conforto, para que você possa se concentrar na lógica em si.

Os princípios que mostraremos aqui são bastante simples. Você não precisa se preocupar com nada.

Lembra do seu primeiro programa?

Não sabemos em que linguagem você o escreveu, mas se fosse PHP, provavelmente seria algo assim:

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

echo soucet(23, 1); // imprime 24

Algumas linhas triviais de código, mas nelas se escondem tantos conceitos-chave. Que existem variáveis. Que o código é dividido em unidades menores, como funções. Que passamos argumentos de entrada para elas e elas retornam resultados. Faltam apenas condições e loops.

O fato de passarmos dados de entrada para uma função e ela retornar um resultado é um conceito perfeitamente compreensível, usado também em outras áreas, como na matemática.

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

Agora imagine que a assinatura da função fosse assim:

function soucet(float $x): float

Soma com um parâmetro? Isso é estranho… E que tal assim?

function soucet(): float

Isso já é muito estranho, não é? Como a função seria usada?

echo soucet(); // o que será que imprime?

Ao olhar para tal código, ficaríamos confusos. Não apenas um iniciante não entenderia, mas nem mesmo um programador experiente entenderia tal código.

Você está pensando como essa função seria por dentro? Onde ela obteria os operandos? Provavelmente, ela os obteria de alguma forma por conta própria, talvez assim:

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

No corpo da função, descobrimos ligações ocultas a outras funções globais ou métodos estáticos. Para descobrir de onde os operandos realmente vêm, precisamos investigar mais.

Não por aqui!

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

  • a assinatura da função fingia não precisar de operandos, o que nos confundiu
  • não sabemos como fazer a função somar outros dois números
  • tivemos que olhar o código para descobrir onde ela obtém os operandos
  • descobrimos ligações ocultas
  • para entender completamente, é necessário examinar também essas ligações

E é tarefa da função de soma obter as entradas? Claro que não. Sua responsabilidade é apenas a soma em si.

Não queremos encontrar tal código, e definitivamente não queremos escrevê-lo. A correção é simples: voltar ao básico e simplesmente usar parâmetros:

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

Regra nº 1: peça para ser passado

A regra mais importante é: todos os dados que uma função ou classe precisa devem ser passados para ela.

Em vez de inventar maneiras ocultas pelas quais eles poderiam obtê-los sozinhos, simplesmente passe os parâmetros. Você economizará o tempo necessário para inventar caminhos ocultos, que definitivamente não melhorarão seu código.

Se você seguir esta regra sempre e em toda parte, estará no caminho para um código sem ligações ocultas. Para um código que é compreensível não apenas para o autor, mas também para qualquer pessoa que o leia depois dele. Onde tudo é compreensível a partir das assinaturas das funções e classes e não há necessidade de procurar segredos ocultos na implementação.

Essa técnica é tecnicamente chamada de injeção de dependência. E esses dados são chamados de dependências. Na verdade, é apenas a passagem comum de parâmetros, nada mais.

Por favor, não confunda injeção de dependência, que é um padrão de projeto, com “contêiner de injeção de dependência”, que é uma ferramenta, ou seja, algo diametralmente diferente. Falaremos sobre contêineres mais tarde.

De funções para classes

E como as classes se relacionam com isso? Uma classe é uma unidade mais complexa do que uma função simples, mas a regra nº 1 se aplica integralmente aqui também. Apenas existem mais opções para passar argumentos. Por exemplo, de forma bastante semelhante ao caso de uma função:

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

$math = new Matematika;
echo $math->soucet(23, 1); // 24

Ou usando outros métodos, ou diretamente o construtor:

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

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

}

$soucet = new Soucet(23, 1);
echo $soucet->spocti(); // 24

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

Exemplos reais

No mundo real, você não escreverá classes para somar números. Vamos passar para exemplos práticos.

Temos uma classe Article representando um artigo de blog:

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

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

e o uso será o seguinte:

$article = new Article;
$article->title = '10 coisas que você precisa saber sobre perder peso';
$article->content = 'Todo ano milhões de pessoas em ...';
$article->save();

O método save() salva o artigo em uma tabela do banco de dados. Implementá-lo usando Nette Database seria moleza, se não fosse por um obstáculo: onde Article obtém a conexão com o banco de dados, ou seja, o objeto da classe Nette\Database\Connection?

Parece que temos muitas opções. Pode obtê-lo de algum lugar em uma variável estática. Ou herdar de uma classe que fornece a conexão com o banco de dados. Ou usar o chamado singleton. Ou as chamadas facades, que são usadas no 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, resolvemos o problema.

Ou não?

Lembre-se da #Regra nº 1: peça para ser passado: todas as dependências que a classe precisa devem ser passadas para ela. Porque se quebrarmos a regra, entramos no caminho do código sujo cheio de ligações ocultas, incompreensibilidade, e o resultado será uma aplicação que será dolorosa de manter e desenvolver.

O usuário da classe Article não tem ideia de onde o método save() salva o artigo. Em uma tabela do banco de dados? Em qual, produção ou teste? E como isso pode ser alterado?

O usuário precisa olhar como o método save() é implementado e encontra o uso do método DB::insert(). Então, ele precisa investigar mais, como esse método obtém a conexão com o banco de dados. E as ligações ocultas podem formar uma cadeia bastante longa.

Em código limpo e bem projetado, nunca existem ligações ocultas, facades do Laravel ou variáveis estáticas. Em código limpo e bem projetado, 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 mais adiante, será pelo 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, pode estar pensando que Article não deveria ter um método save(), deveria representar puramente um componente de dados e o armazenamento deveria ser responsabilidade de um repositório separado. Isso faz sentido. Mas isso nos levaria muito além do escopo do tópico, que é a injeção de dependência, e do esforço para fornecer exemplos simples.

Se você for escrever uma classe que requer, por exemplo, um banco de dados para sua operação, não invente de onde obtê-lo, mas peça para que seja passado. Talvez como um parâmetro do construtor ou de outro método. Admita as dependências. Admita-as na API da sua classe. Você obterá um código compreensível e previsível.

E 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, seguimos a #Regra nº 1: peça para ser passado?

Não seguimos.

A informação chave, ou seja, o diretório com o arquivo de log, a classe obtém por si mesma a partir de uma constante.

Veja o exemplo de uso:

$logger = new Logger;
$logger->log('A temperatura é 23 °C');
$logger->log('A temperatura é 10 °C');

Sem conhecer a implementação, você conseguiria responder à pergunta de onde as mensagens são escritas? Você pensaria que para funcionar é necessária a existência da constante LOG_DIR? E você conseguiria criar uma segunda instância que escreveria em outro lugar? Certamente não.

Vamos corrigir 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 compreensível, configurável e, portanto, mais útil.

$logger = new Logger('/caminho/para/log.txt');
$logger->log('A temperatura é 15 °C');

Mas isso não me interessa!

“Quando crio um objeto Article e chamo save(), não quero lidar com o banco de dados, só quero que ele seja salvo naquele que configurei.”

“Quando uso o Logger, só quero que a mensagem seja escrita, e não quero me preocupar onde. Que use a configuração global.”

Essas são observações válidas.

Como exemplo, mostraremos uma classe que envia newsletters e registra o resultado:

class NewsletterDistributor
{
	public function distribute(): void
	{
		$logger = new Logger(/* ... */);
		try {
			$this->sendEmails();
			$logger->log('E-mails foram enviados');

		} catch (Exception $e) {
			$logger->log('Ocorreu um erro ao enviar');
			throw $e;
		}
	}
}

O Logger aprimorado, que não usa mais a constante LOG_DIR, requer que o caminho do arquivo seja especificado no construtor. Como resolver isso? 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: peça para ser passado: todos os dados que a classe precisa, nós passamos para ela.

Então isso significa que passamos o caminho do log através do construtor, que então usamos ao criar o objeto Logger?

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

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

Assim não! O caminho, de fato, não pertence aos dados que a classe NewsletterDistributor precisa; esses são necessários pelo Logger. Você percebe a diferença? A classe NewsletterDistributor precisa do logger como tal. Então, passamos ele:

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

	public function distribute(): void
	{
		try {
			$this->sendEmails();
			$this->logger->log('E-mails foram enviados');

		} catch (Exception $e) {
			$this->logger->log('Ocorreu um erro ao enviar');
			throw $e;
		}
	}
}

Agora está claro pelas assinaturas da classe NewsletterDistributor que o log faz parte de sua funcionalidade. E a tarefa de trocar o logger por outro, talvez para testes, é completamente trivial. Além disso, se o construtor da classe Logger mudar, isso não terá nenhum efeito em nossa classe.

Regra nº 2: pegue o que é seu

Não se deixe enganar e não peça para passar as dependências de suas dependências. Peça para passar apenas suas dependências.

Graças a isso, o código que utiliza outros objetos será completamente independente das mudanças em seus construtores. Sua API será mais verdadeira. E, principalmente, será trivial trocar essas dependências por outras.

Novo membro da família

Na equipe de desenvolvimento, foi decidido criar um segundo logger, que escreve no banco de dados. Criaremos então a classe DatabaseLogger. Então temos duas classes, Logger e DatabaseLogger, uma escreve em arquivo, a outra no banco de dados… não parece algo estranho nessa nomenclatura? Não seria melhor renomear Logger para FileLogger? Certamente sim.

Mas faremos isso de forma inteligente. Sob o nome original, criaremos uma interface:

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

… que ambos os loggers implementarão:

class FileLogger implements Logger
// ...

class DatabaseLogger implements Logger
// ...

E graças a isso, não será necessário alterar nada no restante do código onde o logger é utilizado. Por exemplo, o construtor da classe NewsletterDistributor continuará satisfeito em exigir Logger como parâmetro. E caberá a nós qual instância passar para ele.

Por isso, nunca damos aos nomes das interfaces o sufixo Interface ou o prefixo I. Caso contrário, não seria possível desenvolver o código de forma tão elegante.

Houston, temos um problema

Enquanto em toda a aplicação podemos nos contentar com uma única instância de logger, seja de arquivo ou de banco de dados, e simplesmente passá-la para todos os lugares onde algo é registrado, a situação é bem diferente no caso da classe Article. Suas instâncias são criadas conforme necessário, até mesmo várias vezes. Como lidar com a dependência do banco de dados em seu construtor?

Como exemplo, pode servir um controller que, após o envio de um formulário, deve salvar o artigo no banco de dados:

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

Uma solução possível se oferece diretamente: passamos o objeto do banco de dados pelo construtor para EditController e usamos $article = new Article($this->db).

Assim como no caso anterior com Logger e o caminho do arquivo, este não é o procedimento correto. O banco de dados não é uma dependência de EditController, mas de Article. Passar o banco de dados, portanto, vai contra a regra nº 2: pegue o que é seu. Quando o construtor da classe Article mudar (um novo parâmetro for adicionado), será necessário modificar também o código em todos os lugares onde instâncias são criadas. Ufa.

Houston, o que você sugere?

Regra nº 3: deixe para a fábrica

Ao eliminar as ligações ocultas e passar todas as dependências como argumentos, obtivemos classes mais configuráveis e flexíveis. E, portanto, precisamos de algo mais, que crie e configure essas classes mais flexíveis para nós. Chamaremos isso de fábricas.

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

As fábricas são substitutos mais inteligentes do operador new no mundo da injeção de dependência.

Por favor, não confunda com o padrão de projeto factory method, que descreve um uso específico de fábricas e não está relacionado a este tópico.

Fábrica

Uma fábrica é um método ou classe que produz e configura objetos. A classe que produz Article chamaremos de ArticleFactory e poderia parecer, por exemplo, assim:

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

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

Seu uso no controller será o seguinte:

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

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

Neste momento, se a assinatura do construtor da classe Article mudar, a única parte do código que precisa reagir é a própria fábrica ArticleFactory. Todo o restante do código que trabalha com objetos Article, como EditController, não será afetado de forma alguma.

Talvez você esteja batendo na testa agora, se realmente nos ajudamos. A quantidade de código aumentou e tudo começa a parecer suspeitosamente complicado.

Não se preocupe, em breve chegaremos ao Contêiner de DI do Nette. E ele tem vários ases na manga que simplificarão imensamente a construção de aplicações usando injeção de dependência. Por exemplo, em vez da classe ArticleFactory, será suficiente escrever apenas uma interface:

interface ArticleFactory
{
	function create(): Article;
}

Mas estamos nos adiantando, aguarde mais um pouco :-)

Resumo

No início deste capítulo, prometemos mostrar um procedimento para projetar código limpo. Basta para as classes

  1. passar as dependências que precisam |
  2. e, inversamente, não passar o que não precisam diretamente |
  3. e que objetos com dependências são melhor criados em fábricas |

Pode não parecer à primeira vista, mas essas três regras têm consequências de longo alcance. Elas levam a uma visão radicalmente diferente do design de código. Vale a pena? Programadores que abandonaram velhos hábitos e começaram a usar consistentemente a injeção de dependência consideram este passo um momento crucial em suas vidas profissionais. Abriu-se para eles o mundo de aplicações claras e sustentáveis.

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

versão: 3.x