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.
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
Chaves podem ser representadas por valores do tipo number
ou por instâncias da classe DBKey
,
essa última, mais utilizada no runtime JavaScript V8. 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 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.