Introdução a injeção de dependência com Jakarta CDI

Neste artigo você vai ver:

Aprenda a deixar seu código legível com injeção de dependência e Jakarta CDI

Dentro da programação, a orientação a objetos (também conhecida como OOP) é um dos paradigmas mais ricos em documentações e padrões, como o caso da injeção de dependência

No entanto, o mais comum é que esses padrões sejam mal interpretados e implementados e o que permitiria uma visão clara e um bom design para uma aplicação cooperativa, na prática, prejudica a qualidade e a eficiência de seu código. 

Neste artigo, vou trazer as vantagens de explorar a orientação a objetos, mais especificamente a injeção de dependência, em conjunto com a especificação do mundo Java que cobre essa API: o Jakarta CDI.

O que é injeção de dependência?

O primeiro passo é contextualizar o que é injeção de dependência e por que ele é diferente de inversão de controles, embora os dois conceitos sejam muito confundidos por aí.

No contexto da OOP (Orientação a Objetos), a dependência está relacionada a qualquer subordinação direta de uma classe e que pode ser feita de diversas formas. Por exemplo, através de construtores, de métodos de acesso, como os setters, ou de métodos de criação, como o Factory Method.

public Person(String name) {
 this.name = name;
}

Trecho de código com exemplo de dependência através do construtor, no qual a pessoa necessita de um nome para a sua criação.

Dentro dessa injeção de exemplo, também é possível medir o quanto uma classe está diretamente ligada à outra, que é o que chamamos de acoplamento. Isso é muito importante porque faz parte do trabalho em um bom design de código se preocupar com uma alta coesão e um baixo acoplamento.

A diferença entre injeção e inversão de dependências 

Uma vez explicados os conceitos base de OOP, o próximo passo é entender o que difere a injeção da inversão de dependência.

Essa distinção acontece por causa do que chamamos de DIP, ou Princípio de Inversão de Dependência, que funciona como um guia de boas práticas focadas no desacoplamento de uma classe de dependências concretas por meio de duas recomendações:

  1. Módulos de alto nível não devem depender dos de baixo nível, porém ambos precisam depender de abstrações (Exemplo: interfaces).
  2. As abstrações, por sua vez, não devem depender de detalhes. Entretanto, detalhes ou implementações concretas devem depender de abstrações.

Enquanto que a inversão pressupõe todos esses requisitos, a injeção de dependências é uma técnica de DIP para suprir as dependências de uma classe, no geral através de um construtor, atributo ou um método como um setter. 

Em resumo: a injeção de dependências faz parte do conjunto de boas práticas defendidas pela inversão de controle (IoC).
Por mais difíceis que esses conceitos sejam, em um primeiro momento, eles são fundamentais porque podem ser correlacionados ao princípio da Barbara Liskov, afinal tanto o princípio de Liskov quanto a injeção de dependência fazem parte do SOLID.

Onde entra o CDI e como chegamos no Jakarta CDI?

Assim como no caso da injeção e da inversão, é importante dar um passo atrás e contextualizar o que é CDI. 

O CDI (Injeção de Dependências e Contextos), na realidade, surgiu da necessidade de criar uma especificação dentro do JCP, JSR 365, principalmente porque, no mundo Java – assim como em projetos como Spring e Google Guice – existem diversas soluções e implementações desses frameworks. 

Por isso que, com a chegada do Jakarta EE, devido à mudança para a Eclipse Foundation, nasceu o Jakarta CDI (conheça mais sobre ele aqui).

Em suma, o objetivo do Jakarta CDI é permitir a gestão do controle de vida de componentes stateful ou stateless via contexto e injeção de componentes.

Esse é um projeto em constante evolução. Neste momento, está sendo otimizado para melhorar a inicialização através do CDI Lite. Caso queira acompanhar as últimas atualizações vale a pena conhecer o Eclipse Open-DI.

CDI na prática

Para exemplificar os recursos do CDI, criaremos uma simples aplicação Java SE com CDI para mostrar seis dos recursos mais importantes dentro do CDI, que são:

  1. Realizar uma simples injeção.
  2. Diferenciar implementações através de qualificações.
  3. Ensinar o CDI a criar e distribuir objetos.
  4. Aplicar o Observer.
  5. Aplicar o Decorator.
  6. Utilizar o interceptor.

