Depois de anos desenvolvendo software, muitos programadores acabam percebendo que existem certas heurísticas que, se aplicadas, resultam em código mais conciso e sólido. O intuito desse artigo é apenas catalogar algumas dessas boas práticas, que muitos programadores experientes sabem, até inconscientemente, e que poderão ajudar aqueles que estão ingressando na área.

Aplicando renormalizações no código

Em Física, renormalização é uma técnica para simplificar cálculos, eliminando a presença de infinitos nas equações. É uma forma de se tentar domar a complexidade do domínio do problema, reduzindo-o a um escopo menos amplo.

Essa técnica também pode ser aplicada na nossa programação diária, tornando assim nosso código mais seguro.

Enumeration

Usar constantes int, String ou qualquer outro tipo primitivo para representar um conjunto finito e pequeno de estados ou valores, ao invés de usar Enumeration, é um erro que muitos programadores cometem. Vejamos esse exemplo na Listagem 1.


  public static final String APROVADO = "Aprovado";
  public static final String REPROVADO = "Reprovado";
  public static final String PENDENTE = "Pendente";
  public void facaAlgo(String status) {
    // ...
  }
Listagem 1. Código em Enum

Vamos supor que o método facaAlgo só deveria aceitar um dos três estados: APROVADO, REPROVADO e PENDENTE. Para qualquer outra entrada ele lançaria uma exceção. Qual o problema com esse código?

Como o argumento do método é do tipo String, qualquer String poderia ser passada, mesmo não sendo parte do conjunto de entradas que o desenvolvedor do método gostaria que fosse. Não há uma restrição formal que garanta o uso correto do método, então temos múltiplas possibilidades de entrada. Para eliminar esse "infinito", basta aplicarmos uma "renormalização", no caso, o uso de Enumeration ao invés de constantes de String, como mostra a Listagem 2.


enum Status {
   APROVADO,
   REPROVADO,
   PENDENTE
}
   
