Delphi Defer

English version of this post

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?

Postpone image 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

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:

Ressalvas

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.

comments powered by Disqus