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.

Vale ressaltar que a interface cliente do Web Framework é uma implementação interna do sistema e é gerada de forma dinâmica a partir do uso das APIs públicas e documentadas. Não devem ser realizadas customizações ou automações que dependam de detalhes internos de implementação da interface, como a hierarquia e identificadores de tags HTML, classes CSS e APIs JavaScript disponíveis no cliente. Essas estruturas são alteradas livremente na evolução da interface cliente do sistema e não há como preservar o comportamento das versões anteriores. Parte delas são geradas de forma automática pelas ferramentas utilizadas na construção do sistema, podendo ser modificadas a cada nova versão liberada.

Evite utilizar APIs legadas

Via de regra, a evolução do sistema tenta preservar ao máximo as APIs públicas existentes a fim de garantir que os códigos construídos sobre elas continuem a funcionar corretamente a cada atualização do sistema. Mesmo APIs marcadas como legadas são preservadas de forma indefinida, desde que o seu uso não comprometa o funcionamento e a segurança do sistema.

No entanto, é importante observar que apesar de serem mantidas em funcionamento, as APIs legadas deixam de receber evoluções, melhorias de desempenho e até correções que possam afetar o comportamento dos códigos existentes. Por esse motivo, é recomendado que em algum momento o seu uso seja revisto e que elas jamais sejam utilizadas em códigos novos. Insistir no uso delas pode perpetuar os problemas que levaram essas APIs a serem classificadas como legadas.

Importante: apesar do objeto connection e sua classe Connection, não serem marcados como APIs legadas, a maioria das suas propriedades e métodos são. O connection mistura informações de diversos domínios e fontes de dados, algumas obtidas rapidamente a partir do cache local ou memória, outras por meio de custosas requisições ao Engine servidor ou ao banco de dados. O seu uso é confuso e historicamente tem provocado problemas graves de desempenho e estabilidade do sistema. Por esse motivo, todos os métodos e propriedades que não dizem respeito ao Engine servidor associado à conexão são considerados legados e não deveriam ser mais utilizados. Na maioria das situações, o uso do connection deve ser substituído pelas APIs database, dbCache, classes, security e engine.

Scripts de inicialização e configuração não devem acessar a base de dados

Scripts de inicialização, como os de Sessão, Engine e Web Framework, e os scripts de definição das classes de dados, como os arquivos x-config, x-model, x-class e x-view, não devem fazer consultas à base de dados, nem utilizar APIs que façam requisições ao Engine servidor.

Além de prejudicarem o desempenho do sistema, as requisições ao banco de dados e ao Engine servidor durante a execução dos scripts de inicialização e configuração impedem a operação off-line do sistema e podem sobrecarregar o Engine servidor com requisições recursivas na obtenção das configurações das classes de dados do sistema, podendo levar até o esgotamento do atendimento do servidor.

Observação: o objeto connection não deveria ser utilizado em scripts de inicialização e configuração, pois não é claro quais dos seus métodos e propriedades são resolvidos sem requisições ao servidor. Em seu lugar, utilize as APIs dbCache, classes, security e engine.

Evite nomear uma função definida em uma expressão

Expressões de funções atribuídas a objetos ou variáveis tem o seu nome definido automaticamente pelo runtime JavaScript, não devendo ser nomeadas manualmente.

Errado:

const test = function Test() {
};

function Class() {}
Class.prototype.test = function Class_test() {
};

Correto:

const test = function () {
};

function Class() {}
Class.prototype.test = function () {
};

No exemplo acima, a primeira função terá o nome definido automaticamente pelo runtime JavaScript como test e a segunda como Class.test. A nomeação manual é um trabalho supérfluo que gera manutenção, pois a variável ou nome da classe podem mudar. Quando isso ocorre, o nome definido manualmente pode ficar desatualizado, podendo confundir as análises do profiler ou da pilha de chamadas de erro realizadas por um desenvolvedor que está esperando o comportamento padrão do sistema.

Uma exceção importante a essa prática ocorre em módulos que exportam funções. Nesse caso, é uma boa prática nomear a função, pois o runtime JavaScript não tem como sugerir um nome adequado. Exemplo:

module.exports = function test() {
};

Banco de dados

Não concatene valores diretamente em uma expressão SQL

Todo valor concatenado em uma expressão SQL deve ser tratado a fim de prevenir ataques de injeção.