public void facaAlgo(Status status)  { //... }
Listagem 2. Código com Enum

Apesar de ser um exemplo simples, ele tem implicações importantes:

  1. Restringimos a entrada do método a apenas três estados possíveis, ao invés de "infinito";
  2. O compilador irá trabalhar a nosso favor, pois fazer verificação de tipo em tempo de compilação é uma das melhores técnicas para prevenir que programadores utilizem erroneamente um trecho de código. Devemos, sempre que possível, utilizar os recursos da linguagem para fazer o compilador garantir as premissas do código.

Tiny Types e Programação Orientada a Strings

Veremos na Listagem 3 um caso mais interessante de aplicação da renormalização e verificação segura pelo compilador.


  public class Pessoa {
      
      private String nome;
      private String sobrenome;
      private String rg;
      
      public Pessoa(String nome) {
          this.nome = nome;
      }
      
      public Pessoa(String nome, String sobrenome) {
          this.nome = nome;
          this.sobrenome = sobrenome;
      }    
      
      public Pessoa(String x, String y, String z) {
          this.nome = x;
          this.sobrenome = y;
          this.rg = z;
      }       
  }
Listagem 3. Classe Pessoa

Esse é o típico modelo de dados orientado a Strings. Além do problema da infinidade, o compilador não estará disposto a te salvar de coisas como:


new Pessoa("De Niro"); // Sobrenome ao invés do nome
new Pessoa("99999999-99") // RG ao invés do nome
new Pessoa("Fowler", "Martin"); // Inversão

Qualquer programador desavisado poderia cometer o erro de inverter valores, passar um valor semanticamente errado, etc. Mas, como fazer o compilador nos prevenir desses erros


  public class Nome {
     private String nome;
     public Nome(String texto) { this.nome = texto; }
  }
      
  public class Sobrenome {
      private String sobrenome;
      public Sobrenome(String texto) { this.sobrenome = texto; }
  }
      
  public class RG {
      private String rg;
      public RG(String texto) { this.rg = texto; }
  }
   
  public class Pessoa {    
     
      private Nome nome;
      private Sobrenome sobrenome;
      private RG rg;
      
      public Pessoa(Nome x) {
          this.nome = x;
      }
      
      public Pessoa(Nome x, Sobrenome y) {
          this.nome = x;
          this.sobrenome = y;
      }    
      
      public Pessoa(Nome x, Sobrenome y, RG z) {
          this.nome = x;
          this.sobrenome = y;
          this.rg = z;
      }       
  }
Listagem 4. Tiny Types

Utilizando tipos mais robustos (ao invés de Strings) no seu modelo de dados, como mostra a Listagem 4, os erros cometidos no exemplo da construção de objetos Pessoa seriam avisados em tempo de compilação, fornecendo uma segurança maior no uso dessa classe.

Sim, mais código terá que ser escrito para suportar esse modelo de programação (aumenta a verbosidade do código), além do trabalho para fazer as transformações para outros tipos de dado na camada de persistência ou no uso de determinados frameworks. Porém, o ganho em termos de segurança de tipos em tempo de compilação e consequentemente menos chances de erro por parte do desenvolvedor em Produção são aspectos a serem considerados. Outro benefício é que o código se torna mais descritivo e intuitivo.

Além do mais, poderíamos tornar essas classes mais ricas. Por exemplo, se houvesse uma classe CPF, poderíamos incluir nela um método de validação de CPF. A classe Nome poderia validar entradas que incluam números, remover espaçamentos no começo e fim, impor um tamanho máximo, etc.

Padrão Builder

O padrão Builder é uma ótima ferramenta quando queremos que determinados objetos tenham seus atributos mais importantes inicializados, evitando assim os famosos erros de NullPointerException ou erros de SQL quando o campo na tabela que corresponde ao campo da classe é do tipo required (embora essa validação possa ser feita na camada de persistência).

Veja um exemplo na Listagem 5.

Listagem 5. Campos obrigatórios e opcionais.


public class Pessoa {    
    private String nome;  // Nao deve ser null | campo obrigatório no banco de dados
    private String sobrenome;  // Nao deve ser null | campo obrigatório no banco de dados
    private String rg;   // Opcional
    private String cpf;   // Opcional
    ...
}

Mas como fazer o compilador nos ajudar a cumprir essas premissas, sem depender da boa vontade do programador? Vejamos a Listagem 6.


  // Classe Pessoa se torna imutável
  public final class Pessoa {    
   
      private final String nome;  
      private final String sobrenome;
      private final String rg;   
      private final String cpf;   
       
      public static class Builder {
          // Required parameters
          private final String nome;    
          private final String sobrenome;
   
          // Optional parameters - initialized to default values
          private String rg  = ""; // NULL safe
          private String cpf  = ""; // NULL safe
   
          public Builder(String nome, String sobrenome) {
              this.nome = nome;
              this.sobrenome = sobrenome;
          }
   
          public Builder rg(String val)
              { rg = val;   return this; }
          public Builder cpf(String val)
              { cpf = val;  return this; }
          
          public Pessoa build() {
              return new Pessoa(this);
          }
      }
   
      private Pessoa(Builder builder) {
          nome       = builder.nome;
          sobrenome  = builder.sobrenome;
          rg         = builder.rg;
          cpf        = builder.cpf;
      }
      
      public static void main(String... args) {
          //Pessoa pessoa = new Pessoa(); // Nao compila        
          
          // Somos obrigados pelo compilador a passar no mínimo
           o nome e sobrenome...
          Pessoa pessoa1 = new Pessoa.Builder("Roberto", "Carlos")
          .build();
   
          // + cpf
          Pessoa pessoa2 = new Pessoa.Builder("Roberto", "Carlos")
          .cpf("12345").build();
   
          // + cpf e rg
          Pessoa pessoa3 = new Pessoa.Builder("Roberto", "Carlos")
          .cpf("12345").rg("00000").build();        
      }
  }
Listagem 6. Uso do padrão Builder para garantir as premissas da classe Pessoa

O padrão Builder, além de ajudar na inicialização/criação de classes com muitos atributos, pode ser usado para impor restrições de uso para os programadores, garantindo o uso correto de determinadas classes na aplicação.

Percebam que, independente da técnica abordada (Enumeration, Tiny Types ou Builder), o objetivo real é impor restrições que serão verificadas pelo compilador, evitando que os programadores quebrem premissas, que de outra forma, seriam vagas e facilmente esquecidas.

Imutabilidade

Como falar de restrições sem mencionar o aspecto da imutabilidade? Mesmo no exemplo anterior, a presença da imutabilidade é fator fundamental para, junto com o padrão Builder, garantir as premissas da classe Pessoa.

Esse tópico não será abordado em profundidade, mas no portal temos um artigo sobre.

Optional – Novo Recurso do Java 8

Optional é uma nova classe introduzida no Java 8 e é baseada no padrão Option/Some/None da linguagem Scala. Nesse artigo não será abordado o uso de Optional com Lambdas e Streams do Java 8. Como estamos falando sobre renormalização e segurança em tempo de compilação, a classe Optional será abordada sobre esse prisma.

Quem programa em Java já se deparou alguma vez com a famosa exceção NullPointerException. O próprio criador do conceito de referência nula, Tony Hoare, declarou que esse foi o "erro de um bilhão de dólares".

Vejamos o seguinte trecho de código:


public String getNomeDoCarro()  { //.... }

Vendo essa declaração de método, podemos inferir que o retorno é sempre uma String, certo? Pense novamente...

Mesmo nesse método simples, dois tipos de retorno são possíveis: String ou null. Sim, sempre nos esquecemos que qualquer método em Java (que não retorna um tipo primitivo), ou devolve um objeto ou null.

E devido a essa simples asserção, jamais poderemos garantir que qualquer chamada a método é null-safe sem analisar minuciosamente a sua implementação ou acreditar na sua documentação (no caso da API de terceiros).


// Sem ver a implementação, não dá para afirmar se list é null-safe
List<Pessoa> pessoas = repository.list();  
Endereco endereco = pessoas.get(0).getEndereco();
// Nao da para garantir que endereco seja != null sem analisar todo o contexto
System.out.println(endereco.getName());
....     
Listagem 7. Possíveis retornos de null

Sim, temos que analisar o código cuidadosamente e ver se determinado método retorna null ou não, como mostra a Listagem 7. Se não temos certeza se o método pode realmente retornar null, teremos que usar sempre if(obj != null) para validar qualquer retorno.

Se um método sempre pode retornar dois tipos de dados (null ou um objeto válido), porque não formalizar esse conceito? Para isso podemos usar Optional. Independente do papel de Optional para reduzir a verbosidade e prover um estilo mais declarativo, funcional e limpo com flatMap/map/filter, a simples existência desse conceito já promove sérios benefícios.


  import java.util.Optional;
  import java.util.Random;
   
  public class Exemplo {
      
      public static String getString1()  { return "OK"; }
      
      public static Optional<String> getString2()  { 
          
          String retorno;
          
          if(new Random().nextInt(2) == 0) {
              retorno = null;
          } else {
              retorno = "OK";
          }
          
          return Optional.ofNullable(retorno);
      }
      
      public static void main(String... args) {
          for(int i = 0; i < 10; i++) {
              Optional<String> option = getString2();            
              if(option.isPresent()) {
                  System.out.println(option.get());
              }
          }
      }
  }
Listagem 8. Uso de Optional

O que podemos afirmar sobre os dois métodos getString da Listagem 8?

  1. Podemos afirmar, com certeza, que o primeiro sempre irá retornar uma String;
  2. Já o segundo pode ser uma String ou null, por isso o retorno é um Optional.

Com a presença de Optional agora podemos definir claramente quando um método é null-safe (getString1()) e quando ele pode retornar null(getString2()). Percebam o quando o aspecto idiomático é importante e poderoso. Se um método pode retornar null, devolveremos um Optional ao invés de T. Se sabemos que o método nunca retornará null, poderemos apenas devolver T.

Essa convenção favorece o programador, pois o mesmo não precisará analisar o código do método para ver se ele é null-safe ou não, bastando apenas ver o tipo de retorno, ou argumentos de entrada do método (e nisso uma IDE é de grande ajuda).

No caso de getString2(), como o usuário do método sabe que o mesmo retorna Optional, ele provavelmente não irá esquecer de fazer o teste com o método isPresent(), para ver se o valor retornado é um objeto T ou null. Se for um objeto válido, ou seja, isPresent() == true, podemos usar o método get para obter o objeto. Usar get sem testar com isPresent() é perigoso, pois se Optional contém null, teremos uma exceção.

Uma alternativa a get seria usar o código da Listagem 9.


public static void main(String... args) {
  for(int i = 0; i < 10; i++) {
      Optional<String> option = getString2();            
      System.out.println(option.orElse("NULL"));
  }
}
Listagem 9. Uso de orElse

Se Optional (um contâiner) contém um objeto válido (no caso uma String), o mesmo será devolvido e impresso. Se contém null, a string "NULL" será devolvida e impressa.

A classe Optional favorece um estilo de programação mais defensivo, pois avisa explicitamente o programador de um possível retorno nulo de um método (ou quando um argumento passado para um método pode ser nulo ou não), ajudando a minimizar a presença do NullPointerException. Obviamente, como o Optional foi introduzido no Java 8, a grande maioria das classes da API do Java não faz uso desse conceito, o que limita um pouco o seu poder (diferente do Scala, que tem amplo suporte ao Option em sua biblioteca). Veja seu código completo na Listagem 10.


  import java.util.function.Consumer;
  import java.util.function.Function;
  import java.util.function.Predicate;
  import java.util.function.Supplier;
   
  public final class Optional<T> {
   
      private static final Optional<?> EMPTY = new Optional<>();
   
      private final T value;
   
      private Optional() {
          this.value = null;
      }
   
      public static<T> Optional<T> empty() {
          @SuppressWarnings("unchecked")
          Optional<T> t = (Optional<T>) EMPTY;
          return t;
      }
   
      private Optional(T value) {
          this.value = Objects.requireNonNull(value);
      }
   
      public static <T> Optional<T> of(T value) {
          return new Optional<>(value);
      }
   
      public static <T> Optional<T> ofNullable(T value) {
          return value == null ? empty() : of(value);
      }
   
      public T get() {
          if (value == null) {
              throw new NoSuchElementException("No value present");
          }
          return value;
      }
   
      public boolean isPresent() {
          return value != null;
      }
   
      public void ifPresent(Consumer<? super T> consumer) {
          if (value != null)
              consumer.accept(value);
      }
   
      public Optional<T> filter(Predicate<? super T> predicate) {
          Objects.requireNonNull(predicate);
          if (!isPresent())
              return this;
          else
              return predicate.test(value) ? this : empty();
      }
   
      public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
          Objects.requireNonNull(mapper);
          if (!isPresent())
              return empty();
          else {
              return Optional.ofNullable(mapper.apply(value));
          }
      }
   
      public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
          Objects.requireNonNull(mapper);
          if (!isPresent())
              return empty();
          else {
              return Objects.requireNonNull(mapper.apply(value));
          }
      }
   
      public T orElse(T other) {
          return value != null ? value : other;
      }
   
      public T orElseGet(Supplier<? extends T> other) {
          return value != null ? value : other.get();
      }
   
      public <X extends Throwable> T 
      orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
          if (value != null) {
              return value;
          } else {
              throw exceptionSupplier.get();
          }
      }
   
      @Override
      public boolean equals(Object obj) {
          if (this == obj) {
              return true;
          }
   
          if (!(obj instanceof Optional)) {
              return false;
          }
   
          Optional<?> other = (Optional<?>) obj;
          return Objects.equals(value, other.value);
      }
   
      @Override
      public int hashCode() {
          return Objects.hashCode(value);
      }
   
      @Override
      public String toString() {
          return value != null
              ? String.format("Optional[%s]", value)
              : "Optional.empty";
      }
  }
