Submarino.com.br

Ratio

Objetivo

Permitir manipular frações minimizando as operações de divisão.

Propósito

A operação de divisão é uma das mais complexas no mundo da computação porque leva facilmente a uma perda de exatidão nos cálculos. Usando valores primitivos de ponto flutuante como double e float isso se torna mais óbvio, mas mesmo utilizando BigDecimal esse problema se coloca ao termos que escolher o modo de arredondamento a cada divisão.

O padrão Ratio(Rácio,Razão, Proporção) minimiza o problema ao adiar ao máximo a necessidade de executar a divisão, controlando o valor do numerador e do denominador separadamente e usando as regras aritméticas para frações.

Implementação

Para este exemplo de implementação usaremos a classe BigDecimal como base para mantermos os valores do numerador e do denominador.

public class Ratio implements Comparable<Ratio>{

	public static Ratio valueOf(String numerador){
		return Ratio(new BigDecimal(numerador), BigDecimal.ONE);
	}
	
	public static Ratio valueOf(String numerador, String denominador){
		return Ratio(new BigDecimal(numerador), new BigDecimal(denominador));
	}
	
	private BigDecimal numerador;
	private BigDecimal denominador;
	
	private Ratio(BigDecimal numerador, BigDecimal denominador){
		this.numerador = numerador;
		this.denominador = denominador;
	}

	public Ratio plus(Ratio other){
		// a soma de a/b + c/d é igual a (ad+cb)/bd
		
		BigDecimal ad = this.numerador.multiply(other.denominador);
		BigDecimal cd = this.denominador.multiply(other.numerador);
		
		BigDecimal bd = this.denominador.multiply(other.denominador);
		
		return new Ratio(ad.add(cd), bd); // não ha divisão
	}
	
	public Ratio subtract(Ratio other){
		return this.plus(other.negate());
	}
	
	public Ratio negate(){
		return new Ratio(this.numerador.negate(), this.denominador);
	}
	
	public Ratio multiply(Ratio other){
		// o produto de a/b x c/d é igual a ac/bd
		
		BigDecimal ac = this.numerador.multiply(other.numerador);
		BigDecimal bd = this.denominador.multiply(other.denominador);
		
		return new Ratio(ac, bd); // não ha divisão
	}
	
	public Ratio divide(Ratio other){
		// a divisão de a/b  por c/d é igual 
		// ao produto de a/b x d/c que é igual a ad/bc
	
		BigDecimal ad = this.numerador.multiply(other.denominador);
		BigDecimal bc = this.denominador.multiply(other.numerador);
		
		return new Ratio(ad, bc); // não ha divisão
	}
	
	public boolean isZero(){
		return numerator.compareTo(BigDecimal.ZERO)==0;
	}
	
	public boolean isOne(){
		return numerator.compareTo(denominador)==0;
	}
	
	public BigDecimal asNumber(){
		return numerator.signum()==0 
              ? BigDecimal.ZERO 
              : denominator.compareTo(BigDecimal.ONE)==0
                  ? numerator 
                  : numerator.divide(denominator, 15, RoundingMode.HALF_EVEN);
	}
	
	public int compareTo(Ratio other){
	    // a/b == c/d => ad == cb

	 	BigDecimal cb = denominator.multiply(other.numerator);
        BigDecimal ad = numerator.multiply(other.denominator);
                
        return ad.compareTo(cb);
	}
	
	public int hashCode(){
		return 0;
	}
	
	public boolean equals(Object other){
		return other instanceof Ratio && compareTo((Ratio)other)==0;
	}
	
}						
			
Código 1: Implementação do padrão Ratio

Repare que a única vez que dividimos o numerador pelo denominador é quando somos obrigados a retornar um valor condensado em asNumber(). Mesmo quando comparamos dois Ratio o fazemos sem recorrer à divisão.

Esta implementação de Ratio tem a implementação de Comparable compativel com equals. Para isso tivemos que sacrificar a computação de um hashCode melhor. Isto porque numeradores e denominadores iguais garantem igualdade, mas numeradores e denominadores diferentes não necessariamente significam desigualdade. Por exemplo, 2/3 é o mesmo que 4/6 e são objetos iguais, embora os numeradores e denominadores sejam todos diferentes. O uso de uma constante garante que o método hashCode está implementado em coerência com equals.

Discussão

O padrão Ratio adia todas as operações de divisão desde que todas as operações sejam feitas com ele. Isso permite que trabalhemos até com números impossíveis como 0/0, o que pode ser um problema no caso geral.

Aqui nomeamos a implementação da classe com o nome do padrão, mas numa aplicação real, usaríamos um nome mais sugestivo como NumeroReal, por exemplo.

No exemplo de implementação usamos BigDecimal para exemplificar e manter os cálculos simples. Na prática poderíamos usar outras estratégias como usar BigInteger ou mesmo usar objetos que implementem Quantity, como Money.

O próprio padrão Money pode ser implementado internamente usando um objeto Ratio internamente ou ele mesmo ser implementado conforme este padrão. Ao trabalhar com dinheiro divisões implicam em arredondamentos que implicam em perda ou ganho de centavos o que implica num problema de contabilidade.

Padrões associados

Ratio está diretamente associado a Value Object pois representa essencialmente um valor numérico. O objetivo de Ratio é prevenir o uso da operação de divisão e isso tem aplicação direta em várias áreas mas principalmente na financeira relacionando o padrão Ratio com o padrão Money