Como disse Martin Fowler no livro Patterns of Enterprise Application Architecture:
"Uma boa parte dos computadores neste mundo manipula dinheiro, então sempre me intrigou que Money não seja um tipo de dados em qualquer linguagem de programação convencional."
Patterns of Enterprise Application Architecture sugere a implementação de uma classe Money, com atributos currency e amount, bem como um método allocate que recebe uma lista de proporções e distribui esse dinheiro sem perder valor com arredondamentos. Nesse artigo vou mostrar como implementamos nosso Money pra resolver um problema real de perda de centavos em rateio, bem como resolver o mistério do título.
Já tínhamos uma classe Money, o que facilitou muito na escrita do Money Pattern. Então se eu puder ter a pretensão de dar uma primeira dica é: não use Double ou BigDecimal pra valores financeiros. Construa seu Value Object. Depois de ter valores financeiros espalhados por todo seu sistema, vai ser bem difícil - mas não impossível - refatorar.
Nossa classe Money não tem o atributo currency pois não temos a necessidade de internacionalização, pelo menos não ainda. E não vamos ter implementação de currency enquanto não precisarmos de fato. Assim como também não tínhamos o método allocate. Até que precisamos. Portanto nosso Money era assim:
public class Money implements Serializable {
private final BigDecimal amount;
private Money(BigDecimal amount) {
if (amount == null) {
throw new AmountCantBeEmptyException();
}
this.amount = amount;
}
public static Money of(BigDecimal amount) {
return new Money(amount);
}
public Money plus(Money addition) {
return Money.of(this.amount.add(addition.amount));
}
public Money minus(Money discount) {
return Money.of(this.amount.subtract(discount.amount));
}
public Money times(BigDecimal factor) {
return Money.of(this.amount.multiply(factor));
}
public BigDecimal getAmount() {
return amount;
}
@Override
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof Money)) {
return false;
}
return this.amount.compareTo(((Money) obj).amount) == 0;
}
@Override
public int hashCode() {
return this.amount.hashCode();
}
}
Isso resolvia boa parte dos nossos problemas financeiros. Até que o fatídico dia em que precisamos do allocate chegou. Uma despesa de R$ 1000,20 deveria ser rateada entre várias contas da seguinte maneira:
Como podem perceber, se aplicarmos o percentual de proporção de rateio em cada item, nunca chegaremos em R$ 1000,20 - isso porque centavo é uma medida indivisível, então um valor financeiro representado por Money não pode ser R$ 229,0458 por exemplo. Se arredondarmos pra baixo, "perdemos" 3 centavos. Se arredondarmos pra cima, "ganhamos" 2 centavos. E note que mesmo usando o Round Half Even, perdemos precisão. Esse último é o modo de arredondamento comumente utilizado em sistemas financeiros, inclusive homologado pela ABNT através da norma NBR 5891.
E sabem quem enfrentou um problema parecido? Ele mesmo, George Washington, o primeiro presidente dos Estados Unidos, e uma das soluções propostas foi de Alexander Hamilton, que veio a ser o Primeiro Secretário do Tesouro dos EUA, e seu Largest Remainder Method.
O problema enfrentado por eles à época era resumidamente o seguinte, descrito no U.S. Census Bureau:
Um país com 4 estados, com diferentes populações em cada estado, e 20 assentos no Senado. Quantos senadores cada estado teria para que eles fossem representados proporcionalmente? Um senador, assim como um centavo, também não pode ser dividido em partes. Certo? Só pra confirmar aqui... =D
O método de Hamilton consiste em calcular a proporção da população total de 11882 pela quantidade de 20 assentos disponíveis chegando no divisor de 594,1. Agora pra cada estado divide-se sua população por esse divisor, por exemplo no estado 1 2560/594,1 chegando no quociente de 4,31.
Como já concordamos, não podemos ter 4 senadores e 0,31 de um senador para um estado. Então o número de senadores do estado 1 é 4, que é a parte inteira do resultado. E isso acontece para cada um dos outros resultados. Ao final, por conta dos arredondamentos da parte inteira, o total de senadores alocados nos estados é de 18. Pra quais estados vão os outros 2? É aí que Hamilton entra com uma possível solução. A proposta dele é que distribuamos esses 2 assentos para os estados que ficaram com a maior parte decimal antes do arredondamento.
Explico. Se suprimirmos a parte inteira da coluna Senadores por Estado e ordenarmos pela parte decimal, o resto, em ordem decrescente, ficaríamos com:
- 1,67
- 5,58
- 8,44
- 4,31
Como temos um total de 2 senadores pra redistribuir, os estados que recebem esses senadores são os 2 primeiros da lista. Resultando na coluna Senadores por estado após Método Hamilton.
Existe um paradoxo a se notar que foi descoberto ao aplicar o método Hamilton na população do Alabama, por isso também é chamado de Paradoxo do Alabama, que não será coberto aqui. Mas é onde entra o nosso querido Uncle Bob no artigo.
Entre suas inúmeras contribuições para a comunidade de desenvolvimento de software, Uncle Bob popularizou os princípios SOLID. Era relevante lá na década de 90, é relevante hoje. O princípio que iremos focar agora é o Open-Closed Principle.
Precisamos alterar nossa classe Money para comportar o método allocate pra conseguir distribuir seu valor em propoções sem perder centavos. Então criamos o seguinte método:
public class Money implements Serializable {
private static final BigDecimal ONE_HUNDRED = new BigDecimal(100);
public List<Money> allocate(List<BigDecimal> ratios, RemainderDistribution distribution) {
long amountInCents = toCents();
List<Quota> quotas = new ArrayList<>();
for (BigDecimal ratio: ratios) {
quotas.add(new Quota(amountInCents, ratio));
}
distribution.distribute(quotas, amountInCents);
return quotas.stream().map(Quota::toMoney).collect(Collectors.toList());
}
public long toCents() {
return this.amount.multiply(ONE_HUNDRED).longValue();
}
}
public class Quota {
private static final BigDecimal ONE_HUNDRED = new BigDecimal(100);
private long amount;
private long total;
private BigDecimal ratio;
public Quota(long total, BigDecimal ratio) {
this.total = total;
this.ratio = ratio;
this.amount = ratio.multiply(BigDecimal.valueOf(total)).longValue();
}
}
public interface RemainderDistribution {
void distribute(List<Quota> quotas, long total);
}
Lembra do Paradoxo do Alabama? Mesmo tendo validado com nosso Product Manager que adotaríamos o método de Hamilton ainda temos dúvidas sobre quais consequências esse paradoxo, ou até outro, pode ter. Então não queríamos "chumbar" a distribuição no código do Money. Queríamos deixar nosso Value Object aberto para extensão mas fechado para modificação. Por isso RemainderDistribution é uma interface. Uma das suas implementações, a que utilizamos, é a HamiltonApportionmentDistribution.
public class HamiltonApportionmentDistribution implements RemainderDistribution {
@Override
public void distribute(List<Quota> quotas, long total) {
long remain = total;
for (Quota quota : quotas) {
remain = remain - quota.getAmount();
}
List<Quota> sortedQuotas = quotas.stream().sorted(Comparator.comparing(Quota::getFractionalPart).reversed()).collect(Collectors.toList());
Iterator<Quota> iterator = sortedQuotas.iterator();
while(remain > 0) {
remain = remain - 1;
iterator.next().addRemain(1);
}
}
}
Dessa maneira resolvemos aquele problema inicial, onde arredondando pra baixo "perdíamos" 3 centavos (lembre-se que o arredondamento pra baixo é por conta dos centavos, similar a quando "arredondamos um senador" pra baixo). O cálculo deve ser feito utilizando a menor unidade de medida, ou seja, centavos. E chegamos no seguinte resultado:
Espero que até aqui eu tenha desmistificado a intersecção entre Alexander Hamilton, Martin Fowler e Uncle Bob com os sistemas financeiros. E, de quebra, você ganha uns códigos pra poder aplicar na sua linguagem favorita. Ah, todo esse código foi coberto por testes unitários. E sabe quando você leu as sentenças na primeira pessoa do plural? É porque desenvolvemos essa solução em 3, então todo o crédito também ao Victor Sales de Brito e Lucelia Siqueira. Valeu, time!
Bônus: pra saber quem foi Alexander Hamilton, existe um musical disponível na Disney+, chamado Hamilton. Conta a história desse que foi o Primeiro Secretário do Tesouro Americano em forma de rap, hip hop e jazz, gravado diretamente da Broadway em 2016. Segue trailer desse musical fantástico:
Top comments (0)