Estratégias de design para dissociação de módulos Java

Estratégias de design para dissociação de módulos Java

1. Visão geral

OJava Platform Module System (JPMS) fornece encapsulamento mais forte, mais confiabilidade e melhor separação de interesses.

Mas todos esses recursos úteis têm um preço. Como os aplicativos modularizados são construídos sobre uma rede de módulos que dependem de outros módulos para funcionar corretamente,in many cases, the modules are tightly-coupled to each other.

Isso pode nos levar a pensar que modularidade e acoplamento fraco são recursos que simplesmente não podem coexistir no mesmo sistema. Mas, na verdade, eles podem!

Neste tutorial, veremos em profundidade dois padrões de design bem conhecidos que podemos usar para desacoplar facilmente módulos Java.

2. O Módulo Pai

Para mostrar os padrões de design que usaremos para desacoplar os módulos Java, construiremos um projeto Maven de demonstração com vários módulos.

Para manter o código simples, o projeto conterá inicialmente dois módulos Maven eeach Maven module will be wrapped into a Java module.

O primeiro módulo incluirá uma interface de serviço, juntamente com duas implementações - os provedores de serviços. O segundo módulo usará os provedores para analisar um valorString.

Vamos começar criando o diretório raiz do projeto denominadodemoproject, e definiremos o POM pai do projeto:

pom


    servicemodule
    consumermodule



    
        
            
                org.apache.maven.plugins
                maven-compiler-plugin
                3.8.1
                
                    11
                    11
                
            
        
    

Vale ressaltar alguns detalhes na definição do POM pai.

Primeiro,the file includes the two child modules that we mentioned above, ou seja,servicemoduleeconsumermodule (vamos discuti-los em detalhes mais tarde).

A seguir, como estamos usando Java 11,we’ll need at least Maven 3.5.0 on our system, as Maven supports Java 9 and higher from that version onward.

Finalmente, também precisaremos de pelo menos a versão 3.8.0 doMaven compiler plugin. Portanto, para ter certeza de que estamos atualizados, verificaremosMaven Central para obter a versão mais recente do plugin do compilador Maven.

3. O módulo de serviço

Para fins de demonstração, vamos usar uma abordagem rápida e suja para implementar o móduloservicemodule, para que possamos identificar claramente as falhas que surgem com este design.

Vamos fazerthe service interface and the service providers public, colocando-os no mesmo pacote e exportando todos eles. Esta parece ser uma escolha de design bastante boa, mas como veremos em um momento,it highly increases the level of coupling between the project’s modules.

No diretório raiz do projeto, criaremos o diretórioservicemodule/src/main/java. Em seguida, precisamos definir o pacotecom.example.servicemodule, e colocar nele a seguinte interfaceTextService:

public interface TextService {

    String processText(String text);

}

A interfaceTextService é muito simples, então vamos agora definir os provedores de serviço.

No mesmo pacote, vamos adicionar uma implementaçãoLowercase:

public class LowercaseTextService implements TextService {

    @Override
    public String processText(String text) {
        return text.toLowerCase();
    }

}

Agora, vamos adicionar uma implementaçãoUppercase:

public class UppercaseTextService implements TextService {

    @Override
    public String processText(String text) {
        return text.toUpperCase();
    }

}

Finalmente, no diretórioservicemodule/src/main/java, vamos incluir o descritor do módulo,module-info.java:

module com.example.servicemodule {
    exports com.example.servicemodule;
}

4. O Módulo do Consumidor

Agora precisamos criar um módulo de consumidor que use um dos provedores de serviços que criamos antes.

Vamos adicionar a seguinte classecom.example.consumermodule.Application:

public class Application {
    public static void main(String args[]) {
        TextService textService = new LowercaseTextService();
        System.out.println(textService.processText("Hello from example!"));
    }
}

Agora, vamos incluir o descritor do módulo,module-info.java, na raiz de origem, que deve serconsumermodule/src/main/java:

