Boas Práticas de Programação – Código Simples, Resultados Rápidos

Introdução

Este tópico está entre “Dicas Valiosas” e “Boas Práticas de Programação”, de qualquer modo aborda um exemplo de código, ainda em desenvolvimento por um colega que está iniciando no “Mundo do AdvPL”. Partindo deste fonte de exemplo, vamos analisar o que o fonte faz, como faz, e inclusive avaliar algumas opções de escrever um algoritmo que faça a mesma coisa, usando funções diferentes ou escritos de uma outra forma, e se existe algum ganho ou perda, direto ou indireto ao optarmos por uma ou outra forma de resolução.

O Fonte em AdvPL – Primeira versão

#INCLUDE "PROTHEUS.CH"
#INCLUDE "TOTVS.CH"
#INCLUDE "FILEIO.CH"

USER FUNCTION TESTE()
  
        local nArquivo
        local i
        local nNumero
        local nNum := 7 

        msgAlert("Início.")

        nArquivo := fcreate("C:\devstudio\Melhorias\src\arquivo.txt")

        if ferror() != 0
                msgalert ("ERRO GRAVANDO ARQUIVO, ERRO: " + str(ferror()))
                return
        endif 
        for i:=1 to 500
                nNumero = randomize(100000,999999)
                if (mod(nNumero,nNum)) == 0 //dividendo e divisor respectivamente
                        fWrite(nArquivo, "O numero < " + alltrim(str(nNumero)) + " > é divisível por " + alltrim(str(nNum)) + chr(13) + chr(10))
                else
                        fwrite(nArquivo, "O numero <" + alltrim(str(nNumero)) + "> não é divisível " + alltrim(str(nNum)) + chr(13) + chr(10))
                Endif
        next    
        fclose(nArquivo)

        msgAlert("Fim.")
Return

Engenharia Reversa

Olhando o fonte linha por linha, podemos deduzir o que ele faz ao ser executado. Inicialmente, o fonte declara as variáveis utilizadas com escopo “Local”, mostra uma mensagem na tela do SmartClient informando que o programa está no “Início”, após pressionar OK ou o simplesmente fechar a janela a aplicação continua, criando um arquivo em um path específico na máquina onde está sendo executado o SmartClient –que pode ser a mesma máquina onde o SmartClient pode estar sendo executado, ou uma outra máquina conectando neste Protheus Server via REDE TCP/IP.  — Arquivo “C:\devstudio\Melhorias\src\arquivo.txt””.

A aplicação usa fCreate() para criar o arquivo, verifica se houve falha na criação verificando o retorno da função fError(), faz um loop de 500 iterações,  sorteia um número entre 100000 e 999999, e escreve no arquivo o número sorteado usando fWrite(), e antes de escrever determina se o número sorteado é múltiplo de 7 ou não, usando a função Mod().

Primeira Revisão

Logo nas primeiras linhas do arquivo, foram usados dois includes: “protheus.ch” e “totvs.ch”. Internamente, ambos são o mesmo arquivo. Na verdade, o include totvs.ch inclui internamente o arquivo protheus.ch. Foi criado um include com o nome de “totvs.ch” apenas para padronização de código. Logo, tanto faz qual deles for usado, precisamos somente de um deles.

O include fileio.ch possui algumas constantes que opcionalmente podem ser usadas pelas funções de baixo nível de arquivo, como a fCreate(), fOpen() e fSeek(). No caso do fonte acima, como nenhuma constante foi usada, fazer referência a este arquivo também é desnecessário.

Quanto a verificação de erro de criação de arquivo, normalmente verificamos se o resultado da função fCreate() é diferente de -1 (menos um), pois este é o valor retornado em caso de falha da função. Porém, não há nada de mais em usar a função fError() — desde que logo após a chamada da fCreate() — pois em caso de sucesso, fError() retorna 0 (zero).

Dento do laço for…next de 500 iterações, cada volta sorteia um número e verifica se ele é ou não múltiplo de 7. Quando queremos uma conversão de um valor numérico para string, sem espaços adicionais, podemos usar tranquilamente a expressão alltrim(str(nNumero)). De qualquer modo, no AdvPL temos uma função básica da linguagem que realiza exatamente este tipo de conversão, inclusive para outros tipos de dados de entrada, resultando em uma string — chama-se cValToChar(). A utilização dela neste cenário apenas economiza a chamada de funções — ao invés de duas, realizamos a conversão com apenas uma.

