quarta-feira, 30 de dezembro de 2020

Você está fazendo isso errado - Introdução!

Fala galera! Como vocês estão em tempos de pandemia? Espero que bem!

Hoje vou contar um pouco sobre a apresentação "Você está fazendo isso errado!".

Como alguns de vocês já sabem, eu fui instrutor certificado e também consultor Delphi durante muitos anos. Isto me permitiu ver códigos de todos os tipos, foi um período de muito aprendizado, em todos os sentidos possíveis.

Foi neste período que comecei a montar uma pequena lista de erros que desenvolvedores Delphi cometem, pequenos erros, mas com uma frequência grande o suficiente para me chamar a atenção.

Pois bem, separei alguns destes itens, dando prioridade para os que poderiam ser explicados em poucos minutos, e montei uma apresentação que foi rodada a primeira vez ano passado (2019) no Delphi Squad, aqui em Porto Alegre, com o já mencionado título "Você está fazendo isso errado!". Esta apresentação teve bastante repercussão e excelente aceitação.

Pois bem, este ano montei a versão II da apresentação, que foi apresentada na Embarcadero Conference 2020, que também foi muito bem aceita (obrigado comunidade pelas avaliações!).

Ontem, dia 28/12, ainda respondi um e-mail relativo à dúvidas decorrentes da apresentação da Conference, então, decidi aprofundar alguns dos itens (ou todos, vamos ver), aqui no blog também, e quem sabe, atingir mais Delpheiros que, por ventura, ainda praticam estes erros ;D

De imediato, vou começar por falar talvez do item com mais repercussão em todas as vezes que já rodei a primeira edição da apresentação...

#1 A correta relação entre Create x Try x Finally x Free de um objeto!

Uma das primeiras coisas que aprendemos no Delphi é instanciar objetos a partir de classes. Algum breve momento depois, aprendemos que, na maioria dos casos, somos também responsáveis por destruir os objetos que criamos. Depois, um novo elemento é inserido neste aprendizado, que é o fato de que nem sempre tudo acontece segundo o "caminho feliz", e nosso código deve estar preparado para eventuais erros, e aqui me refiro às exceções mesmo, e neste momento, em outras palavras, a destruição destes objetos deve estar protegida quanto à possíveis exceções.

Este é um conhecimento relativamente básico, um desenvolvedor Delphi júnior deve ter este conhecimento, e até aqui tudo bem. O problema surge quando avaliamos as inúmeras variações que os desenvolvedores usam para resolver o cenário acima. Quem me conhece sabe que eu sou contra as receitas definitivas, ou, pior ainda, frases de efeito como "nunca faça isso", "nunca use aquilo", mas, existem alguns casos onde o "nunca" realmente se aplica (vide comando with ;D), e existem também os casos que o desenvolvedor, ao seguir uma receita de bolo, realmente vai resolver 98% dos seus problemas, e os outros 2% passam por entender o que cada dos comandos usados faz na prática, e então o desenvolvedor está livre para escrever a variação que desejar, e este é o caso para construir objetos cujo ciclo de vida é local.

Se o seu objetivo é criar um objeto, usá-lo, e então destruí-lo, esta deveria ser a receita de bolo a ser seguida na grande maioria dos casos:

1
2
3
4
5
6
var lObjeto := TMinhaClasse.Create;
try
  { uso do objeto aqui }
finally
  lObjeto.Free;
end;

Volto a chamar a atenção para o fato de que este código é extremamente simples, muitos desenvolvedores devem estar pensando agora que "é exatamente assim que eu faço" (ótimo!), e tem também a galera que diz "sim, eu faço assim, aaaaahhhh, na realidade eu faço uma coisa aqui ou ali diferente, mas é assim que eu faço!", e aqui entram os códigos que, ou não caracterizam erros, mas poderiam ser melhores, mais otimizados, ou são códigos que realmente podem gerar erros quando a situação sai do caminho feliz.