module com.example.consumermodule {
    requires com.example.servicemodule;
}

Por fim, vamos compilar os arquivos de origem e executar o aplicativo, de dentro de nosso IDE ou de um console de comando.

Como podemos esperar, devemos ver a seguinte saída:

hello from example!

Definitivamente, isso funciona, mas com uma advertência importante a ser observada:we’re unnecessarily coupling the service providers to the consumer module.

Como estamos tornando os provedores visíveis para o mundo externo, os módulos de consumidor estão cientes deles.

Além disso, essa luta contra a fabricação de componentes de software depende de abstrações.

5. Fábrica de prestadores de serviços

Podemos facilmenteremove the coupling between the modules by exporting only the service interface. Por outro lado, os prestadores de serviços não são exportados, permanecendo ocultos dos módulos de consumo. Os módulos do consumidor veem apenas o tipo de interface de serviço.

Para conseguir isso, precisamos:

  1. Coloque a interface de serviço em um pacote separado, que é exportado para o mundo exterior

  2. Coloque os prestadores de serviços em um pacote diferente, que não é exportado

  3. Crie uma classe de fábrica, que é exportada. Os módulos do consumidor usam a classe factory para procurar os prestadores de serviços

Podemos conceituar as etapas acima na forma de um padrão de projeto:public service interface, private service providers, and public service provider factory.

5.1. Interface de serviço público

Para ver claramente como esse padrão funciona, vamos colocar a interface de serviço e os provedores de serviço em pacotes diferentes. A interface será exportada, mas as implementações do provedor não.

Então, vamos moverTextService para um novo pacote que chamaremos decom.example.servicemodule.external.

5.2. Provedores de serviços privados

Então, vamos da mesma forma mover nossosLowercaseTextService eUppercaseTextService paracom.example.servicemodule.internal.

5.3. Fábrica de prestadores de serviços públicos

Como as classes do provedor de serviço agora são privadas e não podem ser acessadas de outros módulos,we’ll use a public factory class to provide a simple mechanism that consumer modules can use for getting instances of the service providers.

No pacotecom.example.servicemodule.external, vamos definir a seguinte classeTextServiceFactory:

public class TextServiceFactory {

    private TextServiceFactory() {}

    public static TextService getTextService(String name) {
        return name.equalsIgnoreCase("lowercase") ? new LowercaseTextService(): new UppercaseTextService();
    }

}

Certamente, poderíamos ter tornado a classe da fábrica um pouco mais complexa. Para manter as coisas simples, porém, os provedores de serviço são simplesmente criados com base no valorString passado para o métodogetTextService().

Agora, vamos substituir nosso arquivomodule-info.java para exportar apenas nossoexternal package:

module com.example.servicemodule {
    exports com.example.servicemodule.external;
}

Notice that we’re only exporting the service interface and the factory class. As implementações são privadas, portanto, não são visíveis para outros módulos.

5.4. A classe de aplicativo

Agora, vamos refatorar a classeApplication, para que ele possa usar a classe de fábrica do provedor de serviços:

public static void main(String args[]) {
    TextService textService = TextServiceFactory.getTextService("lowercase");
    System.out.println(textService.processText("Hello from example!"));
}

Como esperado, se executarmos o aplicativo, veremos o mesmo texto impresso no console:

hello from example!

Ao tornar a interface de serviço pública e os provedores de serviço privados, efetivamente nos permitiu separar o serviço e os módulos do consumidor por meio de uma classe de fábrica simples.

Nenhum padrão é uma bala de prata, é claro. Como sempre, devemos primeiro analisar nosso caso de uso para ajustar.

6. Módulos de serviço e consumidor

O JPMS fornece suporte para módulos de serviço e consumidor prontos para uso, por meio das diretivasprovides…witheuses.

Portanto, podemos usar esta funcionalidade para desacoplar módulos,without having to create additional factory classes.