Na declaração de variáveis, foi usada corretamente a notação húngara — a primeira letra do nome da variável indica o seu tipo. Isto é “praxe” em AdvPL, ajuda bastante. E, outra coisa que ajuda é colocar nomes que tenham relação com o conteúdo. Olhando o fonte, sabemos que nNumero foi gerado por sorteio, e nNum é o número para verificar se o primeiro é múltiplo dele. Porém, ficaria mais “elegante” nomear nNum com, por exemplo “nDivTst” — divisor de teste.

Outros modos de fazer a mesma coisa

Existem duas formas de verificar se um número tem resto inteiro após uma divisão por outro. Podemos usar a função “mod()” ou o operador “%“, o resultado é o mesmo, ambos estão certos.

if ( mod(nNumero, nNum )  ==  0)
if ( ( nNumero % nNum ) == 0 )

 

Porém, existe também uma forma de redução de código, que eu particularmente acho feia, pois dificulta a legibilidade do código, e não há ganho NENHUM em desempenho, por exemplo:

if (mod(nNumero,nNum)) == 0 //dividendo e divisor respectivamente
   fWrite(nArquivo, "O numero < " + alltrim(str(nNumero)) + " > é divisível por " + alltrim(str(nNum)) + chr(13) + chr(10))
else
   fwrite(nArquivo, "O numero <" + alltrim(str(nNumero)) + "> não é divisível " + alltrim(str(nNum)) + chr(13) + chr(10))
Endif

O código acima poderia ser escrito de outra forma, com a mesma funcionalidade, porém com uma sintaxe totalmente diferente.

fWrite(nArquivo, ;
    "O numero <" + cValToChar(nNumero) + ">"+;
    IIF( mod(nNumero,nNum) == 0 , " é "," não é ") + ;
    "divisível por " + cValToChar(nNum) +chr(13)+chr(10) )

EXCEPCIONALMENTE NESSE EXEMPLO usando o recurso de quebrar linhas de código com ponto e vírgula no final, e tratando uma condição SIMPLES, com duas situações de retorno PEQUENO, escrever dessa forma não ficou tão “feio”. Usamos a função IIF() — ou “if em linha” — cujo comportamento é avaliar e retornar o segundo parâmetro informado caso a condição informada no primeiro argumento seja verdadeira, ou processar e retornar o terceiro parâmetro caso a condição informada seja falsa.

De qualquer modo, eu recomendo fortemente que esse tipo de redução seja usada com muita cautela, escrever um código todo rebuscado cheio de operadores e expressões “MegaZórdicas” não quer dizer que você é um bom programador. Escrever o IF — ELSE — ENDIF tradicional e colocar exatamente o que você quer fazer, mesmo que em alguns casos possa repetir um pouco de código, é preferível para dar LEGIBILIDADE ao código.

“Codifique sempre como se o cara que for dar manutenção seja um psicopata violento que sabe onde você mora.” – Martin Golding

“Qualquer um pode escrever um código que o computador entenda. Bons programadores escrevem códigos que os humanos entendam.” – Martin Fowler

 

Desempenho

Para realizar um teste de desempenho, eu removi as chamadas de MsgAlert(), e inseri uma contagem de tempo, declarando uma variável nTempo como “Local”, atribuindo nesta variável imediatamente antes da instrução “For” o retorno da função seconds(), e depois na instrução “Next”, eu mostro na tela uma MsgAlert() com o tempo que a função demorou para executar o loop de geração de números e de gravação em disco, com precisão de 3 casas decimais.  O fonte após a revisão ficou desta forma:

#INCLUDE "PROTHEUS.CH"

USER Function TESTE()
Local nArquivo , nNumero
Local nDiv := 7 
Local i, nTempo

nArquivo := fcreate("c:\Temp\arquivo.txt")
If ferror() != 0
   MsgAlert("ERRO GRAVANDO ARQUIVO, ERRO: " + cValToChar(ferror()))
   return
