Boas Práticas

Clean Code: Utilizando no ADVPL

Hoje falaremos um pouco mais sobre as técnicas do Clean Code focando na linguagem de programação AdvPL. Para quem não sabe, o AdvPL é uma linguagem de programação de propriedade da empresa TOTVS, e ao contrário do que muitos pensam ela também pode adotar a padronização do Clean Code sem maiores problemas.

Sabemos que o ADVPL possui como característica uma limitação quanto a nomenclatura de variáveis e funções (limitação essa que caiu com o surgimento do TLPP). Todavia, como o objetivo desse artigo é abordarmos o uso do Clean Code deixo aqui algumas dicas que sigo ao decorrer de desenvolvimentos (não se trata de um padrão convencional da TOTVS):

Nomenclaturas

Nesse caso não devemos olhar para as limitações que a linguagem nos impõe e sim saber abreviar ou escrever de forma inteligente.

User Function TESTE01()
    Local cNumPed       := "" //RUIM
    Local cNumeroPedido := "" //BOM
   
    validarClienteExiste()

Return

Static Function validarClienteExiste()
    Local cPedido       := ""   //RUIM
    Local cPedCompra    :=      //BOM
    
Return

Nota-se que no exemplo acima excedemos o limite máximo, mas concordam que a legibilidade do nome da função e das variáveis ficou bem melhor do que se estivesse abreviado?

Notaram também que quando precisei abreviar a variável cPedCompra foi para que se no mesmo fonte eu precisasse guardar a informação do número de um pedido de venda bastaria abreviar também essa nova variável, ficando por exemplo cPedVenda, mitigando as chances de dar duplicidade com a já existente.

Funções

Dizemos no Clean Code, que uma função não pode possuir muitos parâmetros, pois quanto mais parâmetros, maior a possibilidade daquela função estar fazendo algo a mais que não deveria. Por isso o recomendado é que a cada função o número máximo de parâmetros informado seja de 3. Utilizamos também o padrão Camel Case para a nomenclatura de Static Function.

//BOM
Static Function validarClienteExiste( cCodigoCliente, cLojaCliente )
    //Seu código aqui...

Return

//RUIM
Static Function ValidarClienteExiste( cCodigoCliente, cLojaCliente, cPedidoVendaCliente, cTransportadoraCliente )
    //Seu código aqui...

Return

Comentários

Uma função bem escrita deve ser autoexplicativa, ou seja, não necessita de muitos comentários. Lembrem-se, se uma função foi possui muitos comentários está na hora de revisa-la.

//RUIM
Static Function bloquearClienteInadimplente( cCodCli, cLojaCli )
    
    //Aqui seleciono a tabela de Clientes
    dbSelectArea("SA1")

    //Seto a ordem dos registros pelo indice 1
    SA1->( dbSetOrder(1) )

    //Posiciono no topo
    SA1->( dbGoTop() )

    //Aqui verifico se o cliente existe na SA1
    If SA1->( msSeek( FWxFilial("SA1") + cCodCli + cLojaCli ) )
        //Aqui altero o cliente para bloqueado
        RecLock("SA1", .F.)
            SA1->A1_MSBLQL := "1"
        SA1->( msUnlock() )
    EndIf

    //Encerro a área aberta
    SA1->( dbCloseArea() )

Return

Agora, vejam essa mesma função sendo reescrita, só que dessa vez utilizando as técnicas do Clean Code:

//BOM

/*/{Protheus.doc} TESTE01

Aqui vem um breve exemplo do que sua rotina faz

@type function
@author Desenvolvedor
@since 07/02/2022
@version P11,P12
@database MSSQL,Oracle

*/
User Function TESTE01()
    Local lClienteEncontrado    := .F.
    Local cCodigoCliente        := ""
    Local cLojaCliente          := ""

    dbSelectArea("SA1")

    lClienteEncontrado := verificarClienteExiste( cCodigoCliente, cLojaCliente )

    If lClienteEncontrado
        bloquearClienteInadimplente()
    EndIf

    SA1->( dbCloseArea() )
Return

