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.

Geral

Não utilize APIs não documentadas

O runtime JavaScript Ije não possui um método eficiente de criar APIs privadas. Por esse motivo, métodos e propriedades são prefixados ou sufixados com “_” para indicar privacidade. No entanto, essa solução não pode ser aplicada aos módulos e classes, sem poluir artificialmente os caminhos de inclusão desses módulos. Também há propriedades legadas que nunca chegaram a adotar uma nomenclatura de privacidade.

Em razão disso, deve-se considerar que apenas os módulos, classes, propriedades e métodos documentados em https://api.nginstack.com são APIs públicas. APIs que não estejam documentadas no site devem ser consideradas privadas ou experimentais e não há nenhuma garantia de estabilidade sobre elas. Atualizações do sistema podem remover ou alterar o comportamento dessas APIs não documentadas sem nenhum tipo de aviso.

Banco de dados

Não altere o tipo do dado das colunas

De uma forma geral, a ampliação do tamanho de campos do tipo “varchar” e o aumento da precisão e escala dos tipos numéricos são as únicas alterações bem suportadas por todos os SGBDs.

As demais alterações de tipo de dado de uma coluna não são recomendadas pelos SGBDs suportados pelo Engine. O Oracle em particular não permite a alteração de tipo de dados em colunas que têm valores preenchidos e os demais SGBDs permitem a conversão de forma restrita, normalmente às custas de uma reescrita completa da tabela. Reescritas de tabelas podem ser extremamente demoradas e onerosas, bloqueando o uso do sistema durante todo o processo de conversão, que pode ser na ordem de horas.

Os processos de atualização do sistema se limitam a aplicar os comandos de alteração do tipo de dado e falharão caso o SGBD não suporte as modificações realizadas. Por esse motivo, caso a conversão seja estritamente necessária, ela deverá ser realizada por processos de manutenção que devem ser desenvolvidos para os casos específicos. É importante que o processo avalie a natureza da alteração e trate a possibilidade da conversão não poder ser realizada durante uma janela de manutenção da base de dados de produção. Também deve ser avaliado o custo de armazenagem requerido para a conversão, que pode duplicar temporariamente o tamanho das tabelas afetadas.

Utilize apenas a sintaxe SQL padrão e os tipos normalizados pelo Engine

As instruções SQL não devem utilizar funcionalidades específicas de um SGBD e devem utilizar os tipos de dados e funções normalizadas pelo Engine a fim de garantir a interoperabilidade entre os SGBDs. Mais detalhes no manual Bancos de dados.

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.