Muitos sistemas precisam utilizar valores monetários, mas como representá-los de forma precisa e confiável? Neste artigo, vamos investigar esse problema e as motivações que levaram à Money and Currency API do Java (JSR-354), que chamaremos simplesmente de "Money API".
A estratégia mais direta para representar valores monetários é o uso de tipos já presentes no Java. Porém, Joshua Bloch, no seu livro Java Efetivo (Effective Java), assim como outros especialistas, não recomendam usar double e float. O problema já fica claro no seguinte exemplo simples:
double val = 1.03 - 0.42;
System.out.println(val); //0.6100000000000001
O mesmo livro recomenda usar long e int, fazendo a conversão dos valores para centavos. É uma solução é recomendada quando a velocidade e a ocupação de memória são especialmente importantes. No entanto, devemos nos preocupar com o número de casas decimais (não é recomendada a representação de mais que nove casas, nesse caso).
Outro problema de utilizar a representação de valores monetários com int e long é a redução de legibilidade. Imagine que um produto tem o preço de doze reais; como representamos o valor em centavos, precisamos usar o valor como 1.200 centavos:
public class Produto {
private String nome;
private int valor;
...
}
Produto banana = new Produto("banana", 1200);
Produto macarrao = new Produto("macarrao", 400);
int soma = banana.getValor() + macarrao.getValor();
Mas o que aconteceria se esquecermos de converter o valor de reais para centavos (no caso, usar apenas 12 em vez de 1200)? Certamente o resultado seria desastroso. Outro problema que ocorre está no controle de arredondamentos.
Uma alternativa bastante usada é representar os valores com BigDecimal, que evita a necessidade de conversão em centavos e também o controle de arredondamentos.
public class Produto {
private String nome;
private BigDecimal valor;
// getters e setters; construtor
}
Produto banana = new Produto("banana", BigDecimal.valueOf(12D));
Produto macarrao = new Produto("macarrao", BigDecimal.valueOf(4D));
BigDecimal soma = banana.getValor().add(macarrao.getValor());
Porém, ainda temos problemas na nossa classe Produto, pois não foi representada a moeda (reais, dólares, pesos etc.). Ela ficou subentendida. Caso o sistema lide apenas com uma moeda, não seria problema; mas imagine que o produto seja vendido internacionalmente. Apenas o '12' não traria a informação completa e precisaríamos da moeda. Vamos ver algumas formas de representá-la.
Uma das mais simples é utilizar um String, mas, com ela, um pequeno erro de digitação seria suficiente para quebrar o programa. Outra estratégia é utilizar um enum para representar a moeda, restringindo as opções para valores predefinidos.
public class Produto {
private String nome;
private BigDecimal valor;
private Moeda moeda;
...
}
enum Moeda {
REAL, DOLLAR, EURO;
}
No entanto, nosso enum precisará ser mais rico ao lidar com aspectos de internacionalização, entre eles o padrão ISO 4217.
Uma alternativa mais robusta é utilizar uma classe já existente no JDK desde a versão 1.4, a java.util.Currency. Com ela, apenas serão aceitos valores do tipo Currency no setter e ganhamos suporte ao padrão ISO 4217.
public class Produto {
private String nome;
private Currency valor;
private Moeda moeda;
...
}
Agora precisamos garantir que as moedas são as mesmas ao realizar operações envolvendo valores monetários. Afinal, não podemos somar valores em reais com valores em dólares, sem levar em conta as cotações:
Produto celular = ...
Produto notebook = ...
if (celular.getMoeda() == notebook.getMoeda()) {
BigDecimal soma = celular.getValor().add(notebook.getValor());
} //exceção!
É provável que precisemos fazer essa validação em diversos pontos do código, então vamos criar uma classe utilitária:
public class ProdutoUtil {
public static BigDecimal soma(Produto pA, Produto pB) {
if (pA.getMoeda() == pB.getMoeda()) {
return pA.getValor().add(pB.getValor());
}
throw new IllegalArgumentException("Moedas incompatíveis.");
}
}
Pronto, com isso resolvemos todos os nossos problemas, certo? Os básicos sim, mas não todos! Ainda estamos com algumas dificuldades importantes.
Para realizar a soma de produtos é necessário lembrar de realizar a chamada da classe utilitária, mas o que acontece com o que temos de lembrar? Exato, fatalmente esquecemos! Além disso, os valores monetários precisam ser usados não apenas com produtos, mas com diversas outras coisas como serviços, salários etc. Assim será necessário duplicar os campos 'moeda' e 'valor' em diversos pontos.
No mais, quando tivermos diversas classes utilizando valores monetários, há duas estratégias para realizar a validação: uma seria criar classes utilitárias para todo modelo que use dinheiro, ServicoUtil, SalarioUtil etc.; ou criar uma classe utilitária recebendo quatro parâmetros (o valor e a moeda dos dois).
public class DinheiroUtil {
public static BigDecimal soma(
Moeda moedaA, BigDecimal valorA, Moeda moedaB, BigDecimal valorB) {
…
}
public class ServicoUtil {...}
public class SalarioUtil {...}
Há outros problemas:
- O que acontece se for apenas definido um item do dinheiro: o valor ou a moeda? Faz sentido dizer que o produto vale doze? Ou que ele vale 'dólar'? Claro que não; vale doze dólares e isso precisa ser validado;
- Não é de responsabilidade da classe Produto, ou de qualquer outra que precise trabalhar com o valores monetários, cuidar da criação e do controle de estado do dinheiro;
- Ao utilizar classes utilitárias para realizar a validação, estaríamos quebrando o encapsulamento, pois é possível somar dois valores ignorando a validação de moedas e gerando erros.
Uma ideia de solução para o problema veio de um artigo do Martin Fowler, em que ele cita o exemplo de dinheiro como o seu favorito. Ao criar o tipo representando dinheiro, temos as seguintes vantagens
- Centralização do código: todo o comportamento do dinheiro estará na classe dinheiro;
- Remoção da responsabilidade das outras classes: não será necessário ter o controle na hora de criar valores dentro da classes;
- Adeus às classes utilitárias: com a validação feita na classe dinheiro, as classes utilitárias desaparecem, sem falar no clássico problema de esquecer de usá-las.
Esta seria a estrutura básica:
public class Dinheiro {
private Moeda moeda
private BigDecimal valor;
// funcionalidades
}
Produto banana = new Produto("banana", new Dinheiro(12, “dolar”));
Product macarrao = new Produto("macarrao", new Dinheiro(4, “dolar”))
Dinheiro soma = banana.getDinheiro().somar(macarrao.getDinheiro());
Pronto, resolvemos várias das questões expostas acima. Porém, um item ficou de fora: o de não reinventar a roda. Afinal, um bom programador não cria uma coisa que já existe; sempre se apoia em tecnologias testadas e usadas pela comunidade de software.
Seguindo raciocínio similar foi criada e implementada uma especificação voltada ao trabalho com valores monetários, a API citada no início, "Money / Currency API". Com ela, nosso exemplo ficaria:
public class Produto {
private String nome;
private MonetaryAmount valor;
}
CurrencyUnit usd = Monetary.getCurrency("USD");
Produto banana = new Product("banana", Money.of(12, usd));
Produto macarrao = new Product("macarrao", Money.of(4, usd))
Money total = banana.getMoney().add(macarrao.getMoney());
Veja outro exemplo, que mostra mais aspectos básicos da API:
public class OperacoesBasicas {
public static void main(String[] args) {
CurrencyUnit currency = Monetary.getCurrency("BRL");
MonetaryAmount money = Money.of(100, currency);
Number number = 20;
MonetaryAmount divisionResult = money.divide(number);//BRL 5
MonetaryAmount remainderResult = money.remainder(30);//BRL 10
MonetaryAmount multiplyResult = money.multiply(5);//BRL 500
}
}
Próximos passos
Este artigo, se concentrou em contextualizar a criação da Money API e a motivação para usá-la. Há muitas funcionalidades na API que nem tocamos aqui. Veja vários exemplos no site Baeldung e outros detalhes nas apresentações Money API e dinheiro em Java, ambas no QCon SP. Uma referência adicional é o gitbook sobre a API.