/*
    Verifica se o cliente existe na tabela
*/
Static Function verificarClienteExiste( cCodigoCliente, cLojaCliente )
    Local lClienteEncontrado := .F.

    SA1->( dbSetOrder(1) )
    SA1->( dbGoTop() )

    If SA1->( msSeek( FWxFilial("SA1") + cCodigoCliente + cLojaCliente ) )
        lClienteEncontrado := .T.
    EndIf

Return lClienteEncontrado

/*
    Responsável por bloquear o cliente inadimplente encontrado
*/
Static Function bloquearClienteInadimplente()
    
    RecLock("SA1", .F.)
        SA1->A1_MSBLQL := "1"
    SA1->( msUnlock() )

Return

Apesar deste segundo exemplo ter ficado maior, podem notar que as linhas não precisam de comentários como antes, e as funções por si só se comprometem a fazer aquilo que lhes foi determinada.

Uso do Return

Todo programa deve ter apenas um início e um fim, e com o AdvPL não é diferente. Prefira variáveis de controle a inserir Return no meio do código fonte e correr o risco de deixar de executar uma função importante para a rotina. Seu código deve ser linear.

//RUIM
Static Function verificarClienteExiste( cCodigoCliente, cLojaCliente )
    Local lClienteEncontrado := .T.
    
    dbSelectArea("SA1")
    SA1->( dbSetOrder(1) )
    SA1->( dbGoTop() )

    If SA1->( msSeek( FWxFilial("SA1") + cCodigoCliente + cLojaCliente ) )
        Return
    EndIf

    RecLock("SA1", .F.)
        SA1->A1_MSBLQL := "1"
    SA1->( msUnlock() )

    SA1->( dbCloseArea() )

Return

//BOM
Static Function verificarClienteExiste( cCodigoCliente, cLojaCliente )
    Local lClienteEncontrado := .T.

    dbSelectArea("SA1")
    SA1->( dbSetOrder(1) )
    SA1->( dbGoTop() )

    If !SA1->( msSeek( FWxFilial("SA1") + cCodigoCliente + cLojaCliente ) )
        lClienteEncontrado := .F.
    EndIf

    If lClienteEncontrado
        RecLock("SA1", .F.)
            SA1->A1_MSBLQL := "1"
        SA1->( msUnlock() )
    EndIf
    
    SA1->( dbCloseArea() )

Return

Encapsulamento de Funções

É comum realizarmos a chamada de User Function em outros programas, mas concordam comigo que nenhum desenvolvedor merece ter que navegar até essa função para descobrir o que a mesma faz? Para isso recomenda-se o uso do encapsulamento do código, para mascarar de uma maneira mais intuitiva qual sua finalidade

//RUIM
User Function TESTE001()
    Local cTeste := ""

    U_TESTE002()
    
Return
//BOM
User Function TESTE001()
    Local cTeste := ""

    somarDoisNumeros()

Return

Static Function somarDoisNumeros()
    U_TESTE002()

Return

Vejam como nesse segundo exemplo fica mais legível seu código, poupando tempo e esforço de ter que buscar a function em outro arquivo e descobrir sua funcionalidade. Lembrem-se estamos falando em produtividade.

dbSeek x msSeek

Prefira sempre o uso do msSeek ao dbSeek. Conforme orientado pelo próprio TDN, o msSeek garante uma maior performance.

//RUIM
User Function TESTE001()
    Local lClienteEncontrado := .T.

    dbSelectArea("SA1")
    SA1->( dbSetOrder(1) )
    SA1->( dbGoTop() )

    If !SA1->( dbSeek( FWxFilial("SA1") + cCodigoCliente + cLojaCliente ) )
        lClienteEncontrado := .F.
    EndIf

    If lClienteEncontrado
        RecLock("SA1", .F.)
            SA1->A1_MSBLQL := "1"
        SA1->( msUnlock() )
    EndIf
    
    SA1->( dbCloseArea() )

