Boas práticas

Este documento foi criado com o objetivo de apresentar sugestões de boas práticas para assuntos que são particulares da plataforma de desenvolvimento do Engine e do Web Framework.

Operações aritméticas com valores decimais

Os valores numéricos decimais no JavaScript são representados internamente por pontos flutuantes de precisão dupla. Essa é uma representação extremamente eficiente para cálculos computacionais, no entanto, por serem uma representação aproximada, não permite que todos os valores possam ser representados precisamente por ela. Cálculos triviais podem gerar resultados que em um primeiro momento podem parecer surpreendentes, como:

9646.55 - 9646.54 == 0.01; // => false

Esse resultado não é específico do Engine, nem é algo particular do JavaScript. Todas as linguagens de programação que adotam essa representação apresentam esse comportamento. É importante observar que essa não é a única forma de representar números decimais e para cálculos financeiros são recomendadas APIs ou tipos alternativos que permitam aritmética decimal com precisão arbitrária, como a classe BigDecimal disponibilizada pelo Engine.

Dado o contexto acima, seguem algumas práticas recomendadas na manipulação de valores numéricos.

Use a classe BigDecimal

Sempre que for possível, utilize a classe BigDecimal. Ela permite cálculos aritméticos com precisão de até 38 dígitos. Exemplo:

new BigDecimal(9646.55).minus(9646.54).equals(0.01); // => true

Arredonde os resultados das operações ariméticas

Os resultados das operações aritméticas devem ser arredondados por meio da função Math.decimalRound para uma quantidade de casas decimais que faça sentido para o domínio de negócio tratado. Por exemplo, para valores monetários, possivelmente não fará sentido armazenar valores com mais de 2 casas decimais.

A função decimalRound também permite indicar a estratégia de arredondamento, que pode ser diferente de acordo com as regulamentações envolvidas. Por exemplo, no convênio ICMS 85 / 01 é determinado que os valores das vendas registradas em equipamentos fiscais sejam arredondados de acordo com a norma ABNT NBR 5891, equivalente ao RoundingMode.HALF_EVEN, exceto quando se tratar de comercialização de combustíveis, que requer que os valores sejam truncados, equivalente ao modo RoundingMode.DOWN. Outras regulamentações podem existir, não havendo uma solução única para todos os domínios de negócio. O desenvolvedor deve estar atento às exigências legais envolvidas e usar o modo de arredondamento adequado. Exemplo de uso:

Math.decimalRound(2.555, 2, RoundingMode.HALF_EVEN); // => 2.56
Math.decimalRound(2.555, 2, RoundingMode.DOWN); // => 2.55

Outro aspecto que deve ser levado é consideração é o limite de precisão dos valores gravados no banco de dados. Apesar do tipo number utilizar internamente uma representação de ponto flutuante de precisão dupla, o Engine grava esses valores no banco de dados em campos de tipo numérico com precisão definida em 38 e escala 10. Na prática, isso significa que no máximo poderão ser armazenados valores com 28 dígitos inteiros e 10 casas decimais. Valores com uma quantidade maior de casas decimais serão arredondados e aqueles com mais de 28 dígitos inteiros não serão aceitos, sendo gerado um erro no momento da gravação. É importante que o desenvolver tenha esses limites em mente e antecipe o arrendondamento para a camada de negócio, evitando assim a divergência entre o que foi gravado e o que será lido posteriormente do banco de dados.

Evite comparações exatas

Em vez de verificar se um número é zero, utilize Math.isZero. Essa função verifica se valor é aproximadamente zero tolerando uma diferença de até 0.0000001 positiva ou negativa. Essa tolerância é suficiente para contornar as imprecisões introduzidas pelo tipo number, que normalmente ocorrem na ordem de 1E-012. Exemplo de uso:

(9646.55 - 9646.54 - 0.01) === 0; // => false
Math.isZero(9646.55 - 9646.54 - 0.01); // => true

Um erro similar é verificar se o valor está contido em intervalo sem levar em consideração a imprecisão do tipo number. No exemplo abaixo, a função doSomething() não será chamada, apesar do valor ser equivalente a 0.01:

const value = 9646.55 - 9646.54; // => 0.00999999999839929
if (value >= 0.01 && value < 10) {
    doSomething(value);
}

A lógica acima pode ser revista para utilizar a classe BigDecimal para realizar as comparações de forma precisa:

const value = new BigDecimal(9646.55 - 9646.54).toDecimalPlaces(2); // => 0.01
if (value.greaterThanOrEqualTo(0.01) && value.lessThan(10)) {
    doSomething(value);
}

Uso de chaves

Chaves são um recurso limitado do sistema e não devem ser desperdiçadas. De uma forma geral:

  • não devem ser criadas chaves que não serão de fato gravadas no banco de dados.
  • abertura de operações nunca devem criar chaves, pois uma operação pode ser aberta diversas vezes após a sua criação, sem que sejam efetivadas alterações. É importante que sejam criados testes de integração que garantam que a abertura de operações não criem chaves. A API session.limitKeyCreation pode ser utilizada para limitar a criação de chaves durante a execução dos testes.

Evite criar chaves ao sincronizar dataSets

Ao sincronizar dataSets, tenha cuidado em garantir que o dataSet a ser atualizado não está com a propriedade insertWithKey ativa, pois o append() irá gerar uma chave que será logo em seguida sobreposta pela chave da origem dos dados.

Errado:

for (origem.first(); !origem.eof; origem.next()) {
  if (!destino.findKey(origem.chave)) {
    destino.append();
  }
  destino.copyRecord(origem);
  destino.post();
}

Correto:

const bkpInsertWithKey = destino.insertWithKey;
destino.insertWithKey = false;
try {
  for (origem.first(); !origem.eof; origem.next()) {
    if (!destino.findKey(origem.chave)) {
      destino.append();
    }   
    destino.copyRecord(origem);
    destino.post();
  }
} finally {
  destino.insertWithKey = bkpInsertWithKey;
}

Não dependa da ordem das chaves

As chaves geradas pelo sistema não seguem necessariamente uma ordem crescente. As chaves são reservadas aos Engines por meio de blocos que podem ser consumidos em momentos distintos. Portanto, ao ver que um registro tem um chave maior que a de outro, não presuma que ele foi criado depois dele, nem crie lógicas que dependam dessa ordem.