segunda-feira, 5 de outubro de 2009

Assertivas - Boas práticas de programação

Olá a todos.

Há alguns dias atrás eu estava corrigindo um daqueles bugs que só ocorrem muito esporadicamente e que você mal sabe como reproduzi-lo, menos ainda o motivo dele estar ocorrendo.
Provavelmente vocês concordarão que na maioria das vezes esse tipo de bug é ocasionado por um descuido durante a programação. E que a correção desse tipo de falha costuma ser um detalhe em uma única linha de código.
Quem nunca gastou horas de depuração para descobrir que usou a variável errada ou o teste errado dentro de, por exemplo, uma condição "if"?
Por essa razão resolvi revisar muitas daquelas boas práticas de programação que costumamos aprender, mas que deixamos de utilizá-las por achar que somos super-programadores ou por qualquer outro motivo.
O uso de assertivas é um assunto bastante velho e não é um recurso exclusivo do Delphi, muito pelo contrário, quase todas as linguagens que tive a oportunidade de aprender oferecem suporte a esse recurso tão simples e útil.

Definição
As assertivas são validações lógicas de uma condição julgada necessária para o correto funcionamento do algoritmo no momento em que são verificadas. A condição testada pela assertiva representa uma pressuposição assumida e instrui o sistema a notificar sempre que essa condição não for satisfeita, garantindo que não haja um comportamento imprevisto.
Asserções no Delphi são implementadas através da rotina Assert. Apesar do IDE informar que esse procedimento faz parte da unit System, você não encontrará seu código fonte porque é uma função interna do compilador.
A sintaxe de uma assertiva, em Delphi, é a seguinte:
Assert(Condition: Boolean; [Message: string]);

O parâmetro "Condition" é uma expressão booleana que representa a condição a ser satisfeita. O parâmetro "Message" é opcional e pode ser utilizado para especificar uma mensagem customizada exibida quando a condição não for satisfeita.
O funcionamento do Assert é bastante simples. A condição especificada pelo parâmetro Condition é validado e, se o resultado for True, o fluxo de execução continua normalmente. Caso contrário, se a condição resultar em False, uma exceção do tipo EAssertFailed será levantada apresentando a mensagem de texto especificada pelo parâmetro "Message".
Caso você não especifique o parâmetro Message, o Delphi irá utilizar por padrão o texto ‘Assertion failure'.
Um exemplo pode ser observado na imagem abaixo:



Por si só a mensagem padrão exibida já é suficientemente útil. Para a maioria das falhas detectada com as assertivas, sabendo-se o local exato no código fonte onde a assertiva encontra-se e obtendo a stack tracing do depurador, é possível identificar e corrigir o bug.

Relevância
Mesmo sendo seu uso muito simples é importante ficar atendo para a finalidade na qual as assertivas se propõem e, assim, evitar que sejam usadas erroneamente.
Tenha em mente que o Assert é uma prática de programação defensiva que instrumenta o código fonte e o instrui a identificar falhas e comportamentos não originalmente previstos e, assim, detectar bugs em seus estágios iniciais.
Apesar de ser na prática bastante usada para validar as entradas de métodos/rotinas, sua proposta não é essa e sim validar condições consideradas obrigatórias e que, quando não satisfeitas, identifica uma falha no sistema.
Muitas vezes a validação de entradas e/ou saídas das rotinas carrega uma pressuposição e, conseqüentemente, passível de uso de assertivas (e recomendável). No entanto, essa sutil diferença de conceito pode fazer a diferença entre incrementar o grau de corretude do seu código fonte ou acrescentar mais bugs.

