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:
Para | Prefira |
xFilial | FWxFilial |
isInCallStack | FWIsInCallStack |
cFilEmp | FWCodEmp |
cFilAnt | FWCodFil |
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!