DEV Community 👩‍💻👨‍💻

João Victor Martins
João Victor Martins

Posted on

[PT-BR] Desacoplando módulos JPMS

Quando estamos desenvolvendo um software, temos sempre que trabalhar para ter um menor acoplamento entre nossos componentes. Alguns padrões (patterns) foram pensados para nos auxiliar nessa missão. Neste post será apresentado uma aplicação desenvolvida com JPMS (Java Platform Module System), onde aplicou-se o Service Pattern para desacoplamento dos módulos. A ideia é explicar com detalhes sobre o que foi feito.

A aplicação

A ideia do projeto desenvolvido é que o usuário possa selecionar uma fonte de dados para gravar algum objeto. Essas fontes podem ser bancos de dados, arquivos ou qualquer outra que cumpra o requisito. Deve-se inserir novas fontes de dados sempre que necessário.

Alt Text

Na imagem acima, observa-se um módulo br.com.fontededados.application que consome (consumer) informações dos módulos br.com.fontededados.mysql e br.com.fontededados.arquivo (producers). Porém, desta maneira o consumidor irá conhecer detalhes da implementação dos produtores e quando for necessário incluir um novo produtor, será necessário alterar o consumidor. Para tratar o alto acoplamento entre os módulos, foi utilizado o Service Pattern. Este padrão utiliza uma api entre consumer e producers, fazendo com que um não saiba da existência do outro, ocasionando desacoplamento dos módulos e maior flexibilidade na inserção de novas fontes de dados.

Alt Text

Na próxima seção será mostrado o código desenvolvido para obter este resultado.

Vamos ao código

A aplicação possui o módulo br.com.fontededados.api que tem uma interface FonteDeDados e o module-info. O conteúdo da interface pode ser visto abaixo:

public interface FonteDeDados {

    public void gravar();

    public String getNome();
}
Enter fullscreen mode Exit fullscreen mode

E o module-info:

module br.com.fontededados.api {
    exports br.com.fontededados.api;
}
Enter fullscreen mode Exit fullscreen mode

A interface faz parte do pacote br.com.fontededados.api e é exportada para outros módulos.

Para o exemplo do post, será utilizado um consumer CLI (Command Line Interface). Ele faz parte do módulo br.com.fontededados.application, possui um arquivo module-info e uma classe FonteDeDadosCLI. A seguir o conteúdo da classe:

// Pacote e imports ocultos
public class FonteDeDadosCLI {

    public static void main(String[] args) {
        System.out.println(listarNomes());
    }

    public static List<String> listarNomes() {
        List<String> nomes = new ArrayList<>();
        ServiceLoader<FonteDeDados> fontes = carregar();
        if(fontes != null) { 
            fontes.forEach(fonte -> nomes.add(fonte.getNome()));
        return Collections.unmodifiableList(nomes);
        } return nomes;
    }

    public static ServiceLoader<FonteDeDados> carregar() {
        return ServiceLoader.load(FonteDeDados.class);
    }
}
Enter fullscreen mode Exit fullscreen mode

Para entender o que o código faz, é necessário entender o que é ServiceLoader.

O Service Loader é um facilitador no carregamento de provedores de serviço. Um provedor de serviço é composto por interface ou classe abstrata (Service Provider Interface - SPI) e implementação (Service Provider). A classe está na plataforma desde o Java 6, porém foram realizadas mudanças para facilitar o trabalho com JPMS.

Ao chamar o método carregar(), cabe o método load retornar uma instância do ServiceLoader. Caso a instância não esteja nula, é porque existe pelo menos uma classe concreta (Service Provider) que implementa a interface (SPI) FonteDeDados.

O método listarNomes() é responsável por iterar sobre a instância de ServiceLoader e criar instâncias das classes que possuem implementação de FonteDeDados. Como são objetos Java, pode-se chamar o método getNome() de cada uma das instâncias, com o objetivo de popular uma lista de Strings, que representarão as fontes de dados disponíveis para gravação do objeto.

A seguir será explorado o conteúdo do module-info:

module br.com.fontededados.application {
    requires br.com.fontededados.api;

    uses br.com.fontededados.api.FonteDeDados;
}
Enter fullscreen mode Exit fullscreen mode

A palavra reservada uses instrui o ServiceLoader que o módulo br.com.fontededados.application deseja usar implementações de FonteDeDados.

Para compilar os módulos acima, pode-se usar o comando

javac -d mods --module-source-path src -m br.com.fontededados.application

Um diretório mods será criado e possuirá os bytecodes de br.com.fontededados.application e br.com.fontededados.api.

Pode-se executar a aplicação com o comando abaixo:

java --module-path mods -m br.com.fontededados.application/br.com.fontededados.application.FonteDeDadosCLI

O resultado será um [], pois não há implementações de FonteDeDados.

O próximo passo será mostrar o producer. O módulo br.com.fontededados.mysql possui uma classe chamada FonteDeDadosMySQL e um module-info. A classe possui o seguinte conteúdo:

// Pacote e imports ocultos
public class FonteDeDadosMySQL implements FonteDeDados {

    public void gravar() {
        System.out.println("Implementação do método gravar com MySQL");
    }

    public String getNome() {
        return "mysql";
    }
}
Enter fullscreen mode Exit fullscreen mode

O objetivo não é mostrar possíveis implementações do método gravar(..)

A grande questão é que apenas implementando a interface FonteDeDados não é o suficiente para o ServiceLoader carregá-la. É necessário utilizar um recurso no module-info que será mostrado a seguir:

module br.com.fontededados.mysql {
    requires br.com.fontededados.api;

    provides br.com.fontededados.api.FonteDeDados
        with br.com.fontededados.mysql.FonteDeDadosMySQL;
}
Enter fullscreen mode Exit fullscreen mode

O provides/with é justamente o responsável por declarar que o módulo em questão provê uma implementação da interface FonteDeDados (SPI) com a classe FonteDeDadosMySQL (Service Provider) e assim poderá ser carregado pelo ServiceLoader.

É importante destacar que só será possível usar o provides se a interface em questão estiver no mesmo módulo que a está declarando ou em um módulo que seja possível usar o requires.

Para finalizar, o módulo será compilado e colocado no mesmo module-path dos módulos anteriores.

javac -d mods --module-source-path src -m br.com.fontededados.mysql

Pode-se executar o projeto com o comando abaixo:

java --module-path mods -m br.com.fontededados.application/br.com.fontededados.application.FonteDeDadosCLI

E agora o resultado será:

[mysql]

Percebe-se que para o correto funcionamento não foi necessário recompilar todos módulos, compilando apenas o provider, o projeto funcionou como esperado.

É importante reforçar que os detalhes das implementações dos providers não estão sendo expostos. Quando as instâncias forem criadas em application, o módulo saberá o que a instância faz, mas não como faz.

Revisando o diagrama, tem-se este resultado:

Alt Text

Concluindo...

O Service Pattern trabalha com producers e consumers. O Service Provider são classes que provêm implementações para Service Provider Interface (SPI's). Um ou mais consumers poderão usar essas implementações, graças ao ServiceLoader, que as instanciam. É importante destacar que o consumidor sabe o que a instância faz, mas não como ela faz e assim consegue-se o desacoplamento tão desejado. No exemplo foi visto a aplicação do pattern de uma forma simples e espero que a ideia tenha sido compreendida. Se ficou alguma dúvida, estarei aberto a esclarece-las.

Top comments (0)

12 APIs That You Will Love

>> Check out this classic DEV post <<