Listagem 10. Código fonte da classe Optional

Criteria API

Tanto no JPA 2.0 como no Hibernate, podemos escrever consultas através de Strings (HQL/JPQL) ou usando a API de Criteria. Novamente caímos no impasse: programação orientada a strings versus fortemente tipada. A maior vantagem do uso da Criteria API é a verificação de erros em tempo de compilação (se estamos passando a classe correta, sem erros de grafia, etc). E também estaremos completamente seguros em relação ao SQL injection.

A maior desvantagem é a complexidade adicional em se aprender a Criteria API, pelo menos no começo, já que HQL/JPQL são mais intuitivos para os programadores (uma vez que provavelmente já lidam com a linguagem SQL para banco de dados relacionais). Outra desvantagem é a verbosidade, que aumenta consideravelmente com o uso de Criteria API ao invés de HQL/JPQL.

Com certeza, existem outras técnicas não mencionadas nesse artigo que "renormalizam" e garantem segurança em tempo de compilação, ou um estilo idiomático que indica com mais clareza as intenções do programador.

Ao "renormalizar" determinado trecho de código, impomos um número menor de decisões que um programador deve tomar, diminuindo assim a possibilidade de erros.

Os frameworks quase sempre cumprem esse objetivo, pois impõe uma série de restrições e passos para o programador seguir, uniformizando o conhecimento coletivo ao criar uma linguagem comum e aumentando a produtividade como um todo. Seria um caos se cada programador viesse com sua própria visão de como implementar persistência, MVC, etc.

Os padrões de projeto também favorecem o uso de restrições. Bons exemplos são o Template Method, Strategy, State, Command e Factories.

E claro, não podemos nos esquecer dos Generics. Apesar de ser um recurso considerado complexo, principalmente pelos iniciantes, dominar seu uso é questão imperativa para tornar o compilador "seu grande amigo".