Para unir os módulos de serviço e consumidor, precisamos fazer o seguinte:

  1. Coloque a interface de serviço em um módulo, que exporta a interface

  2. Coloque os provedores de serviços em outro módulo - os provedores são exportados

  3. Especifique no descritor do módulo do provedor que queremos fornecer uma implementaçãoTextService com a diretivaprovides…with

  4. Coloque a classeApplication em seu próprio módulo - o módulo consumidor

  5. Especifique no descritor do módulo do consumidor que o módulo é um módulo do consumidor com a diretivauses

  6. UseService Loader API no módulo de consumidor para pesquisar os provedores de serviço

Essa abordagem é muito poderosa, pois aproveita toda a funcionalidade que os módulos de serviço e consumidor trazem para a mesa. Mas também é um pouco complicado.

Por um lado, fazemos com que os módulos de consumidor dependam apenas da interface de serviço, não dos provedores de serviço. Por outro lado,we can even not define service providers at all, and the application will still compile.

6.1. O Módulo Pai

Para implementar esse padrão, precisaremos refatorar o POM pai e os módulos existentes também.

Como a interface de serviço, os provedores de serviço e o consumidor agora viverão em módulos diferentes, primeiro precisamos modificar a seção<modules> do POM pai, para refletir esta nova estrutura:


    servicemodule
    providermodule
    consumermodule

6.2. O módulo de serviço

Nossa interfaceTextService voltará paracom.example.servicemodule.

E vamos mudar o descritor do módulo de acordo:

module com.example.servicemodule {
    exports com.example.servicemodule;
}

6.3. O Módulo Provedor

Conforme declarado, o módulo de provedor é para nossas implementações, então vamos colocarLowerCaseTextServicee UppercaseTextService aqui. Vamos colocá-los em um pacote que chamaremos decom.example.providermodule.

Finalmente, vamos adicionar um arquivomodule-info.java:

module com.example.providermodule {
    requires com.example.servicemodule;
    provides com.example.servicemodule.TextService with com.example.providermodule.LowercaseTextService;
}

6.4. O Módulo do Consumidor

Agora, vamos refatorar o módulo de consumidor. Primeiro, colocaremosApplication de volta no pacotecom.example.consumermodule.

Em seguida, vamos refatorar o métodomain() da classeApplication, para que ele possa usar a classeServiceLoader para descobrir a implementação apropriada:

public static void main(String[] args) {
    ServiceLoader services = ServiceLoader.load(TextService.class);
    for (final TextService service: services) {
        System.out.println("The service " + service.getClass().getSimpleName() +
            " says: " + service.parseText("Hello from example!"));
    }
}

Finalmente, iremos refatorar o arquivomodule-info.java:

module com.example.consumermodule {
    requires com.example.servicemodule;
    uses com.example.servicemodule.TextService;
}

Agora, vamos executar o aplicativo. Como esperado, devemos ver o seguinte texto impresso no console:

The service LowercaseTextService says: hello from example!

Como podemos ver, implementar esse padrão é um pouco mais complexo do que aquele que usa uma classe de fábrica. Mesmo assim, o esforço adicional é altamente recompensado com um design mais flexível e pouco acoplado.

The consumer modules depend on abstractions, and it’s also easy to drop in different service providers at runtime.

7. Conclusão

Neste tutorial, aprendemos como implementar dois padrões para dissociar módulos Java.

Ambas as abordagens fazem com que os módulos do consumidor dependam de abstrações, o que é sempre um recurso desejado no design de componentes de software.

Claro, cada um tem seus prós e contras. Com o primeiro, obtemos uma boa dissociação, mas precisamos criar uma classe de fábrica adicional.

Com o segundo, para obter os módulos desacoplados, temos que criar um módulo de abstração adicional e adicionar um novolevel of indirection com a API Service Loader.

Como sempre, todos os exemplos mostrados neste tutorial estão disponíveis no GitHub. Certifique-se de verificar o código de amostra para os padrõesService FactoryeProvider Module.