Errado:

const ds = database.query("SELECT * FROM iGroupUser WHERE iKey = " + this.chaveUsuario);

Correto:

const toSqlString = require('@nginstack/engine/lib/string/toSqlString');
const ds = database.query("SELECT * FROM iGroupUser WHERE iKey = " + toSqlString(this.chaveUsuario));

Mais detalhes no manual Prevenção de injeção de códigos.

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 aritmé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 comparações estritas de chaves numéricas

Chaves podem ser representadas por valores do tipo number ou por instâncias da classe DBKey. O uso da comparação estrita impede que as duas representações distintas de uma mesma chave sejam tratadas como equivalentes. Quando forem comparadas chaves com os valores dos campos de um DataSet, deve ser dada preferência à comparação não estrita com o valor numérico da chave.

Errado:

const keys = [];
ds.indexFieldNames = 'iParent';
if (ds.find(parent)) {
    while (!ds.eof && ds.getField('iParent') === parent) {
        keys.push(ds.getField('iKey'));
        ds.next();
    }
}

Correto:

const keys = [];
ds.indexFieldNames = 'iParent';
if (ds.find(parent)) {
    while (!ds.eof && ds.num('iParent') == parent) {
        keys.push(ds.num('iKey'));
        ds.next();
    }
}

Evite instâncias persistentes de DBKey

No runtime JavaScript Ije, os campos dos registros armazenados no cache local podem ser consultados de forma simples como se fossem propriedades de uma chave armazenada em uma variável ou propriedade do tipo number. Exemplo:

const userKey = -1898186559;
userKey.istatus.iname; // => 'Ativo'

Essa funcionalidade, no entanto, não é possível de ser implementada no runtime V8, pois nele não é permitido estender o comportamento do tipo primitivo number. Nesse runtime, não existe a propriedade “istatus” em valores do tipo number e o código acima retornará um erro ao tentar ler a propriedade “iname” do valor undefined retornado pela propriedade “istatus”. Para obter o valor de um campo a partir de uma chave literal, é necessário o uso da classe DBKey, conforme exemplo abaixo:

const userKey = -1898186559;
DBKey.from(userKey).str('iStatus.iName'); // => 'Ativo'

Naturalmente pode surgir a ideia de migrar as APIs existentes para armazenarem chaves como DBKey em vez number, tornando os códigos compatíveis com os dois runtimes. No entanto, essa estratégia é arriscada e pode quebrar os códigos existentes que precisam comparar valores de chaves. A linguagem Javascript não permite sobrecarregar o operador == e a comparação de duas instâncias de DBKey sempre retorna false, por especificação da linguagem, tornando obrigatório o uso do método DBKey.equals nesse caso.

Historicamente o sistema foi construído representando chaves como valores do tipo number, sem um tipo específico que distinguisse as chaves de outros valores numéricos. Os bancos de dados, e por consequência os DataSets, também não distinguem as chaves dos demais números inteiros. A ausência dessa distinção torna muito arriscado um esforço de revisão da manipulação de chaves a fim de garantir que elas sempre são representadas por instâncias de DBKey e comparadas por meio do método equals. Por esse motivo, optou-se por continuar utilizando o tipo number como representação natural das chaves e utilizar o tipo DBKey apenas quando for necessário realizar uma operação de leitura de campos a partir de um valor do tipo number.

Levando em consideração o contexto acima, seguem recomendações práticas de uso da classe DBKey:

  1. Preferencialmente, utilize a classe DBKey apenas para fazer operações de lookups ou comparações de chaves:

    const statusName = DBKey.from(this.userKey).str('iStatus.iName');
    const isSameUser = DBKey.equals(this.userKey, userKey);
    
  2. Ao manipular DataSets, utilize os métodos de leitura de campo que suportam expressões lookup, evitando a criação desnecessária de instâncias de DBKey:

    const statusName = ds.str('iUser.iStatus.iName');
    
  3. Apenas em APIs onde se requer o uso frequente da classe DBKey deve ser avaliada a criação de propriedades desse tipo, sempre como alternativas às existentes de representação numérica:

    /**
     * Chave do usuário.
     * @type {number}
     */
    UserProfile.prototype.userKey = null;
    
    /**
     * Chave do usuário representada por uma instância de `DBKey`.
     * @type {DBKey}
     */
    UserProfile.prototype.userDBKey = null;
    
  4. Em funções que comparam chaves, utilize a comparação não estrita caso esteja comparando o valor recebido com uma chave numérica ou utilize o método equals() caso seja uma instância de DBKey.

    UserProfile.prototype.checkUser = function (userKey) {
        if (this.userKey == userKey) { /*...*/ }
        // or
        if (this.userDBKey.equals(userKey)) { /*...*/}
        // or
        if (DBKey.equals(userKey, anotherUserKey)) { /*...*/}
    }
    

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.