Versão Interna x Versão de Produção
Uma característica peculiar das assertivas é a possibilidade de "anular" a geração de código binário.
Essa característica é muito útil quando o sistema é buildado e posto em produção, ou seja, quando a intenção é distribuir o software.
Considerando que a finalidade das assertivas é identificar pressuposições não satisfeitas, quando a versão de sistema é considerada estável e, portanto, passível de ser distribuída, a validação dessas condições não são mais relevantes do ponto de vista dos usuários (clientes) e, portanto, não há necessidade que seja gerado código binário.
O Delphi possui um item de configuração específico que permite habilita/desabilita a compilação das assertivas.
Acesso o menu Projet -> Options -> aba Compiler -> grupo Debugging -> opção "Assertions", conforme mostra a imagem abaixo:



Também existe uma alternativa que pode ser utilizada para forçar a compilação das assertivas ou, então, para desabilitá-las em trechos específicos de código fonte.
Nesse caso há as diretivas de compilação {$ASSERTIONS ON / OFF} e {$C + / -}. Com elas você pode delimitar uma região do código fonte que habilitarão ou não a geração de código para as assertivas.

Use Case 1:
Esse é um exemplo muito simples e não tem uma funcionalidade prática, mas servirá para demonstrar o uso desse recurso fantástico.

function TfrmPrincipal.GetCount: Integer;
begin
  Assert(Self.FItems <> nil);

  Result := Self.FItems.Count;
end;

Observe que nossa assertiva está pressupondo que a propriedade FItems não pode ser nil.
É fácil prever que se não houvesse essa assertiva e a propriedade FItems não fosse inicializada, quando tal método fosse chamado o sistema certamente falharia. O que caracteriza uma brecha para um bug se instalar e que pode ser facilmente detectado com o acréscimo do Assert.
Entretanto dizer se essa assertiva está ou não empregada corretamente irá depender da finalidade do método e da sua visibilidade (private, protected, public).
Nos próximos exemplos essa questão será abordada.

Use Case 2:
Vamos imaginar um programa simples que disponibiliza ao cliente dois campos de edição para valores numéricos e, ao pressionar um botão, é apresentado um dialog com o valor da divisão entre os valores desses dois campos.
Sabemos que a divisão por zero ocasiona uma exceção (EDivByZero) e necessita ser tratado.

function Dividir(const Dividento, Divisor: Integer): Double;
begin
  Assert(Divisor <> 0);

  Result := Dividento / Divisor;
end;

procedure TfrmPrincipal.btnResultadoClick(Sender: TObject);
var
  lResultado: Double;
begin
  lResultado := Dividir(sedtDividento.Value, sedtDivisor.Value);

  ShowMessage('Resultado da divisão: ' + FloatToStr(lResultado));
end;

Quando o botão é pressionado, o evento “btnResultadoClick” é gerado. Este chama a rotina “Dividir” repassando os valores dos dois campos e apresentando um resultado em um dialog.
No método Dividir há uma assertiva que pressupõem que o valor do parâmetro Divisor não seja 0 (zero).
Esse é um exemplo do emprego incorreto do Assert.
No momento em que a compilação de assertivas for desabilitada, a possibilidade de falha seria alta, principalmente porque os valores da divisão são fornecidos diretamente pelo cliente.
A visibilidade da rotina “Dividir” vai além de simplesmente ser usada internamente. Além disso, as assertivas não devem ser utilizadas para validar regras de negócio e, portanto, seu emprego no exemplo acima deve ser revisto.

Use Case 3:
O exemplo abaixo apresenta uma situação bastante problemática e que certamente resultará em dores de cabeça no momento em que o sistema for posto em produção.

procedure TfrmPrincipal.AddItem(Item: TObject);
begin
  Assert(Self.FItems.Add(Item) >= 0);
end;

O método “AddItem” tem a finalidade de acrescentar um objeto, passado por parâmetro, a uma lista interna. E há uma pressuposição de que esse item sempre será acrescentado à lista.
Mas aí eu questiono: O que acontecerá se a compilação dos Assertions for desabilitada?
A resposta é muito simples, o sistema falhará. E pior, a falha pode ocorrer em um ponto do código que aparentemente não apresenta relação com o verdadeiro causador do bug.
Então você houve aquela famosa frase: “Sim, mas eu testei e estava funcionando perfeitamente!”