EndIf
nTempo := seconds()
For i:=1 to 5000
   nNumero = randomize(100000,999999)
   If (mod(nNumero,nDiv)) == 0
      fWrite(nArquivo, "O numero < " + cValToChar(nNumero) + " > é divisível por " + cValToChar(nDiv) + chr(13) + chr(10))
   Else
      fwrite(nArquivo, "O numero <" + cValToChar(nNumero) + "> não é divisível " + cValToChar(nDiv) + chr(13) + chr(10))
   EndIf
Next
MsgAlert("Tempo Total = "+str(seconds()-nTempo,12,3)+' s.')
fclose(nArquivo)
Return

Para uma métrica de tempo mais apurada, 500 iterações era muito pouco. Aumentei para 5000 iterações, e no meu equipamento a execução deste código demora entre 500 e 600 milissegundos (0.5 a 0.6 segundos).

Pontos de Atenção

Após olhar o arquivo com as informações geradas, reparei em um comportamento interessante. Os números gerados randomicamente começavam em 100000 , mas nunca eram maiores que 132000. Após consultar a documentação da função Randomize() na TDN, reparei que ela têm uma observação sobre seu funcionamento.

A função Randomize(), trabalha com um intervalo interno de 32767 números, a partir do número inicial informado, inclusive se o número inicial for negativo.

Logo, embora eu possa especificar um número inicial e final, se a diferença entre ambos for maior que 32767, ela considera o limite final igual ao limite inicial + 32767 menos uma unidade, e não o limite informado. Neste caso, precisamos usar um outro algoritmo para chegar a um número tão grande —  veremos como contornar isto mais para frente, em um próximo post.

Pontos de Melhoria

Sempre têm alguma coisinha que podemos fazer para melhorar o código, direta ou indiretamente. No caso do programa de exemplo acima, por uma mera questão circunstancial o arquivo a ser criado e que receberá os dados a serem gravados foi passado para a função contendo o que chamados de “path completo”, desde a unidade de disco, diretório e nome do arquivo com extensão. A maioria das funções de baixo nível de arquivo do AdvPL entende que, se um arquivo possui a letra da unidade de disco informada, este arquivo deve ser criado e manipulado pelo SmartClient.

Isto significa que, quando o Servidor roda a instrução fCreate(), ele envia uma mensagem ao SmartClient para que ele crie o arquivo, não importa se o SmartClient e o Protheus Server estão rodando na mesma máquina. E, cada instrução fWrite() executada faz o Protheus Server enviar uma linha com as informações para o SmartClient acrescentar no arquivo.

Os tempos de execução deste programa no meu ambiente, inserindo 5 mil linhas, estava em torno de 0.5 a 0.6 segundos. Vamos criar dentro da pasta do ambiente atual em uso (RootPath especificado na configuração de ambiente do appserver.ini) uma pasta chamada “Temp”, e no fonte AdvPL, vamos passar a criar este arquivo a partir do Servidor, mais precisamente dentro da pasta “Temp”.

nArquivo := fcreate("c:\Temp\arquivo.txt")

Deve ser trocado para

nArquivo := fcreate("\Temp\arquivo.txt")

Agora, executamos o programa novamente, e … surpresa, o tempo de execução CAIU  para 0,170 a 0,200 segundos. Isso é quase três vezes mais rápido, e olhe que no meu ambiente o SmartClient e o Application Server estão no mesmo equipamento. Logo, este tempo a mais — que chamamos de “overhead” — envolve a aplicação montar a mensagem de gravação para o SmartClient, enviar pela rede, o SmartClient receber a mensagem, decodificar a mensagem, processar a mensagem e informar que a operação foi executada com sucesso. Esta diferença de desempenho é mais acentuada ainda quando o SmartClient e o Application Server estão realmente sendo executados em duas máquinas diferentes, com uma rede TCP entre elas.

É possível melhorar esse tempo no SmartClient ?