Vamos para algumas das variações incorretas ou que permitem otimizações:

  • Instanciar o objeto dentro do bloco try: Existem situações em que esta prática realmente não configura erro, mas em se tratando de uso de variáveis locais, que não foram inicializadas antes do try, aí realmente trata-se de uma prática bem ruim.
Lembrem-se que, uma vez dentro de um try protegido por finally, o finally vai ser executado independentemente do que acontecer no try (conhecimento básico de tratamento de exceções, eu sei). O problema todo está na possibilidade de acontecer um erro antes da associação da variável com o objeto criado. "Ah, mas a primeira coisa que eu faço dentro do bloco try é criar o objeto na variável", muitos desenvolvedores esquecem que este processo também é executado, no mínimo, em dois passos, e a associação da variável será sempre o último destes passos. Qual seria o primeiro passo? R: A criação do objeto em si. 
 
Sendo assim, sempre temos que assumir que a própria criação do objeto pode levantar uma exceção, e neste caso, a variável nunca vai receber o objeto criado. Como consequência, a variável local vai continuar apontando para uma área de memória "aleatória", e o resultado do bloco vai ser, muito provavelmente, um Access Violation durante o bloco finally. 
 
Um dos motivos para que os desenvolvedores levem a criação para dentro do bloco try, é justamente pensar que é necessário proteger a destruição do objeto para também no caso do construtor levantar uma exceção: Errado! Caso uma exceção seja disparada e não tratada dentro do construtor, o Delphi SEMPRE vai chamar, automaticamente, o destrutor daquela mesma classe, para então voltar a propagar a exceção, e ainda assim, não ficando objeto vazado na memória. No resumo, é isto aqui que acontece com essa prática:
    1
    2
    3
    4
    5
    6
    try
      lObjeto := TMinhaClasse.Create; // se der exceção aqui...
      { uso do objeto aqui }
    finally
      lObjeto.Free; // provavelmente vai dar AV aqui.
    end;
  • Testar se a variável está associada antes de chamar o Free: Quando eu vejo esta prática, eu sempre pergunto ao autor do código, qual é o nome do destrutor padrão do Delphi? R: Destroy (obviamente). E então, por que temos o hábito de usar Objeto.Free e não Objeto.Destroy? Existe alguma diferença prática entre um e outro? R: Existe!
Ainda me impressiono com a quantidade de Delpheiros que não sabe que o método Free, implicitamente, já faz este teste para nós. Não ficou claro? Esta é uma cópia da implementação do método Free de TObject, retirada do Delphi Sydney:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    procedure TObject.Free;
    begin
    // under ARC, this method isn't actually called since the compiler translates
    // the call to be a mere nil assignment to the instance variable, which then calls _InstClear
    {$IFNDEF AUTOREFCOUNT}
      if Self <> nil then
        Destroy;
    {$ENDIF}
    end;
Então, na prática, testar se o objeto está associado ou não, para então chamar o Free, é redundância de código. Não ficou claro? É mais ou menos isso que o Delphi vai fazer quando você coloca o if antes do Free:
    1
    2
    3
    if ObjetoASerDestruido <> nil then
      if ObjetoASerDestruido <> nil then
        ObjetoASerDestruido.Destroy;
  • Usar FreeAndNil para variáveis que vão ser eliminadas do contexto: Assim como o item anterior, este é um caso menos grave, mas me intriga ver como existem desenvolvedores que se atem às suas raízes, hábitos, muitas vezes paixões (:D) ao estilo e práticas de escrita de código, sem se perguntar o porquê de fazerem isso ou aquilo, simplesmente saem implementando daquela maneira porque aprenderam assim, ou tiveram um problema que aparentemente foi resolvido por aquela prática, e então criam a regra do "sempre faço assim, não me pergunte por que.".
