[PT-BR] Evitando ‘Escaping References’ com JPMS

Posted on

Muitas vezes, quando estamos desenvolvendo nossos softwares, não percebemos possíveis bugs que estão sendo inseridos no código. A questão é que, em alguns cenários, o comportamento da nossa aplicação pode ser altamente impactado. A ideia deste post é apresentar um desses problemas, chamado escaping references, e possíveis soluções.



Show me the code!!

Antes de entrarmos na parte conceitual, vamos conhecer o código com que iremos trabalhar. O projeto possui

  • 1 classe chamada Livro, que é a representação de um livro
package br.com.er.model;
public class Livro {

    public Livro(Long id, String nome, String autor, Double preco) {
        this.id = id;
        this.nome = nome;
        this.autor = autor;
        this.preco = preco;
    }

    private final Long id;
    private final String nome;
    private final String autor;
    private Double preco;

    public String getAutor() { return autor; }

    public void setPreco(Double preco) { this.preco = preco; }

    @Override
    public String toString() {
        return "Livro{" +
                "id=" + id +
                ", nome='" + nome + ''' +
                ", autor='" + autor + ''' +
                ", preco=" + preco +
                '}';
    }
}
Enter fullscreen mode

Exit fullscreen mode

  • 1 classe chamada ColecaoDeLivros responsável por fornecer uma lista de livros e alguns comportamentos para manuseá-la.
package br.com.er.collection;
//imports omitidos
public class ColecaoDeLivros {

    public List<Livro> livros;

    public ColecaoDeLivros() {
        livros = List.of(
                new Livro(1L, "Clean Code", "Uncle Bob", 119.99)
        );
    }

    public void mostrarLivros() {
        livros.forEach(System.out::println);
    }

    public Livro buscarPorAutor(String autor) {
        return livros.stream().
                filter(livro -> livro.getAutor().contains(autor))
                .findAny()
                .orElseThrow(RuntimeException::new);
    }
}
Enter fullscreen mode

Exit fullscreen mode

  • 1 classe Principal responsável por executar o código.
package br.com.er;
import br.com.er.collection.ColecaoDeLivros;
public class Principal {

    public static void main(String... x) {

        ColecaoDeLivros cl = new ColecaoDeLivros();

        cl.mostrarLivros();
        System.out.println(cl.buscarPorAutor("Uncle Bob"));

    }
}
Enter fullscreen mode

Exit fullscreen mode



Executando o projeto

O código acima está 100% funcional e podemos garantir isso executando a nossa classe Principal.

Intellij IDE usada para executar o código

/Library/Java/JavaVirtualMachines/jdk-11.0.9.jdk/Contents/Home/bin/java -javaagent:/Applications/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=50174:/Applications/IntelliJ IDEA CE.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/jv.martins/Documents/workspace/avoid-escaping-reference/out/production/avoid-escaping-reference br.com.er.Principal
Livro{id=1, nome='Clean Code', autor='Uncle Bob', preco=119.99}
Livro{id=1, nome='Clean Code', autor='Uncle Bob', preco=119.99}

Process finished with exit code 0
Enter fullscreen mode

Exit fullscreen mode

Vamos fazer uma pequena modificação na classe Principal.

//resto do código omitido
public static void main(String... x) {

        ColecaoDeLivros cl = new ColecaoDeLivros();

        cl.mostrarLivros();
        cl.buscarPorAutor("Uncle Bob").setPreco(0.0);
        cl.mostrarLivros();
    }
Enter fullscreen mode

Exit fullscreen mode

Como podemos observar, o método setPreco da classe Livro é chamado após o método buscarPorAutor da classe ColecaoDeLivros. Isso está ocorrendo porque o retorno de buscarPorAutor é um Livro. Pelas boas práticas de desenvolvimento, existem duas opções:

  1. Tornarmos impossível chamar o método setPreco.

  2. Se for possível chamar o método setPreco, a chamada não deve surtir efeito, ou seja, o preço do livro retornado não deve ser alterado.

Executando novamente o projeto o resultado será:

/Library/Java/JavaVirtualMachines/jdk-11.0.9.jdk/Contents/Home/bin/java -javaagent:/Applications/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=50116:/Applications/IntelliJ IDEA CE.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/jv.martins/Documents/workspace/avoid-escaping-reference/out/production/avoid-escaping-reference br.com.er.Principal
Livro{id=1, nome='Clean Code', autor='Uncle Bob', preco=119.99}
Livro{id=1, nome='Clean Code', autor='Uncle Bob', preco=0.0}

Process finished with exit code 0
Enter fullscreen mode

Exit fullscreen mode

Como não deveria ser possível alterar o preço do objeto retornado, observamos que nossa aplicação não está se comportando como deveria. Este é um exemplo de escaping reference. Estamos mudando o atributo de um tipo, através do retorno do método de um outro tipo. Para estar de acordo com as boas práticas, vamos alterar nosso código para que não seja possível chamar o método setPreco.



Usando Interfaces

A primeira opção para evitar o problema citado anteriormente, é criando uma interface de Livro que possua apenas os métodos getters

package br.com.er.interfaces;

public interface ILivro {
    String getAutor();
} 
Enter fullscreen mode