Use Case 4:
Na maioria das vezes o Assert é utilizado no início de métodos/rotinas que exigem a obrigatoriedade de parâmetros, faixa de valores específicos, recursos alocados, variáveis ou objetos inicializados ou qualquer outra condição necessária para o correto funcionamento desse método/rotina. Mas como dito no início desse artigo, o uso de Assert é uma prática de programação defensiva.
Imaginemos uma situação onde o sistema realize um cálculo relativamente complexo e que, baseando-se nos parâmetros fornecidos, o resultado nunca deverá ultrapassar uma faixa de valores específica.

procedure TfrmPrincipal.btnResultadoClick(Sender: TObject);
var
  lResultado: Integer;
begin
  lResultado := Calcular(Param1, Param2, Param3, Param4);
  Assert(lResultado <= 80, 'Resultado fora da faixa permitida.');

  // ...
end;

A assertiva aqui é de grande valia e possibilita identificar uma situação falha, seja ela porque os valores dos parâmetros fornecidos não estavam corretos ou porque a função “Calcular” não está realizando o cálculo de forma correta. Independente do motivo, o Assert garante que a inconsistência não seja levada adiante na execução do sistema.

Use Case 5:
Esse próximo exemplo é um pouco semelhante ao caso acima, mas é uma situação comum.
Imagine que o sistema mantenha uma lista dos componentes relacionados com uma funcionalidade qualquer. A referência desses componentes são armazenadas em um objeto TList e em um dado momento é necessário obter o índice de um desses componente dentro da lista.

procedure TfrmPrincipal.btnResultadoClick(Sender: TObject);
var
  lResultado: Integer;
begin
  lResultado := Self.FItems.IndexOf(Self.FSelected);
  Assert(lResultado >= 0, 'Componente não encontrado.');

  // ...
end;

Aqui há a pressuposição de que o componente selecionado deve obrigatoriamente ter usa referência armazenada em um dos itens da lista. Caso isso não ocorra, há o indício de um bug.
Novamente temos um exemplo de programação defensiva que pode evitar dores de cabeça.

Use Case 6:
A situação abaixo não é muito comum e, particularmente, nunca li qualquer relato, mas é uma prática que costumo utilizar quando estou implementando eventos de um formulário.
Vamos supor que um dado sistema deva possuir uma tela de cadastro de clientes, e uma das informações requeridas pelo cadastro é o estado civil.
Então, durante a fase de construção do formulário, você acrescente dois componentes TRadioButtom. Um para representar o estado civil ‘Solteiro’ e o outro para representar o estado civil ‘Casado’.
O sistema determina que se o cliente não for solteiro, alguns campos devem ser habilitados e preenchidos durante o cadastro.
Então você resolve implementar essa regra de negócio da seguinte forma:

procedure TfrmPrincipal.rdbEstadoCivilClick(Sender: TObject); begin
  if Sender = rdbCasado then
  begin
    tabConjugue.Visible := True;
    // Outras tarefas
  end
  else
    tabConjugue.Visible := False;
end;

Ou seja, você compartilha o mesmo evento OnClick com os dois componentes TRadioButtom, tornando visível uma parte do formulário caso o componente rdbCasado esteja marcado.
Depois de um tempo você julga necessário acrescentar outro estado civil (Viúvo) e, então, insere um novo TRadioButtom e associa o mesmo método ao evento OnClick deste novo componente.
Se você esquecer-se de modificar o código prevendo essa nova condição, o sistema não irá falhar e, dependendo do caso, pode ser facilmente detectado em testes funcionais. Entretanto, o uso de assertivas aqui pode economizar um tempo considerável e tornar a vida da equipe de testes mais tranqüila.

procedure TfrmPrincipal.rdbEstadoCivilClick(Sender: TObject); begin
  Assert((Sender = rdbCasado) or (Sender = rdbSolteiro));

  if Sender = rdbCasado then
  begin
    tabConjugue.Visible := True;
    // Outras tarefas
  end
  else
    tabConjugue.Visible := False;
