O que é Injeção de Dependência?
Este capítulo lhe apresentará as práticas básicas de programação que você deve seguir ao redigir qualquer solicitação. Estes são os fundamentos necessários 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 rotineiras para você e lhe proporcionará o máximo de conforto, 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 que se preocupar com nada.
Lembra-se de seu primeiro programa?
Não sabemos em que linguagem você a escreveu, mas se fosse PHP, poderia ter sido 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 uma função pegar dados de entrada e retornar um resultado é um conceito perfeitamente compreensível, que também é usado em outros campos, como a 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, e normalmente não precisamos saber nada sobre a implementação interna.
Agora imagine que a assinatura da função tivesse este aspecto:
function addition(float $x): float
Uma adição com um parâmetro? Isso é estranho… E quanto a isto?
function addition(): float
Isso é realmente estranho, certo? Como a função é utilizada?
echo addition(); // o que imprime?
Olhando para tal código, ficaríamos confusos. Não só um iniciante não entenderia, mas até mesmo um programador experiente não entenderia tal código.
Você está se perguntando como seria realmente uma função desse tipo por dentro? Onde obteria as somas? Provavelmente, de alguma forma, ela os obteria sozinha, talvez 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 das somas, 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 descobrir de onde veio o summands
- encontramos dependências ocultas
- um entendimento completo requer o exame destas dependências também
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 formas ocultas de acesso aos dados em si, basta passar os parâmetros. Você economizará tempo que seria gasto inventando caminhos ocultos que certamente não irão melhorar seu código.
Se você sempre e em todos os lugares segue esta regra, está a caminho de codificar sem dependências ocultas. A 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 a injeção por dependência, que é um padrão de projeto, com um „recipiente de injeção por dependência“, que é uma ferramenta, algo diametralmente diferente. Trataremos dos recipientes mais tarde.
Das funções às aulas
E como as classes estão relacionadas? Uma classe é uma unidade mais complexa do que uma simples função, mas a regra nº 1 também se aplica inteiramente 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 através do 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 estará escrevendo aulas para adicionar números. Passemos a exemplos práticos.
Vamos ter uma aula Article
representando um post no 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()
salvará o artigo em uma tabela de banco de dados. A implementação usando o Nette Database será canja, se não fosse por uma única questão: onde
Article
obtém a conexão com o banco de dados, ou seja, um objeto de classe
Nette\Database\Connection
?
Parece que temos muitas opções. Ele pode tirá-lo de uma variável estática em algum lugar. Ou herdá-la de uma classe que fornece uma conexão de banco de dados. Ou tirar vantagem de um único botão. Ou usar 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: Que seja passada a você: todas as dependências que a classe precisa devem ser passadas a ela. Porque se quebrarmos a regra, embarcamos num caminho de código sujo cheio de dependências ocultas, incompreensíveis, e o resultado será uma aplicação que será dolorosa de manter e desenvolver.
O usuário da classe Article
não tem idéia onde o método save()
armazena o artigo. Em uma
tabela de banco de dados? Qual delas, produção ou teste? E como pode ser mudado?
O usuário tem que olhar como o método save()
é implementado, e encontra o uso do método
DB::insert()
. Portanto, ele tem que pesquisar mais para descobrir como este método obtém uma conexão de banco de
dados. E as dependências ocultas podem formar uma cadeia bastante longa.
Em código limpo e bem projetado, nunca há dependências ocultas, fachadas de Laravel, ou variáveis estáticas. 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,
]);
}
}
Uma abordagem ainda mais prática, como veremos mais adiante, será através do 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 pensar que Article
não deveria ter um método
save()
; ele deveria representar um componente de dados puramente, e um repositório separado deveria se encarregar de
salvar. Isso faz sentido. Mas isso nos levaria muito além do escopo do tópico, que é a injeção de dependência, e o esforço
para fornecer exemplos simples.
Se você escreve uma classe que requer, por exemplo, um banco de dados para seu funcionamento, não invente de onde obtê-lo, mas faça com que ele passe. Seja como um parâmetro do construtor ou outro método. Admita dependências. Admita-as na API de sua classe. Você obterá um código compreensível e previsível.
E quanto a 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, ou seja, o diretório com o arquivo de log, é obtida pela própria classe a partir da constante.
Vejam 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 à questão de onde as mensagens são escritas? Você adivinharia que a
existência da constante LOG_DIR
é necessária para seu funcionamento? E você poderia criar uma segunda instância
que escrevesse 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 compreensível, 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(), eu não quero lidar com o banco de dados; eu só quero que ele seja salvo no 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 pontos válidos.
Como exemplo, vejamos uma aula que envia boletins informativos e registros de como foi:
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 melhorado Logger
, que não usa mais a constante LOG_DIR
, requer a especificação do caminho do
arquivo 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: Que seja passada a você: passe todos os dados que a classe precisa.
Então isso significa que passamos o caminho para o tronco através do construtor, que depois usamos ao 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, assim não! O caminho não faz parte dos dados que a classe NewsletterDistributor
precisa; na verdade, o
Logger
precisa dele. Você vê a diferença? A classe NewsletterDistributor
precisa do próprio
madeireiro. Então, é isso que vamos passar:
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 também faz
parte de sua funcionalidade. E a tarefa de trocar o madeireiro por outro, talvez para testes, é completamente trivial. Além
disso, se o construtor da classe Logger
mudar, isso não afetará nossa classe.
Regra # 2: Tome o que é seu
Não se deixe enganar e não se deixe passar pelas dependências de suas dependências. Basta passar suas próprias 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 acima de tudo, será trivial substituir estas dependências por outras.
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 que escreve para um
arquivo, a outra para um banco de dados … o nome não lhe parece estranho? Não seria melhor renomear Logger
para
FileLogger
? Definitivamente sim.
Mas façamos isso de forma inteligente. Criamos uma interface com o nome original:
interface Logger
{
function log(string $message): void;
}
… que ambos os madeireiros irão implementar:
class FileLogger implements Logger
// ...
class DatabaseLogger implements Logger
// ...
E por causa disso, não haverá necessidade de alterar nada no resto do código onde o madeireiro é utilizado. Por exemplo,
o construtor da classe NewsletterDistributor
ainda estará satisfeito com a exigência de Logger
como
parâmetro. E caberá a nós qual instância passaremos.
É por isso que nunca adicionamos o sufixo Interface
ou o prefixo I
aos nomes das
interfaces. Caso contrário, não seria possível desenvolver o código tão bem.
Houston, temos um problema
Embora possamos passar com uma única instância do registrador, seja baseada em arquivo ou em banco de dados, em toda a
aplicação e simplesmente passá-la onde quer que algo esteja registrado, é bastante diferente para a classe
Article
. Criamos suas instâncias conforme a necessidade, mesmo várias vezes. Como lidar com a dependência do banco
de dados em seu construtor?
Um exemplo pode ser um controlador que deve salvar um artigo no banco de dados depois de submeter 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 é óbvia: passar o objeto do banco de dados para o construtor EditController
e usar
$article = new Article($this->db)
.
Assim 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
. Passar o banco de dados vai contra a
regra nº 2: pegue o que é seu. Se o construtor da classe Article
mudar (um novo parâmetro é adicionado), você precisará modificar o código onde quer que as instâncias sejam
criadas. Ufff.
Houston, o que você sugere?
Regra nº 3: Deixe a Fábrica tratar disso
Ao eliminar dependências ocultas e passar todas as dependências como argumentos, ganhamos classes mais configuráveis e flexíveis. E, portanto, precisamos de algo mais para criar e configurar essas classes mais flexíveis para nós. Vamos chamá-la 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.
Por favor, não confunda com o padrão de projeto método de fábrica, que descreve uma maneira específica de usar as fábricas e não está relacionado a este tópico.
Fábrica
Uma fábrica é um método ou classe que cria e configura objetos. Vamos nomear a classe que produz Article
como
ArticleFactory
, e pode 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 será 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, se a assinatura do construtor da classe Article
mudar, a única parte do código que precisa reagir
é o próprio ArticleFactory
. Todos os outros códigos que trabalham com objetos Article
, como o
EditController
, não serão afetados.
Você pode estar se perguntando se nós realmente fizemos as coisas melhorarem. A quantidade de código aumentou, e tudo começa a parecer suspeitosamente complicado.
Não se preocupe, logo chegaremos ao recipiente Nette DI. E ele tem vários truques na manga, o que simplificará muito as
aplicações de construção usando a injeção de dependência. Por exemplo, ao invés da classe ArticleFactory
,
você só precisará escrever uma interface simples:
interface ArticleFactory
{
function create(): Article;
}
Mas estamos nos adiantando; por favor, seja paciente :-)
Sumário
No início deste capítulo, prometemos mostrar-lhe um processo para projetar um código limpo. Tudo o que é preciso é que as aulas o façam:
- passar as dependências de que necessitam
- por outro lado, não passar o que eles não precisam diretamente
- e que os objetos com dependências são melhor criados em fábricas
À primeira vista, estas três regras podem não parecer ter conseqüências de longo alcance, mas elas levam a uma perspectiva radicalmente diferente sobre o desenho de códigos. Será que vale a pena? Os desenvolvedores que abandonaram velhos hábitos e começaram a usar de forma consistente a injeção de dependência consideram esta etapa um momento crucial em suas vidas profissionais. Ela abriu o mundo de aplicações claras e de fácil manutenção para eles.
Mas e se o código não usar a injeção de dependência de forma consistente? E se ele se baseia em métodos estáticos ou singletons? Isso causa algum problema? Sim, e muito fundamentais.