Dependency Injection Container - combinando padrões de projeto
Nesse artigo exploramos os padrões de projeto Registry, Singleton, Proxy e Decorator com a implementação de um container de injeção de dependências.

Injeção de dependência (dependency injection - ou DI) é uma técnica de programação na qual objetos ou funções recebem outros objetos e funções que eles necessitam, ao invés de construí-los internamente. Abaixo segue uma forma simples de injeção de dependência:
// Definindo as classes
class MinhaClasse {
constructor(minhaDependencia: MinhaDependencia) {}
}
class MinhaDependencia {
}
// Construindo objetos
const meuObjeto = new MinhaClasse(new MinhaDependencia());
Imagine, no entanto, que meuObjeto
dependa de vários outros objetos, e cada um desses objetos dependa de mais alguns e assim por diante... Com o passar do tempo a construção de um objeto simples como meuObjeto
poderia se tornar muito verbosa e de difícil manutenção, vamos a um exemplo:
// Definindo classes
class MinhaClasse {
constructor(minhaDependencia1: MinhaDependencia1, minhaDependencia2: MinhaDependencia2) {}
}
class MinhaDependencia1 {
constructor(outraDependencia: OutraDependencia1) {}
}
class MinhaDependencia2 {
constructor(outraDependencia: OutraDependencia2) {}
}
class OutraDependencia1 {
}
class OutraDependencia2 {
}
// Construindo objetos
const dependencia1 = new MinhaDependencia1(new OutraDependencia1());
const dependencia2 = new MinhaDependencia2(new OutraDependencia2());
const meuObjeto = new MinhaClasse(dependencia1, dependencia2);
// ...
Para esses tipos de situação um container de injeção de dependência (dependency injection container - ou DI container) pode ser útil principalmente para facilitar a leitura e, algumas vezes, ajudando a evitar duplicação de código.
Um container de injeção de dependência, de forma resumida, é uma classe - normalmente global - capaz de criar e configurar objetos. Sendo assim, ele deve ter pelo menos duas funcionalidades bem simples: registrar e injetar dependências.
Ao decorrer desse artigo iremos construir e utilizar um container de injeção de dependência simples utilizando alguns padrões de projeto. O código desenvolvido foi baseado em uma implementação realizada em uma das aulas do curso de Clean Code e Clean Architecture do Rodrigo Branas e desse artigo em seu blog.
Conhecendo o padrão Registry
Martin Fowler, em seu livro Patterns of Enterprise Application Architecture, descreve o padrão Registry como "um objeto [global] bem conhecido que outros objetos podem utilizar para encontrar serviços e objetos comuns" (tradução livre).
Diante das características de um container de injeção de dependência e levando em conta a função de um Registry, precisamos garantir que todos os objetos da aplicação acessem a mesma instância de Registry - e considerando que estamos trabalhando com uma aplicação single-threaded - outro padrão pode nos ajudar com essa tarefa. Estamos falando do...
Singleton
Conforme descrito no livro Padrões de Projeto (Gang of Four), o padrão singleton é um padrão de criação cujo objetivo é "garantir que uma classe tenha somente uma instância e fornecer um ponto global de acesso para a mesma".
Anatomia de um singleton
Em geral um singleton possui algumas características que permitem identificá-lo de uma maneira rápida e direta, são elas:
- Propriedade estática que armazena a instância da classe
- Construtor privado
- Método estático para recuperar a instância do objeto ou instanciá-lo caso não exista.
// Definindo singleton
class MeuSingleton {
static instance: MeuSingleton;
private constructor() {
// ...
}
static getInstance() {
if (!MeuSingleton.instance) {
MeuSingleton.instance = new MeuSingleton();
}
return MeuSingleton.instance;
}
}
// utilizando singleton
const singleton = MeuSingleton.getInstance();
Dessa forma, sempre que invocarmos o método getInstance()
estaremos recebendo a mesma instância de MeuSingleton
independente de onde o chamarmos (desde que esteja executando no mesmo processo).
Criando o Container
O container de injeção de dependência deve possuir, pelo menos duas funcionalidades: registrar dependências e injetar dependências.
Combinando o Registry com o Singleton, conseguimos garantir que todas as dependências registradas no container estarão sempre na mesma instância, independente de onde ele for chamado:
// Definindo registry (DI container)
class Registry {
dependencies: {[name: string]: any}
static instance: Registry;
private constructor() {
this.dependencies = {};
}
register(name: string, dependency: any) {
this.dependencies[name] = dependency
}
inject(name: string) {
if(!this.dependencies[name]) throw new Error("Dependency not found");
return this.dependencies[name];
}
static getInstance() {
if (!Registry.instance) {
Registry.instance = new MeuSingleton();
}
return Registry.instance;
}
}
Com o que temos até aqui já é possível termos os benefícios de um container de injeção de dependência em nossa aplicação, podemos construir o container e passá-lo por injeção às nossas classes, por exemplo:
// main.ts
const registry = Registry.getInstance();
registry.register("minhaDependencia", new MinhaDependencia);
const meuObjeto = new MinhaClasse(registry);
// MinhaClasse.ts
class MinhaClasse() {
private dependency: MinhaDependencia;
constructor(registry: Registry) {
this.dependency = registry.inject("minhaDependencia");
}
}
Essa forma de implementação já é suficiente para simplificar nosso problema inicial, porém ainda exige que todas as nossas classes recebam uma instância de registry para serem capazes de injetar a dependência correspondente para sua correta execução.
Porém ainda podemos simplificar. Algumas linguagens, como o typescript
, implementam, um recurso chamado decorator (ou annotations) que de maneira bem resumida e superficial, podemos definir como, atalhos (açúcar sintático - sugar syntax) de implementação do padrão de projeto de mesmo nome.
Decorator
Decorator é um padrão estrutural descrito no livro Padrões de Projeto (Gang of Four) cujo objetivo é "dinamicamente, agregar responsabilidades adicionais a um objeto".
Em essência o decorator define a mesma interface do componente que será decorado (Component) e matém uma referência ao mesmo. Quando invocado, ele repassa solicitações para objeto podendo executar operações adicionais antes e depois de repassar a solicitação.
Anatomia de um decorator
Vamos ver uma maneira de implementar o padrão decorator:
// definindo o componente que será decorado
interface Component {
meuMetodo();
}
// Decorator Base
class Decorator implements Component{
private _component: Component
constructor(component: Component){
this._component = component;
}
meuMetodo() {
this._component.meuMetodo();
}
}
// Decorator específico
class MeuSuperDecorator extends Decorator {
private _param: string;
constructor(component: Component, param: string) {
super(component);
this._param = param;
}
meuMetodo() {
console.log(this.param);
super.meuMetodo();
}
}
No typescript, de maneira nativa, utilizamos decorators na forma de "função que retorna outra função" e podemos decorar classes, métodos, propriedades, acessores e parâmetros.
Fazendo uma ponte entre a implementação de decorators do typescript
com o que é definido pelo padrão Decorator podemos dizer que a função mais interna é o método do componente, enquanto a externa é a chamada do decorator.
// definindo o decorator
function meuDecorator(parametro: string) {
return (target: any, propertyKey: string) => {
// faz alguma coisa
}
}
// Utilizando decorator
class MinhaClasse {
@meuDecorator("blabla")
minhaProp: string
}
Existe, porém, um detalhe quando utilizamos os decorators do typescript
no que diz respeito à sua inicialização: os decorators são executados assim que a classe é importada.
Esse comportamento nos traz alguns problemas pois podemos ter uma classe sendo importada antes do nosso register ser construido e, consequentemente, quando nossa classe for instanciada não termos dependência nenhuma injetada para utilizar. Para contornar esse problema podemos utilizar um outro padrão de projeto que é implementado de forma nativa pelo typescript
. Estamos falando do Proxy.
Proxy
Outro padrão estrutural presente no livro Padrões de Projeto (Gang of Four) e seu objetivo é "Fornecer um substituto ou marcador da localização de outro objeto para controlar o acesso a esse objeto". A ideia por trás de controlar o acesso a um objeto é adiar sua inicialização até o momento que realmente precisamos utilizá-lo.
A implementação do padrão é semelhante à implementação do decorator, o que muda nesse caso é a intenção de cada um.
Finalizando implementação
Agora que já entendemos um pouco sobre os padrões envolvidos na construção do nosso container, vamos implementar o decorator que utilizaremos nas nossas propriedades:
// registry.ts
export function inject(name: string) {
return (target: any, propertyKey: string) => {
target[propertyKey] = new Proxy({}, {
get(target: any, propertyKey: string, receiver: any) {
const dependency = Registry.getInstance().inject(name);
return dependency[propertyKey];
}
})
}
}
No código acima estamos exportando uma função inject
que utilizaremos da seguinte forma: @inject("nomeDaDependencia")
nas classes que terão a dependência injetada.
Essa função retorna uma outra função (que respeita a assinatura de decorators de propriedades) onde:
target
é o objeto epropertyKey
é a propriedade que está sendo decorada.
Quando invocada, essa função irá sobrescrever a propriedade da classe com um proxy que, quando chamado retornará a instância da dependência que registramos e injetamos.
Considerando o seguinte cenário:
class MinhaClasse {
@inject("minhaDependencia")
dep: Dependencia
}
Quando a classe for importada, dep
será um proxy que, quando chamada resolverá a dependência que foi previamente injetada.
Conclusão
A implementação final do container ficou da seguinte maneira:
// Registry.ts
export class Registry {
dependencies: {[name: string]: any}
static instance: Registry;
private constructor() {
this.dependencies = {};
}
register(name: string, dependency: any) {
this.dependencies[name] = dependency
}
inject(name: string) {
if(!this.dependencies[name]) throw new Error("Dependency not found");
return this.dependencies[name];
}
static getInstance() {
if (!Registry.instance) {
Registry.instance = new MeuSingleton();
}
return Registry.instance;
}
}
export function inject(name: string) {
return (target: any, propertyKey: string) => {
target[propertyKey] = new Proxy({}, {
get(target: any, propertyKey: string, receiver: any) {
const dependency = Registry.getInstance().inject(name);
return dependency[propertyKey];
}
})
}
}
Declaramos então as dependências que serão injetadas na classe desejada:
// MinhaClasse.ts
export class MinhaClasse {
@inject("minhaDependencia")
dependencia: Dependencia
execute() {
this.dependencia.fazAlgo();
console.log("chamou minha classe");
}
}
// Dependencia.ts
export class Dependencia {
fazAlgo() {
console.log("chamou dependencia");
}
}
E, no ponto de entrada (main.ts
) da aplicação basta construir as dependências e registrar no container que elas estarão disponíveis:
// main.ts
const registry = Registry.getInstance();
registry.register("minhaDependencia", new Dependencia());
new MinhaClasse();
Apesar de precisar de mais código e aumentar ligeiramente a complexidade a implementação de um container de injeção de dependência também traz vantagens como:
- A substituição das dependências fica mais simples, pois precisamos alterar um único ponto (e não precisamos procurar em diversos arquivos quais classes que utilizam aquela dependência);
- Código fica mais fácil de ser lido e entendido, uma vez que não temos mais classes com vários parâmetros sendo passados no construtor.
- Garantimos que sempre haverá uma única instância de cada dependência em todo o processo de execução da aplicação.