Sim, é possível sim, melhorar significativamente. Repare que cada iteração no fonte gera uma linha de aproximadamente 41 bytes quando o número não é divisível por 7, e 37 bytes quando é divisível. Um pacote único de rede TCP normalmente suporta até 1500 bytes. Logo, se ao invés de escrever 36 linhas de 41 bytes, uma para cada fWrite(), a gente acumulasse na memória essas 36 linhas em uma variável caractere, gerando uma string de aproximadamente 1476 bytes, e enviasse essa string na função fWrite(), podemos reduzir bem o tempo de transmissão de dados na rede, pois é mais rápido transmitir um pacote de 1476 bytes do que transmitir 36 pacotes de 41 bytes. Neste caso, o fonte ficaria assim:

USER Function TESTE()
Local nArquivo , nNumero
Local nNum := 7 
Local i, nTempo
Local nX
Local cTemp := ''

nArquivo := fcreate("c:\Temp\arquivo.txt")
If ferror() != 0
   msgalert ("ERRO GRAVANDO ARQUIVO, ERRO: " + str(ferror()))
   return
EndIf
nTempo := seconds()
For i:=1 to 5000
   nNumero = Randomize(100000,999999)
   If ( nNumero % nNum ) == 0
      cTemp += "O numero <" + cValToChar(nNumero) + "> é divisível por " + cValToChar(nNum) + chr(13) + chr(10)
   Else
      cTemp += "O numero <" + cValToChar(nNumero) + "> não é divisível por " + cValToChar(nNum) + chr(13) + chr(10)
   EndIf
   IF i % 36 == 0 
      fWrite(nArquivo,cTemp)
      cTemp := ''
   Endif
Next 
if !empty(cTemp)
   fWrite(nArquivo,cTemp)
Endif
conout("Tempo Total = "+str(seconds()-nTempo,12,3)+' s.')
fclose(nArquivo)
return

Conclusão

Para meu espanto, na máquina local (APPServer e SmartClient no mesmo equipamento), os tempos ficaram praticamente iguais, gravando o arquivo no path “c:\Temp” através do SmartClient, ou gravando dentro da pasta “\temp\” a partir do RootPath do ambiente no APPServer.

Espero que este post agregue conhecimento para quem está começando no AdvPL, e para quem já têm alguma experiência com a linguagem e com o ambiente. Afinal, conhecimento nunca é demais. Desejo a todos TERABYTES DE SUCESSO. 

Agradecimentos

Agradeço o colaborador Hussyvel Ribeiro, que me concedeu um fonte de testes para usar neste post. Hussyvel, seja bem vindo ao AdvPL !!! 😀 

Referências

 

 

7 comentários sobre “Boas Práticas de Programação – Código Simples, Resultados Rápidos

  1. Boa noite.

    Muito legal o seu blog. Parabéns pelo conteúdo.

    Sobre as boas práticas, uma coisa que eu particularmente não gosto é “return” no meio da função.
    Isso dificulta o tratamento que se queira fazer ao sair da função (RestArea por exemplo), tendo que repetir esse tratamento em todo lugar que tiver return.

    Isso se agrava se a função tiver controle de transação. Já vi fontes com “return” no meio da transação, fazendo com que a rotina saísse da função sem passar pelo “End Transaction”. Imagine a bagunça.

    E outra coisa que acho que dificulta um pouco a manutenção de fonte é a criação de variáveis com uma letra só. Fazer a pesquisa dessa variável no fonte é bem difícil.

    Curtido por 1 pessoa

    • Return no meio de código, quando o código é curto e específico, até ajuda. Mas em rotinas maiores, é bom evitar …rs… Normalmente usamos variáveis com uma letra, normalmente i ou n , em iterações for .. next , onde a variável é somente um iterador de contagem, sem maior significado para o prograna em si. Porém, quando usamos o contador em processamentos posicionais ou em sub-rotinas, fica bem mais legível usar um nome mais completo, relacionado ao uso e objetivo da variável. Em breve sai mais um “Boas práticas” e eu vou abordar os pontos que você sugeriu 😀

      Abraços

      Curtir

  2. Trabalho na TOTVS e estou aprendendo a programar e algumas pessoas que conheço tem o Júlio como referência, realmente este foi um belo post, parabéns e obrigado por compartilhar o teu conhecimento conosco!

    Curtido por 1 pessoa

Deixe uma resposta para Wagner Lima Cancelar resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s