Return
//BOM
User Function TESTE001()
    Local lClienteEncontrado := .T.

    dbSelectArea("SA1")
    SA1->( dbSetOrder(1) )
    SA1->( dbGoTop() )

    If !SA1->( msSeek( FWxFilial("SA1") + cCodigoCliente + cLojaCliente ) )
        lClienteEncontrado := .F.
    EndIf

    If lClienteEncontrado
        RecLock("SA1", .F.)
            SA1->A1_MSBLQL := "1"
        SA1->( msUnlock() )
    EndIf
    
    SA1->( dbCloseArea() )

Return

Funções DB

Para funções db segue uma série de recomendações:

1 – Para todo dbSelectArea, utilizar um dbCloseArea a fim de evitar estouro de memória devido a quantidade máxima de áreas em aberto (1024).

2 – Evite o uso do dbSelectArea dentro de laços de repetições, pois conforme dito no exemplo anterior, se esquecer de fechar a área em um laço que percorra muitos registros dará estouro de memória.

3 – Para índices personalizados utilizar a função dbNickName ao invés
de dbsetOrder, pois podemos correr o risco de um novo índice ser criado pela Totvs e sobreescrever a posição em que seu índice foi criado, impactando diretamente no código.

4 – Para a utilização de funções db (dbSetOrder, dbGoTop, dbCloseArea, dbSkip etc) utilizar sempre a referência da tabela. Sem isso existe grande chance do Protheus se perder e tentar acessar um campo de outra tabela aberta no mesmo fonte.

5 – Para todo dbSetOrder sempre utilizar em seguida o dbGoTop, para forçar o registro a começar da primeira linha, a fim de evitar problemas com posicionamento.

//RUIM
User Function TESTE001()
    Local lClienteEncontrado := .T.

    dbSelectArea("SA1")
    dbSetOrder(1)

    If msSeek( FWxFilial("SA1") + cCodigoCliente + cLojaCliente )
        lClienteEncontrado := .F.
    EndIf

    While !EoF()
        dbSelectArea("SB1")

        //Seu código aqui
    EndDo

    If lClienteEncontrado
        RecLock("SA1", .F.)
            A1_MSBLQL := "1"
        SA1->( msUnlock() )
    EndIf
    

Return
//BOM
User Function TESTE001()
    Local lClienteEncontrado := .T.

    dbSelectArea("SA1")
    SA1->( dbSetOrder(1) )
    SA1->( dbGoTop() )

    If !SA1->( msSeek( FWxFilial("SA1") + cCodigoCliente + cLojaCliente ) )
        lClienteEncontrado := .F.
    EndIf

    If lClienteEncontrado
        RecLock("SA1", .F.)
            SA1->A1_MSBLQL := "1"
        SA1->( msUnlock() )
    EndIf
    
    SA1->( dbCloseArea() )

Return

Escopo Default

Para variáveis passadas por parâmetro, use e abuse do default, isso te poupará tempo identificando problemas de variáveis que não foram passadas por parâmetro ocasionando em erros de tipagem.

//RUIM
User Function TESTE001( cCodigoFuncionario, nSalarioFuncionario )
    Local lClienteEncontrado    := .T.

    nSalario += 100

Return
//BOM
User Function TESTE001( cCodigoFuncionario, nSalarioFuncionario )
    Local lClienteEncontrado    := .T.
    Default cCodigoFuncionario  := ""
    Default nSalarioFuncionario := 0

    nSalario += 100

Return

Nesse caso, se o desenvolvedor chamar essa função esquecendo de passar o valor para nSalarioFuncionario teremos um erro na rotina, pois ao tentar acrescentar o valor de 100 ao salário o sistema dará problema de tipagem, uma vez que, a variável estará com o tipo Nil.

Declaração de Variáveis

SEMPRE declare uma variável antes de usá-la. Mas não basta declara-la, tem que inseri-la dentro da seção correta. Evite sair declarando variáveis Private no meio do código demasiadamente por preguiça, pois uma hora isso poderá te complicar (declarando uma variável Private em um PE a chance de nomear e alterar o comportamento de uma variável já existente torna-se maior do que imagina). Prefira o uso de variáveis locais e sua utilização por parâmetros via ponteiro (@).