Neste artigo, até para não deixar o texto muito grande, colocaremos os destaques dos códigos. Se quiser, você pode depois acessar o código na íntegra.

É bacana mencionar que este conteúdo foi criado em conjunto com a Karina Varela e faz parte das aulas que fizemos ao longo do ano de 2021, principalmente em países da América e na Europa, em parceria com a Microstream e a Payara

1. Realizar uma simples injeção

Na nossa primeira implementação, vamos criar uma interface “Veículo” e uma implementação usando o container do Java SE. 

Um ponto importante: além da injeção de dependências, temos o contexto, ou seja, podemos definir o ciclo de vida de uma classe ou bean e, neste caso, a implementação terá o escopo de aplicação.

try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
    Vehicle vehicle = container.select(Vehicle.class).get();
    vehicle.move();

    Car car = container.select(Car.class).get();
    car.move();

    System.out.println("Is the same vehicle? " + car.equals(vehicle));
}

public interface Vehicle {

    void move();
}

@ApplicationScoped
public class Car implements Vehicle {
//... implementation here
}

Neste pedaço de código, temos a interface “Vehicle (Veículo)” e sua respectiva implementação, “Car (Carro)”, na qual o CDI poderá injetar uma instância tanto pela interface quanto pela implementação. 

Já que estamos falando de boas práticas, entenda que estamos usando o caso de uma única interface para uma única implementação para fins didáticos. Na prática, o ideal é que você não faça isso para não quebrar o princípio KISS.

Um bom indicador para isso são interfaces que começam com “I” ou implementações que terminam com “Impl”. Além de indicador de complexidade desnecessária, é um code smell, afinal não é um nome significado e isso quebra o princípio do Clean Code.

2. Diferenciar implementações através de qualificações

No exemplo anterior, tivemos o caso de uma relação um para um, ou seja, uma interface para uma única implementação. Porém, o que acontece quando temos várias implementações para a mesma interface?

Se não tivermos isso definido, o CDI não saberá qual é a implementação padrão e, com isso, lançará o AmbiguousResolutionException. Você pode resolver este problema usando a anotação Named ou um Qualificador. No nosso caso, usaremos os Qualificadores.
Imagine o seguinte cenário no qual temos uma orquestra com vários instrumentos musicais. Essa orquestra precisará tocar todos os instrumentos junto, além de poder discriminá-los. No nosso exemplo, esse cenário seria algo semelhante ao código a seguir:

public interface Instrument {
    String sound();
}

public enum InstrumentType {
    STRING, PERCUSSION, KEYBOARD;
}

@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface MusicalInstrument {
    InstrumentType value();
}

Neste exemplo de código, temos a interface de instrumento, um enumerador para definir o seu tipo e o Qualificador do CDI através de uma nova anotação.

Depois das implementações e dos qualificadores, ficará bastante simples para orquestra tocar todos os instrumentos juntos, além de selecionar instrumentos considerando seus diversos tipos, por exemplo, se é de corda, percussão ou madeira.

@MusicalInstrument(InstrumentType.KEYBOARD)
@Default
class Piano implements Instrument {
    @Override
    public String sound() {
        return "piano";
    }
}

@MusicalInstrument(InstrumentType.STRING)
class Violin implements Instrument {
    @Override
    public String sound() {
        return "violin";
    }
}

@MusicalInstrument(InstrumentType.PERCUSSION)
class Xylophone implements Instrument {
    @Override
    public String sound() {
        return "xylophone";
    }
}

@ApplicationScoped
public class Orchestra {

    @Inject
    @MusicalInstrument(InstrumentType.PERCUSSION)
    private Instrument percussion;
    @Inject
    @MusicalInstrument(InstrumentType.KEYBOARD)
    private Instrument keyboard;

    @Inject
    @MusicalInstrument(InstrumentType.STRING)
    private Instrument string;

    @Inject
    private Instrument solo;

    @Inject
    @Any
    private Instance instruments;

}

Temos as implementações de instrumentos musicais e seus respectivos objetos que, por fim, resultam na injeção através da orquestra.

Um ponto interessante é que, além dos qualificadores, definimos o “Piano” como a implementação padrão para a orquestra e o melhor: o cliente – neste caso, a orquestra – não precisa saber desses detalhes.

