Design, Design Patterns

Sua API fala? – Interfaces fluentes

Pra começo de conversa eu chego e digo logo que não gosto da forma como Java trata datas e calendários. Há alguns anos atrás eu lia alguns programadores ‘rock-stars’ dizerem que era uma API mal feita e tudo mais. Mas eu não entendia o porquê. Bom, há algum tempo eu tenho entendido a razão disso. Tirando o fato da classe Date e da classe Calendar serem mutáveis (inclusive essa questão já é clichê, falo disso em outro post – prometo), as operações não são nada elegantes. Trabalhosas, verbosas, exigem um monte de classes pra formatar tipo DateFormat, NumberFormat (no caso de números), apresenta problemas de performance em certos casos, difícil de estender e etc.  Todo mundo que já trabalhou um pouco com Java sabe como essa API funciona. Talvez não tenha noção do como ela é chata de trabalhar, mas sabe como funciona. Veja isso por exemplo:


//Isso tudo é pra setar uma data (12 de fevereiro de 1987)

 Calendar calendar = Calendar.getInstance();
 calendar.setTimeZone(TimeZone.getTimeZone("Universal");
 calendar.set(Calendar.DATE, 12);
 calendar.set(Calendar.MONTH, 2 - 1);
 calendar.set(Calendar.YEAR, 1987);
 Date feb12_1987 = calendar.getTime();

Um monte de linha de código pouquíssimo expressivas. Mais feio que parto de ouriço. E olhe que essa data não está sendo formatada. Se alguém quiser escrever isso no cabeçalho duma página, por exemplo? Mais trabalho ainda pra formatar em String e… haja paciência. Não estou querendo dizer que você jamais deva usar a API Dates e Calendars na vida. Mas, veja como essa API soa melhor:


//Setando a mesma data (12 de fevereiro de 1987) usando Joda-Time API

DateTime feb12_1987 = new DateTime().withDayOfMonth(12).withMonthOfYear(2).withYear(1987);

Então? Bem melhor, não? Não mudei o objeto em questão (cada método chamado retorna um novo objeto DateTime modificado), não usei variáveis static, não precisei criar um calendário pra fazer operações no meu tipo principal (no caso data) e o melhor de tudo: tudo em uma linha e facílimo de ler. Muito elegante. Esse é um belo exemplo onde a API Joda-Time usa method chaining, que é a técnica de encadear os métodos chamando-os em seguida, mas, vai além, faz o serviço de forma fluente! Repare como essa chamada parece mesmo uma frase: “nova data com dia do mês:12, com mês do ano:2 e com ano 1987.”  Alguém pode dizer que isto é, na verdade, o padrão Builder. Bom, também. Mas não apenas. Builder se preocupa apenas em construir objetos, não com legibilidade e fluência. Builder por padrão é usado com métodos set() e isso por si só já tira a fluência da chamada. Ah sim, além disso podemos fazer qualquer operação no objeto com interfaces fluentes (formatar saída, fazer conversões etc.), enquanto com Builder a idéia é apenas construir o objeto. Enfim, a API do Joda fornece uma porção de interfaces fluentes que me deixam feliz e satisfeito =D . Mas, o que exatamente é a tal da interface fluente?

Interface fluente é um estilo de escrever código no qual a função ou as funções que você deseja desempenhar são feitas de uma maneira bem mais simples do que a maneira usual e simplesmente “fluem”. “Interfaces” no sentido mais amplo da palavra , ou seja, os métodos expostos de uma dada classe. E não necessariamente interfaces Java.  Também podem ser chamadas de Interfaces Humanas, visto que o código cliente é que vai ser muito mais fácil e legível de escrever e compreender. Já pra quem escreve a API é mais difícil. Sim, mais difícil porque exige muito mais tempo pensando (às vezes muito tempo mesmo) em como prover um sistema de encadeamento de métodos que qualquer pessoa possa usar facilmente, com uma nomenclatura super clara que realmente indique qual o passo a seguir e ainda com todos os atributos de qualidade que uma boa API deve ter – alta coesão e baixo acoplamento.  Nem adianta eu ficar tentando explicar com conceitos esse estilo de API. Um exemplo vale mais do que mil palavras. E com certeza você vai entender melhor o que são de fato com exemplos. Suponha uma classe Money que representa uma quantia de dinheiro em qualquer moeda. Eu quero dar suporte aos clientes dessa interface para fazer soma de valores, subtrações de valores, conversão de moedas (tanto em símbolo que representa a moeda como em taxa de conversão) e formatação pra String. Então criei uma classe Money (me apeguei mais a fluência dela e deixei algumas potenciais falhas , por exemplo  tratamento de erros e outros, não reparem):

 


public class Money {

  public static final String REAIS = "R$";

  public static final String USDOLLARS = "U$";

  public static final String EUROS = "€";

  public static final String COMMA = ",";

  public static final String POINT = ".";

  private double value;

  private boolean usingComma;

  private static NumberFormat formatter;

  static {

    formatter = NumberFormat.getNumberInstance();

    formatter.setMinimumFractionDigits(2);

  }

  //Valor padrão

  private String currency = USDOLLARS;

  private Money(double value) {

    this.value = value;

    this.usingComma = false;

  }

  public static Money get(double value) {

    return new Money(value);

  }

  public static Money get(String value)  {

    double numValue = Double.parseDouble(value);

    return new Money(numValue);

  }

  public double value() {

    return this.value;

  }

  public long inCents() {

    return (long)this.value*100;

  }

  public String asString() {

    String formattedValue = formatValue();

    return formattedValue;

  }

  public Money in(String currentCurrency) {

    this.currency = currentCurrency;

    return this;

  }

  public Money usingComma() {

    this.usingComma = true;

    return this;

  }

  public Money convertTo(String currency) {

    this.value *= MoneyConversion.calculate(this.currency, currency);

    this.currency = currency;

    return this;

  }

  public Money plus(double amount) {

    this.value += amount;

    return this;

  }

  public Money minus(double amount) {

    this.value -= amount;

    return this;

  }

  private String formatValue() {

    String formattedValue = this.currency + formatter.format(this.value);

    if(this.usingComma) {

      if(formattedValue.contains(POINT)) {

        formattedValue = formattedValue.replace(POINT, COMMA);

      }

    }

    else {

      if(formattedValue.contains(COMMA)) {

        formattedValue = formattedValue.replace(COMMA, POINT);

      }

  }

  return formattedValue;

}

public static void main(String[] args) {

    String result1 = Money.get(10).in(REAIS).convertTo(USDOLLARS).usingComma().asString();

    System.out.println(result1);

    long result2 = Money.get(1).plus(15).plus(60).minus(50.5).inCents();

    System.out.println(result2 + " cents");

    String result3 = Money.get(0).plus(100).in(EUROS).asString();

    System.out.println(result3);

  }

}

 

“Ah!” – alguém pode dizer – “mas não era melhor fazer métodos do tipo…  new Money(10).convertFromReaisToDollarsUsingCommaAndFormatAsString ?” Veja bem, nesse caso o método viola o Princípio da Responsabilidade Única fazendo 3 coisas. Além disso, eu continuo a ter uma grande quantidade de métodos como seria no caso sem interface fluente. Imagina só fazer um método para cada combinação diferente de operações! Flexibilidade zero.

É meio estranho o fato de você retornar o próprio objeto num método modificador. Mas lembre-se que esse é um padrão de escrita diferente. Diferente de  JavaBeans, onde o objeto teria apenas seus atributos e cada um com seu get e set correspondente. Para obtermos a fluência precisamos invocar os métodos desejados sempre retornando algo que possa ser usado na próxima invocação. No caso, eu escolhi por retornar novas instâncias de Money(de forma que eles são imutáveis) exceto quando o cliente desejar pegar o resultado final das operações, quando o retorno é uma String com o valor formatado (asString()) ou a quantidade de centavos (caso queira formatar da sua própria forma). Mas como eu disse, Money é uma classe boba no sentido dos pequenos detalhes. A ênfase está nos nomes e eu não me canso de estressar esse ponto: Pare bastante pra pensar nos nomes dos métodos da sua interface fluente! Sem dúvida é bem mais trabalhoso fazer algo assim com qualidade, mas os resultados são fascinantes. Ah sim! Note que algumas linguagens tornam essa tarefa mais fácil, por exemplo Ruby e Python, tão falados hoje em dia. Em Java e C# já não é tão fácil ou os resultados não são ótimos. C++ e  C são verbosas demais, cheias de lixo sintático e tal. No mais, linguagens dinâmicas (também por serem ricas em syntax sugar) são mais produtivas pra isso também.

Mas cuidado! Não saia por aí fazendo interfaces fluentes em todo código que pegar. É uma técnica que deve ser usada com moderação e razoabilidade. Uma classe com 2 métodos e sem nenhuma chance de crescer não tem motivo de ser fluente – já é simples por natureza. Se não houver um bom range de combinações possíveis de operações também não compensa – muito trabalho pra projetar algo que não vai ser usado. E cuidado pra não ficar com mania de fluência e querer aplicar princípios de fluent interfaces em classes normais, fazendo sets retornar objetos e tal. Isso é uma violação do princípio CommandQuerySeparation. Simplesmente não funciona, ok?

Note que TDD (Test-Driven Development) cai como uma luva se você está querendo implementar Fluent Interfaces.  Tipo, primeiro você escreve a forma que o cliente deve chamar as operações (seu código não vai compilar) e depois escreva apenas código necessário para fazer seu teste compilar.  Depois refatore sua implementação para fazer o teste passar. Existe uma pá de bons artigos sobre interfaces fluentes. Eu gosto desse e desse principalmente que é baseado no post clássico do Martin Fowler. Outros exemplos de APIs Java escritas neste estilo e amplamente difundidas são JMock (para testes) e Criteria do Hibernate. Enfim, a idéia motivadora por trás disso tudo é sempre a mesma – programe não apenas para a máquina entender, mas [principalmente] para as pessoas entenderem.

Padrão

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s