Profiler

Evite instrumentações manuais que podem ser automatizadas

O Engine pode gerar automaticamente instrumentação de todas as funções JavaScript por meio da opção “Automatic Profiler Active”, portanto a instrução manual dos métodos de uma classe, das funções e dos módulos é um trabalho manual desnecessário que é melhor realizado pelo próprio Engine.

A instrução manual do Profiler também é utilizada como uma ferramenta de observabilidade em servidores de produção, sendo muito comum a execução de servidores de produção com a opção “Profiler Enabled” ativa, mas com a opção “Automatic Profiler Active” desativada. Para que o Profiler possa ser utilizado dessa forma, somente devem ser instrumentados os códigos que sejam realmente relevantes de serem observados no ambiente de produção, onde o benefício da observação supera o custo da instrumentação do código.

O overhead do profiler é da ordem de milissegundos, sendo desprezível para operações de alto custo, como consultas ao banco de dados, escrita de arquivos, e operações de I/O de uma forma geral. No entanto, o profiler pode gerar um custo significativo de desempenho se a instrumentação manual for mal utilizada em funções de baixo custo, mas que são executadas milhares de vezes.

Mais detalhes no tópico Instrumentação do código fonte do manual Análise de desempenho.

Sempre instrumente códigos com blocos try-finally

O método startOperation deve ser imediatamente seguido por um bloco try-finally que garanta a execução do endOperation. No primeiro exemplo, um erro lançado pelo código instrumentado ou um return antecipado impedirá a finalização da instrumentação, gerando o desalinhamento das medições. Esse desalinhamento pode provocar estouros de memória em servidores de produção que tenham o profiler ativo.

Errado:

profiler.startOperation('code');
// code
profiler.endOperation();

Correto:

profiler.startOperation('code');
try {
    // code
} finally {
    profiler.endOperation();
}

Scheduler

Trate os erros dos scripts agendados

Ao criar um script que vai ser executado pelo Scheduler é importante estar atento ao seguinte fato: se a execução do script falhar com um erro, ocorrerá uma nova tentativa de execução após alguns minutos, independentemente da periodicidade da tarefa. A configuração de agendamento definida pelo usuário ou desenvolvedor é utilizada apenas quando o script é finalizado com sucesso.

O script é reexecutado para tratar eventuais erros transientes, como falhas de rede ou a indisponibilidade temporária da base de dados. No entanto, scripts que tenham um erro de implementação podem ser executados indefinidamente em intervalos de minutos. Essas execuções podem ser prejudiciais ao servidor, caso a execução consuma muita memória ou CPU, ou pode provocar o desperdício de recursos, como as chaves do sistema, caso o script crie registros.

Por esse motivo, é recomendado que o script tenha um bloco de try/catch englobando toda a lógica do script, permitindo que um eventual erro seja tratado adequadamente. Exemplo:

try {
  // Scheduled script code
} catch (e) {
  handleError(e);
}

O tratamento do erro irá variar de acordo com a importância de notificar o usuário da falha de execução. Rotinas executadas com frequência ou que não sejam críticas podem simplesmente logar a falha nos arquivos de log do Engine utilizando a classe Logger. Outras podem tentar enviar um e-mail para o administrador do sistema. Implementações mais sofisticadas podem gravar o erro em uma tabela de eventos, permitindo que os usuários consultem o status de execução dos scripts agendados. É importante apenas que a função de tratamento de erro não gere um novo erro, caso contrário o script continuará a ter a sua execução considerada como falha e será reexecutado novamente.

Importante: scripts de execução única que tenham o erro capturado e silenciado serão removidos do Scheduler e jamais serão executados novamente. Portanto, o tratamento de erro para esses scripts deve considerar que as informações contidas na tarefa agendada serão perdidas caso o erro seja simplesmente silenciado.