//RUIM
User Function TESTE001( cCodigoFuncionario, nSalarioFuncionario )
    Local lClienteEncontrado    := .T.

    nSalario += 100
    nSalario *= 2

    Private cNome := ""

Return

//BOM
User Function TESTE001( cCodigoFuncionario, nSalarioFuncionario )
    Local lClienteEncontrado    := .T.
    Default cCodigoFuncionario  := ""
    Default nSalarioFuncionario := 0

    nSalario += 100

Return

Evite valores mágicos

Assim como um espectador não é obrigado a saber como é feito um número de mágica, você também não é obrigado a perder horas de análise identificando o que um valor representa. Ao utilizar informações fixas, evite “joga-las” direto no fonte, prefira o uso de constantes com um nome sugestivo, facilitando a análise de outro desenvolvedor. Notem abaixo a diferença na interpretação de um fonte com um valor bem representado para um fonte com um valor simplesmente informado.

//RUIM
User Function TESTE001( cCodigoFuncionario, nSalarioFuncionario )
    Local lClienteEncontrado    := .T.

    nSalario += 100
    nSalario *= 2

    If nSalario >= 100
        conOut("Você atingiu o teto máximo")
    EndIf

Return

//BOM
#DEFINE TETO_MAXIMO_SALARIO     100

User Function TESTE001( cCodigoFuncionario, nSalarioFuncionario )
    Local lClienteEncontrado    := .T.
    Default cCodigoFuncionario  := ""
    Default nSalarioFuncionario := 0

    If nSalario >= TETO_MAXIMO_SALARIO
        conOut("Você atingiu o teto máximo")
    EndIf

Return

Pontos de Entrada

Ao criar um ponto de entrada, evite codificar a regra de negócio dentro do Ponto de Entrada. Para isso deverá ser criada uma User Function e usá-la via ExistBlock e ExecBlock dentro do PE.

//RUIM
User Function MT100TOK()
	Local lRet := PARAMIXB[1]

	conOut("Aqui faz uma coisa")
    conOut("Aqui faz outra coisa")
    
Return
//BOM
User Function MT100TOK()
	Local lRet := PARAMIXB[1]

	If ExistBlock("TESTE001")
		ExecBlock("TESTE001",.F.,.T.)
	Endif

Return

Else

O uso excessivo do Else também pode trazer complexidade desnecessária ao código.

//RUIM
Static Function validarCliente()
    Local lClienteEncontrado := .F.

    If encontrarCliente()
        lClienteEncontrado := .T.
    Else
        lClienteEncontrado := .T.
    EndIf

Return

//BOM
Static Function validarCliente()
    Local lClienteEncontrado := .F.

    If encontrarCliente()
        lClienteEncontrado := .T.
    EndIf

Return

Percebam que nesse exemplo a flag lClienteEncontrado só será verdadeira caso a função encontrarCliente() encontre o cliente efetivamente. Por isso podemos inicializa-la como falso e somente se o cliente for encontrado seu conteúdo será modificado.

Comparativo

No AdvPL temos dois tipos de comparativos: o “=” (igual) e o “==” (exatamente igual). Não recomendo o uso do primeiro pois em alguns casos de comparação de strings podemos ter um falso positivo (parcialmente verdadeiro).

No exemplo acima vimos que ao comparar dois valores, mas com operadores diferentes nosso resultado final também foi divergente? Esse é o caso de um retorno falso positivo. Por isso sempre optem pela utilização do estritamente igual (==) para não acarretar em maiores problemas em seu código e horas de sono perdida.

Bônus

Abaixo algumas alternativas para trazer mais elegância a seu código:

ParaPrefira
xFilialFWxFilial
isInCallStackFWIsInCallStack
cFilEmpFWCodEmp
cFilAntFWCodFil

Bom pessoal, essas foram algumas dicas que utilizo em meus desenvolvimentos, e como dito anteriormente não se trata de uma convenção, mas sim uma forma de elevar a qualidade de suas aplicações.

Espero que tenham gostado e até a próxima!

Deixe um comentário