[PT-BR] Desacoplando módulos JPMS
João Victor Martins
Posted on September 4, 2020
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.
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.
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();
}
E o module-info
:
module br.com.fontededados.api {
exports br.com.fontededados.api;
}
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);
}
}
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;
}
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";
}
}
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;
}
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:
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.
Posted on September 4, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.