end;

Aqui o Assert está pressupondo que esse evento só será gerado pelos componentes “rdbCasado” e “rdbSolteiro”.
Assim, se outro componente fosse associado com esse método, a assertiva irá lembrar-te de que algo está faltando.

Use Case 7:
A criação de componentes ou de classes persistentes (que herdam de TPersistent) exige a implementação do método Assign, cuja finalidade é copiar os dados de um outro objeto de mesmo tipo (ou não) para as respectivas propriedades.
Durante a fase de criação dessas classes é muito comum ocorrer constantes modificações na lista de propriedades, o que exige a atualização do método Assign. No entanto, muitas vezes você não atualiza o método Assign imediatamente após ter acrescentado novas propriedades, deixando para um momento mais apropriado.
Então outras tarefas surgem e os métodos de importação/exportação ficam desatualizados.
Vamos analisar o exemplo abaixo:

TTeste = class(TPersistent)   private
    FID: integer;
    FNome: string;

  public
    property ID:   integer read FID;
    property Nome: string  read FNome write FNome;

    procedure AssignTo(Dest: TPersistent); override;
end;

implementation

procedure TTeste.AssignTo(Dest: TPersistent);
begin
  Assert(Dest <> nil);
  Assert(Dest.inheritsFrom(TTeste));
  Assert(Dest.InstanceSize = 16, ‘A classe TTeste foi modificada, revise o método Tteste.AssignTo()!’);

  TTeste(Dest).FID   := Self.FID;
  TTeste(Dest).FNome := Self.FNome;
end;

Observe o Assert que está em cor vermelha. Se uma nova propriedade fosse acrescentada à classe, o Assert notificaria que o método Assign está desatualizado (logicamente quando ele fosse chamado).
É claro que fazer uma validação utilizando o InstanceSize da classe não irá identificar todos os tipos de mudanças. Você poderia, por exemplo, modificar o tipo da propriedade “FID” para ShortInt e acrescentar outra propriedade, também ShortInt, que e o tamanho da classe permaneceria os mesmos 16 bytes.
Mesmo assim, a assertiva é de grande ajuda. Principalmente em caso onde há dependência de classes.
Métodos de exportação e importação também são exemplos semelhantes onde esse mesmo caso se aplica.

Customizando o Assert
Até agora vimos diversas situações que podemos empregar o uso do Assert para identificar falhas ou nos ajudar a revisar trechos de código fonte que são dependentes de alguma recurso (classe, record, ...).
Para a maioria das necessidades dos desenvolvedores/testers, o uso padrão do comado Assert já é suficiente, mas este comando ainda guarda um recurso muito interessante, que é a sua customização.
Sempre que um asserção é levantada, o Delphi executa a rotina referenciada pela variável AssertErrorProc.
A variável AssertErrorProc está localizada na unit System e é, na verdade, um ponteiro para uma função com a seguinte assinatura:

procedure (const Message, Filename: string; LineNumber: Integer; ErrorAddr: Pointer);

Isso significa que você pode escrever o seu próprio tratador de Assertivas bastando. escrever uma rotina com a mesma assinatura e atribui-la para a variável global AssertErrorProc.

Exemplo:
procedure MeuTratador(const Message, Filename: string; LineNumber: integer; ErrorAddr: Pointer);
begin
   // Seu código fonte.
end;

AssertErrorProc := MeuTratador;

Talvez você se pergunte: “Mas para que eu necessitaria implementar um tratador de assertiva?”
Há algumas situações que podem ser muito úteis como por exemplo o envio de um email de notificação para o time desenvolvimento; manter um log de falhas em arquivo; automatizar alguma ação relacionada a testes; abrir um ocorrência de falhas em um sistema externo;...

Um comentário:

jcfaria disse...

Obrigado: muito bom!