Orchestra orchestra = container.select(Orchestra.class).get();
orchestra.percussion();
orchestra.keyboard();
orchestra.string();
orchestra.solo();
orchestra.allSound();

O container do CDI injeta uma instância de Orquestra e demonstra o uso de cada instrumento.

3. Ensinar o CDI a criar e distribuir objetos

Muitas vezes, não é possível, além de definir seu escopo, realizar atividades como definir ou criar uma classe dentro do container do CDI. Esse tipo de recurso é importante para casos como, por exemplo, quando queremos injetar a criação de uma moeda, money-api, dentro de um e-commerce ou uma conexão com algum serviço. 
Dentro do CDI, é possível ensinar o container a criar instâncias e as destruí-las através das anotações Produces e Disposes, respectivamente. Para esse exemplo, criaremos uma conexão que será criada e fechada.

public interface Connection extends AutoCloseable {

    void commit(String execute);
}

@ApplicationScoped
class ConnectionProducer {


    @Produces
    Connection getConnection() {
        return new SimpleConnection();
    }

    public void dispose(@Disposes Connection connection) throws Exception {
        connection.close();
    }

}

Neste momento, temos uma conexão do qual ensinamos como o CDI deve criar e fechar a conexão. Com isso, o cliente não precisará se preocupar em fechar a conexão tão logo ela não seja necessária, uma vez que esta responsabilidade será do CDI.

Connection connection = container.select(Connection.class).get();
connection.commit("Database instruction");

Para este exemplo, injetamos uma instância de Connection, executamos uma operação e não nos preocupamos com o fechamento do recurso por parte do cliente, uma vez que será responsabilidade do CDI.

4. Aplicar o Observer

Dentre os padrões bastante presentes nas arquiteturas corporativas e presentes no GoF, não podemos esquecer do Observer.

Uma das avaliações que faço desse padrão é que, dada sua importância, podemos vê-lo de modo similar em padrões arquiteturais, como a Orientação a Eventos, e até mesmo em paradigma com programação reativa.

No CDI podemos lidar com eventos de forma síncrona e assíncrona. Imagine, por exemplo, que temos um jornalista e ele precisará notificar todas as mídias necessárias. Se fizermos o acoplamento dessas mídias diretamente na classe “Jornalista”, toda vez que uma mídia for adicionada ou removida, será necessário modificá-la. Isso faz com que se quebre o princípio de open-closed para solucionar usaremos o Observer.

@ApplicationScoped
public class Journalist {

    @Inject
    private Event event;

    @Inject
    @Specific
    private Event specificEvent;

    public void receiveNews(News news) {
        this.event.fire(news);
    }
}


public class Magazine implements Consumer {

    private static final Logger LOGGER = Logger.getLogger(Magazine.class.getName());

    @Override
    public void accept(@Observes News news) {
        LOGGER.info("We got the news, we'll publish it on a magazine: " + news.get());
    }
}

public class NewsPaper implements Consumer {

    private static final Logger LOGGER = Logger.getLogger(NewsPaper.class.getName());

    @Override
    public void accept(@Observes News news) {
        LOGGER.info("We got the news, we'll publish it on a newspaper: " + news.get());
    }
}
public class SocialMedia implements Consumer {

    private static final Logger LOGGER = Logger.getLogger(SocialMedia.class.getName());

    @Override
    public void accept(@Observes News news) {
        LOGGER.info("We got the news, we'll publish it on Social Media: " + news.get());
    }
}

Sendo assim, criamos uma classe “Jornalista” que notifica as mídias, uma novidade graças ao padrão Observer com o CDI. O evento é disparado pela instância do “Event” e para escutá-lo, é necessário usar a anotação “@Observers” com o específico parâmetro a ser escutado.

5. Aplicar o Decorator

O padrão Decorator nos permite adicionar um comportamento dentro do objeto, obedecendo o princípio de composição sobre herança. Dentro do mundo Java, vemos isso com os Wrappers do tipo primitivo como Integer, Double, Long etc.

No nosso exemplo, será criado um trabalhador que será decorado por um gerente, assim, adicionamos o comportamento de enviar um e-mail para cada atividade de um trabalhador.

public interface Worker {

    String work(String job);
}

@ApplicationScoped
public class Programmer implements Worker {

