Delphi Defer
Há algumas semanas eu estava refatorando o framework “Foundation 4 Delphi”, pois precisava extender o TComponent.TRecursiveEnumerator para utilizar em outras classes, era uma implementação antiga e resolvi reescrever com generics e aproveitar a vantagem dos smart records para simplificar a integração com o meu TComponentHelper class helper.
Na implementação do record, precisei criar um field “IInterface” apenas para poder simular um destructor no record, foi quando eu pensei como seria bom se tivessemos o Defer do Golang. Quando estava escrevendo os testes unitários, novamente senti a necessidade do Defer, foi quando decidi pensar mais no padrão e comportamento do Defer, o que me levou a tentar implementar em Delphi, apenas para um exercício de lógica, mesmo inicialmente achando que não seria algo útil para produção, mas para a minha surpresa, consegui algo muito útil.
O que é Defer?
Defer define o padrão “adiar a execução de uma procedure”, este adiamento deve agendar a execução de uma “procedure: TProc” para após o fim do método que executou a chamada à função Defer(Proc: TProc).
No final do método, o Defer deve executar todas as procedures empilhadas na ordem inversa ao agendamento.
Defer de acordo com a documentação da Golang:
A declaração defer agenda a chamada da função (a função adiada) para ser executada imediatamente antes da função executando o defer retornar. É uma maneira incomum, mas eficiente de lidar com situações em que recursos devem ser liberados independentes do fluxo que a função executar até retornar. Os exemplos regulares são desbloquear um mutex ou fechar um arquivo.
Uma informação importante aqui, é que Golang é uma linguagem com garbage collection, então não é necessário destruir os objetos criados, apenas desalocar recursos.
E independente do fluxo executado na função o Defer sempre deve executar os métodos já agendados, então se houver um Exit antes do final ou se ocorrer uma exceção, todos os métodos agendados devem ser executados.
Nas minhas pesquisas, descobri que o Defer foi implementado para Swift da Apple, na versão 2.0. Você pode ler mais sobre isto neste artigo com um título legal, try/finally feito corretamente.:
Também li em uma discussão no reddit que este recurso existe na linguagem D, mas não encontrei referências.
Outras utilidades para o Defer
- Fechar conexão com banco de dados, transação ou query
- Finalizar conexão de socket
- Fechar arquivos abertos
- Desbloquear critical section
A Implementação
Após entender como o Defer dever funcionar, eu o implementei utilizando Interfaces, assim seria automaticamente desalocado quando sair de contexto, na instrução “end” do método que originou a chamada.
IDeferrer = Interface(IInterface) function Defer(Proc: TProc): IDeferrer; end;
Eu segui o padrão adotado em Golang para nomear interfaces, adicionando o sufixo “er”. Este padrão difere do que normalmente encontramos em Delphi, que utiliza o sufixo “able”, assim teriamos “Deferrable”.
Internamente, ele deve manter uma pilha com a lista de TProc agendadas, para que estes métodos sejam executados em ordem inversa quando o objeto TDefer for destruído, sem problemas pois Delphi tem uma implementação de TStack<T> em System.Generics.Collections, eu só tive de decorar o TProc com um record TStackItem antes de armazenar.
type TDeferrer = class(TInterfacedObject, IDeferrer) strict private type TStackItem = record Proc: TProc; end; strict private FStack: TStack< TStackItem >; procedure Push(Proc: TProc); procedure ProcessStack; public constructor Create; overload; constructor Create(Proc: TProc); overload; destructor Destroy; override; function Defer(Proc: TProc): IDeferrer; end;
E para finalizar, criei uma função global que retorna a instância criada já armazenando o primeiro método:
function Defer(Proc: TProc): IDeferrer; begin Result := TDeferrer.Create(Proc); end;
Como usar
O objetivo do Defer é adiar métodos, não gerenciar o tempo de vida de objetos, mas como em Delphi Win32/Win64 os objetos são gerenciados manualmente, podemos utilizar o Defer para destruir os objetos, além de finalizar outros recursos.
uses Foundation.System; var Database: TDatabase; Exec: IDeferrer; Query: TQuery; Transaction: TTransaction; begin Database := TDatabase.Create(FWriter); Exec := Defer(Database.Free); Database.Open('foundation-db'); Exec.Defer(Database.Close); Transaction := Database.StartTransaction; Exec.Defer(Transaction.Free); Exec.Defer(Transaction.Commit); Query := Transaction.Query; Exec.Defer(Query.Free); if Query.Open('select value from table') then begin Exec.Defer(Query.Close); end; end;
Este código é parte dos testes unitários, os objetos são apenas Mock, o objeto FWriter passado no construtor é utilizado para gravar as etapas para poder verificar o resultado, esta foi a forma que encontrei para testar o Defer, pois ele só executará os métodos quando estiver sendo destruído.
Este é o log do do teste unitário usado para verificar o resultado:
TDatabase.Open('foundation-db') TDatabase.StartTransaction TTransaction.Query TQuery.Create TQuery.Open('select value from table') // A Execução do Defer inicia aqui TQuery.Close TQuery.Free TTransaction.Commit TTransaction.Free TDatabase.Close TDatabase.Free
Note que o Defer acontece apenas depois de TQuery.Open, exatamente na ordem inversa em que os métodos foram agendados.
Outro detalhe é a declaração da variável “Exec: IDeferrer” para reutilizar a mesma instância. Declarando uma variável e reutilizando nas próximas chamadas somente uma instância é alocada, algo que pode ser dispensado para o benefício de ter o código mais limpo com uma pequena sobrecarga.
Se a referência do IDeferrer não for armazenada em uma variável local, uma nova instância será criada a cada chamada ao Defer. Seria um problema se a ordem em que as instâncias são desalocadas não fosse a mesma do Defer, mas o comportamento foi consistente em todos os testes. Acredito ser o comportamento padrão do gerenciador de memória do Delphi, o FastMM, armazenar as instâncias alocadas em uma pilha, desta forma a ordem para desalocar é a mesma da implementação do Defer. Se o gerenciador de memória do seu projeto for outro como ScaleMM ou Nexus Memory Manager sugiro fazer testes antes de usar o Defer, embora eu acredito que este seja o comportamento padrão para a desalocação de recursos.
Uma implementação alternativa
Na pasta Experimental do repositório tem uma implementação alternativa que estende o Defer na unit Foundation.Pattern.Defer.Auto.
Esta implementação captura a Thread e o ponteiro do método que chamou o Defer e as utiliza como chave para salvar a instância do Defer em um TDictionary<string, IDeferrer> para reutilizar sempre a mesma instância. Funcionou em todos os testes executados em win32, win64, single e multi thread.
Para capturar o ponteiro do método que iniciou a chamada utilizei a unit JclDebug do projeto Jedi Jcl. Para win32 foi possível extrair o código necessário e colocar na unit Foundation.Vendor.JclDebug, com os devidos créditos e licença do autor, dispensando a necessidade de instalar a Jcl completa no ambiente de testes. Para win64 é necessário utilizar a unit JclDebug, também recomendada para ambiente de produção.
A vantagem é clara, apenas uma instância de Defer é criada para cada método, por outro lado a sobrecarga gerada pela implementação da lista com TDictionary, o mutex necessário para garantir o bom comportamento em multi thread e a dependência da Jcl, são maiores do que declarar uma variável ou ter mais de uma instância por método de origem. Tendo em mente que em uma aplicação, normalmente não haverá muitas instâncias ao mesmo tempo.
Mais exemplos
Métodos Anônimos
Database := TDatabase.Create(FWriter); Database.Open('database-name'); Defer( procedure begin Database.Close; Database.Free; end );
Criar log com um tracer
class function TTrace.Method(Writer: TStringsWriter; const MethodName: string; var TraceProc: ITracer): IDeferrer; var {! Workaround "Defer(ITracer.Exit)" : E2010 Incompatible types: 'TProc' and 'procedure, untyped pointer or untyped parameter' } Trace: TTrace; begin Trace := TTrace.Create(Writer); Trace.Enter(MethodName); Result := Defer(Trace.Exit); Supports(Trace, ITracer, TraceProc) end; var Trace: ITracer; begin TTrace.Method(FWriter, 'DelegateDeferToTraceExecute', Trace); Trace.Step('First'); Trace.Step('Second'); Trace.Step('Third'); end;
Que gerou o log:
> Enter DelegateDeferToTraceExecute 1. DelegateDeferToTraceExecute: First 2. DelegateDeferToTraceExecute: Second 3. DelegateDeferToTraceExecute: Third < Exit DelegateDeferToTraceExecute
Finalização condicional com método anonimos
Defer( procedure( if Datatabase.InTransaction then begin if Object.IsValid then Datatabase.Commit else Datatabase.RollBack; end; ) );
Manipulando excessões
Defer( procedure( try Connection.Close; except on E: Exception do Log.Write(E); end; Connection.Free; ) );
Utilizando o Defer como destructor para o record
type TValueType< T > = record private FAnyObject: TAnyObject; FDeferrer: IDeferrer; FValue: T; public constructor Create(Value: T); end; constructor TValueType< T >.Create(Value: T); begin FValue := Value; FAnyObject := TAnyObject.Create FDeferrer := Defer(FAnyObject.Free); end;
Vantagens
Adiar a execução de procedures tem algumas vantagens:
Garante que você não vai esquecer de finalizar um recurso, um erro bem comum quando é feito a manutenção do código e novas condições são adicionadas, criando um novo fluxo.
O código de finalização fica junto ao de inicialização, que é mais legível do que colocar no fim do método;
Dispensa a necessidade de blocos try/finally para garantir que um recurso seja finalizado.
Mesmo que ocorra uma excessão, todos os métodos agendados no Defer serão executados.
Ressalvas
O defer adiciona uma pequena sobrecarga, pois um objeto é criado e a sua finalização irá acumular a execução das procedures, código que seria executado de qualquer forma. Mas eu considero que se não se tratar de um código de alta performance, a utilização traz mais vantagens que desvantagens.
Evite usar defer dentro de loops, se for necessário alocar e desalocar recursos dentro de um loop, a desalocação deve ser determinística. O uso excessivo pode aumentar muito o consumo de memória e causar lentidão.
Ao usar com métodos anônimos tenha em mente que o estado é capturado, qualquer variável utilizada, terá o valor capturado no momento em que o defer for declarado e não o valor alterado durante o fluxo da procedure. Leia mais em Understanding Anonymous Methods
Este projeto foi inspirado na function Defer da Golang e não tem relação com os padrões “Deferred/Promise” e “Deferred Choice”.
Repositório
O projeto está hospedado no github, eu farei outro post apenas para falar do framework e sobre o que estou preparando para ele. Muita coisa está pronta e em produção, então aguardem notícias em breve.