O FreeAndNil é um destes casos, existe a igreja do "Uso FreeAndNil sempre", que tem milhares de devotos mundo afora. E vamos deixar bem claro, eu não estou dizendo que não se deve usar FreeAndNil, eu acho que existem casos e casos, e na grande maioria deles, EU NÃO USO FreeAndNil. Isto se deve ao fato de que a maioria dos objetos que eu preciso destruir manualmente tem ciclos de vida local, e consequentemente, uso também variáveis locais. 
 
Faz mal usar FreeAndNil indiscriminadamente? Depende o que você considera como "mal". Vamos começar por analisar a diferença entre chamar o Free diretamente ou chamar o FreeAndNil. 
 
O Free já discutimos o que faz no item acima, beleza. 
 
Do FreeAndNil, quem nomeou o procedimento foi feliz na tarefa, ele descreve exatamente o que vai acontecer com a variável do tipo objeto que passarmos para ele como parâmetro: resumidamente, vai ser chamado o método Free do objeto apontado pela variável, e então a variável vai ser desassociada (vai apontar para nil). Se analisarmos o código do FreeAndNil, é um código de baixa complexidade, que não envolve a chamada de stack (é um procedimento inline), mas que continua dando mais trabalho ao processador do que um Free daria. 
 
Gosto de fazer uma analogia: você já alugou muitos apartamentos ao longo da vida, sempre que você tem que devolver o apartamento, você sabe que tem que fazer a pintura nova do apartamento, eventuais pequenos reparos, e de tanto fazer isso, você já ficou craque na coisa, toda vez que precisa entregar o apartamento, sai fazendo tudo por impulso, afinal, é simples, sempre fez e deu certo. Então, você recebe uma carta dizendo que você deve deixar o atual apartamento que você aluga, não por pedido do proprietário ou algo assim, mas porque o prédio vai ser implodido. Beleza, você dispara o tradicional script de liberação do apartamento: tira suas coisas, pinta o apartamento, repara o que precisa de reparo, deixa tudo brilhando....   Espeeeeera! Por que raios você vai fazer isso tudo? O apartamento vai ser implodido! Ninguém vai alugar ele depois de você! Concorda? Espero que sim, e sendo assim, bem, o que você faz ao chamar sempre o FreeAndNil é pintar o apartamento toda vez que você desocupa ele, independentemente se vai ser usado por outra pessoa ou se o prédio vai ser implodido. "Ah, é injusta essa comparação, afinal, executar o FreeAndNil é um trabalho simples para o processador.", posso concordar partes com essa frase, mas uma coisa eu tenho certeza pelos vários anos de consultor, é esta falta de preciosismo com o código, de coisas que não mudamos por "sempre fiz assim", preguiça, ou outra desculpa, que nos fazem abrir portas para outras rotinas menos otimizadas ainda onde, por fim, ou impede a migração do mesmo para um cenário onde uma performance melhor seria necessária, como ser reutilizado em um server REST, entre outras situações. Devemos nos preocupar com os grandes gargalos, com os tradicionais grandes erros ao desenvolver um sistema? Não tenho dúvidas, a prioridade é essa. Mas, ao mesmo tempo, continuar escrevendo pequenos códigos que poderiam ser melhores porque "sempre fiz assim", não curto não. ;-)


Enfim, estes foram alguns dos tópicos que demonstrei neste tema do Você está fazendo isso errado". Em um futuro bem próximo vou abordar mais temas da apresentação, bem como podemos falar de outras variações que ainda podem acontecer na relação Create x Try x Finally x Free.


Encerro o post por aqui, desejando que todos tenham crescido de alguma forma com tudo que aconteceu em 2020, e desejando que 2021 seja um super ano para todos, que seja o ano que encerraremos este ciclo triste que estamos vivendo. De coração, muitas felicidades para o ano novo! Até 2021!

Nenhum comentário:

Postar um comentário

Você está fazendo isso errado! Tópico #2

 Fala galera! Como estão todos? Sejam bem-vindos ao primeiro post de 2021. Continuando o tema "Você está fazendo isso errado", va...