    private static final Logger LOGGER = Logger.getLogger(Programmer.class.getName());

    @Override
    public String work(String job) {
        return "A programmer has received a job, it will convert coffee in code: " + job;
    }
}

@Decorator
@Priority(Interceptor.Priority.APPLICATION)
public class Manager implements Worker {

    @Inject
    @Delegate
    @Any
    private Worker worker;

    @Override
    public String work(String job) {
        return "A manager has received a job and it will delegate to a programmer -> " + worker.work(job);
    }
}

Com isso, criamos uma abstração “trabalhador” (Worker), o “programador” (Programmer), e o “Manager” como responsável por delegar o trabalhador. Desse modo, conseguimos adicionar um comportamento, como enviar e-mail, sem modificar o programador.

Worker worker = container.select(Worker.class).get();
String work = worker.work("Just a single button");
System.out.println("The work result: " + work);

6. Utilizar o interceptor

Com o CDI, também é possível realizar e controlar algumas operações no código de modo transversal muito semelhante ao que fazemos com a programação orientada a aspecto e pontos de corte com Spring.

O interceptor do CDI tende a ser bastante útil quando queremos, por exemplo, um mecanismo de log, controle de transação ou um timer para um método que será executado, dentre outros. Neste caso, o exemplo utilizado será criado um timer com interceptor.

@InterceptorBinding
@Target({METHOD, TYPE})
@Retention(RUNTIME)
public @interface Timed {
}

@Timed
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class TimedInterceptor {

    private static final Logger LOGGER = Logger.getLogger(TimedInterceptor.class.getName());

    @AroundInvoke
    public Object auditMethod(InvocationContext ctx) throws Exception {
        long start = System.currentTimeMillis();
        Object result = ctx.proceed();
        long end = System.currentTimeMillis() - start;
        String message = String.format("Time to execute the class %s, the method %s is of %d milliseconds",
                ctx.getTarget().getClass(), ctx.getMethod(), end);
        LOGGER.info(message);
        return result;
    }
}

A implementação do interceptor permitirá a criação da anotação que será usada para indicar a intercepção e a classe, por sua vez, definirá como essa interceptação será implementada, o que, no nosso caso, será um contador.

O próximo e último exemplo é o de criar dois métodos que contaremos, para isso, com duas implementações, sendo que uma delas terá um delay de dois segundos.

public class FastSupplier implements Supplier {

    @Timed
    @Override
    public String get() {
        return "The Fast supplier result";
    }
}

public class SlowSupplier implements Supplier {

    @Timed
    @Override
    public String get() {
        try {
            TimeUnit.MILLISECONDS.sleep(200L);
        } catch (InterruptedException e) {
            //TODO it is only a sample, don't do it on production :)
            throw  new RuntimeException(e);
        }
        return "The slow result";
    }
}


Supplier fastSupplier = container.select(FastSupplier.class).get();
Supplier slowSupplier = container.select(SlowSupplier.class).get();
System.out.println("The result: " + fastSupplier.get());
System.out.println("The result: " + slowSupplier.get());

Duas classes supplier anotadas com a anotação Timed do qual será interceptado, colocando no log o tempo de execução dos dois métodos.

Injeção de dependência com Jakarta CDI: mais opções para o seu desenvolvimento

Diante desses exemplos práticos, podemos ver as diversas possibilidades e desafios possíveis quando atuamos com Orientação a Objetos, além de conhecer melhor alguns dos seus princípios ao redor da injeção de dependências e do CDI. 

É importante lembrar que, como já disse o Tio Ben, “com grandes poderes, vem grandes responsabilidades”. Sendo assim, o bom senso continua sendo a melhor bússola para explorar esses e outros recursos do CDI, resultando em um design de alta qualidade e clareza.
Você já conhecia esse recurso dentro do mundo Java? Comenta aqui embaixo para trocarmos mais experiências no assunto. 

Referências:

Capa do artigo sobre Introdução a injeção de dependência com Jakarta CDI, onde vemos uma pessoa escrevendo código em um notebook.
Foto Otávio Santana
Distinguished Software Engineer
Capacitando devs em todo o mundo para fornecer melhores softwares na nuvem.

Este site utiliza cookies para proporcionar uma experiência de navegação melhor. Consulte nossa Política de Privacidade.