Exit fullscreen mode

Agora trocaremos o retorno do método buscarPorAutor pela interface criada.

//classe ColecaoDeLivros

public ILivro buscarPorAutor(String autor) {
        return livros.stream().
                filter(livro -> livro.getAutor().contains(autor))
                .findAny()
                .orElseThrow(RuntimeException::new);
    }
Enter fullscreen mode

Exit fullscreen mode

Essa ação já será o suficiente para o código não compilar.

Alt Text

Resolvemos a situação e agora não é possível chamar o método setPreco. A afirmativa anterior seria totalmente verdadeira se não houvesse uma maneira de fazer o casting de ILivro para Livro.

public static void main(String... x) {

        ColecaoDeLivros cl = new ColecaoDeLivros();

        cl.mostrarLivros();
        ILivro livro = cl.buscarPorAutor("Uncle Bob");
        Livro livroConvertido = (Livro) livro;
        livroConvertido.setPreco(0.0);
        cl.mostrarLivros();
    }
Enter fullscreen mode

Exit fullscreen mode

Podemos observar que o caminho para chamar o método setLivro ficou mais difícil, mas não impossível. O código acima devolve o mesmo resultado de antes

/Library/Java/JavaVirtualMachines/jdk-11.0.9.jdk/Contents/Home/bin/java -javaagent:/Applications/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=49798:/Applications/IntelliJ IDEA CE.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/jv.martins/Documents/workspace/avoid-escaping-reference/out/production/avoid-escaping-reference br.com.er.Principal
Livro{id=1, nome='Clean Code', autor='Uncle Bob', preco=119.99}
Livro{id=1, nome='Clean Code', autor='Uncle Bob', preco=0.0}

Process finished with exit code 0
Enter fullscreen mode

Exit fullscreen mode

Mas então não temos uma maneira de evitar definitivamente esses escaping references? A resposta vocês verão na próxima seção



Usando JPMS (Java Platform Module System)

O JPMS é uma feature do Java que teve como objetivo modularizar a JDK. Dentre os seus vários benefícios, um deles é fornecer um melhor encapsulamento dos nossos componentes. Nessa seção iremos utilizar esta feature para evitar o problema com escaping references.

O primeiro passo é separar a aplicação em módulos e definir quais classes ficarão em cada módulo. Para o nosso exemplo, vamos definir o primeiro módulo com as classes br.com.er.collection.ColecaoDeLivros, br.com.er.interfaces.ILivro e br.com.er.model.Livro. Segue a estrutura do módulo:

Alt Text

Você deve ter reparado que existe um novo arquivo chamado module-info.java. Este arquivo é necessário para compilar a aplicação e nele é possível fazermos algumas definições. Podemos definir quais componentes queremos que sejam expostos para fora do módulo, fortalecendo o encapsulamento da aplicação e podemos definir quais módulos queremos usar dentro do módulo em que estamos trabalhando. Para entender melhor, vamos verificar o conteúdo do arquivo.

module avoid.escaping.reference {

    exports br.com.er.collection;
    exports br.com.er.interfaces;
}
Enter fullscreen mode

Exit fullscreen mode

A palavra reservada module serve para definição do módulo, que foi nomeado de avoid.escaping.reference. Os exports informam quais pacotes serão expostos para fora do módulo. Qualquer outro pacote fora esses não poderá ser utilizado fora de avoid.escaping.reference

O segundo módulo possuirá a classe Principal e será responsável pela execução do projeto.

Alt Text

O módulo de execução depende do módulo avoid.escaping.reference e esse dependência é definida no arquivo module-info.

module avoid.escaping.reference.engine {

    requires avoid.escaping.reference;
}

Enter fullscreen mode

Exit fullscreen mode

É importante chamar atenção para um ponto. Quando utilizamos o export, nós estamos fazendo a exportação dos pacotes (pacote por pacote) que podem ser utilizados fora do módulo. Quando utilizamos o requires, nós estamos requerendo o módulo como um todo e teremos acesso a todos os recursos que foram exportados do outro módulo.

Um último ponto para aplicação funcionar, é adicionar no build path do módulo avoid.escaping.reference.engine, o módulo avoid.escaping.reference.

Com todas as configurações realizadas, vamos tentar executar o código da classe Principal, com o casting realizado na seção anterior

Alt Text

O código apresenta erro de compilação, porque não exportamos a classe Livro do módulo avoid.escaping.reference, então para o módulo de execução (avoid.escaping.reference.engine) essa classe nem existe. Com essa abordagem, tornamos impossível a chamada do método setPreco e alcançamos o objetivo final.



Concluindo

Percebemos que os problemas citados no post não são sempre fáceis de identificar e o cenário piora porque nem toda solução evita 100% o problema. O uso de interfaces, em alguns cenários, funciona perfeitamente, pois o que fizemos para forçar o casting, não é realidade em certos projetos. Já para outros projetos, o JPMS é a melhor opção, porque garante o encapsulamento de recursos que não devem ser utilizados fora do módulo. A escolha vai depender da necessidade e do projeto. Qualquer dúvida, criticas e sugestões, estou à disposição. Até a próxima!!

Links para o código

Leave a Reply

Your email address will not be published. Required fields are marked *