[Delphi] Validando propriedades de uma classe com RTTI

[Delphi] Validando propriedades de uma classe com RTTI

Olá, pessoal, como estão?
Recebi uma dúvida bem interessante dos leitores Cassiano e Jean Alysson no artigo sobre Abordando o Encapsulamento, envolvendo validações das propriedades de uma classe. Para aproveitar o momento, decidi suspender brevemente a série de artigos sobre Design Patterns para discutir sobre essa dúvida.
O maior propósito deste artigo é abordar a utilização de RTTI. Já conhece com esse recurso fantástico do Delphi?

 

Uma das recomendações mais importantes do Clean Code é evitar o número excessivo de parâmetros em um método, conforme já comentei na segunda parte do artigo sobre a relevância da expressividade no código. Resumindo a recomendação, quando observa-se que o número de parâmetros de um método cresce para 4 ou 5 elementos, é hora de parar, analisar, e criar um objeto para armazenar estes valores, utilizando-o como um parâmetro único.
A dúvida dos leitores Cassiano e Jean envolve este cenário. Eles modelaram uma classe com várias propriedades e utilizam uma instância dela como parâmetro, porém, precisam validar cada uma dessas propriedades nos métodos que recebem essa instância, verificando se estão devidamente preenchidas.
Para compreender essa situação, tome a seguinte classe como exemplo:

TBoleto = class
  private
    FDescDoc: string;
    FCodigoBanco: integer;
    FValorEfetivo: real;
    FDataVenc: TDateTime;
 
    procedure ValidarDados;
  published
    property DescDoc: string read FDescDoc write FDescDoc;
    property CodigoBanco: integer read FCodigoBanco write FCodigoBanco;
    property ValorEfetivo: real read FValorEfetivo write FValorEfetivo;
    property DataVenc: TDateTime read FDataVenc write FDataVenc;
  public
    procedure Salvar;
  end;

O único método público, “Salvar”, invoca o método privado “ValidarDados” que, como o próprio nome infere, valida se as propriedades possuem valores válidos. É sobre este método que discutiremos.
Em primeiro momento, poderíamos pensar na solução abaixo para codificar o método “ValidarDados”:

procedure TBoleto.ValidarDados;
begin
  if FDescDoc = EmptyStr then
  begin
    ShowMessage('Valor não preenchido: Descrição');
    Abort;
  end;
 
  if FCodigoBanco = 0 then
  begin
    ShowMessage('Valor não preenchido: Código do banco');
    Abort;
  end;
 
  if FValorEfetivo <= 0 then
  begin
    ShowMessage('Valor não preenchido: Valor');
    Abort;
  end;
 
  if DataVenc = 0 then
  begin
    ShowMessage('Valor não preenchido: Data de Vencimento');
    Abort;
  end;
end;

Embora funcional, não é um código nada agradável. Eu, particularmente, considero essa sequência de condições IF como repetição de código. Imagine, por exemplo, uma classe com 20 propriedades. Teríamos 20 condições IF ocupando aproximadamente 100 linhas de código em um único método. Essa quantidade de linhas, por sua vez, indicaria que o método está ferindo o princípio de Single Responsibility e deveria ser “fragmentado” em várias validações. No entanto, neste caso, teríamos 20 funções exclusivas de validação. Também não ficaria bom.

 

Para aprimorar este código, faremos uso de um recurso do Delphi chamado RTTI (Run-time Type Information), que nos permite acessar as propriedades de um objeto em tempo de execução, obtendo seus respectivos valores.
Notaram que coloquei todas as properties com visibilidade published? Essa configuração na classe é necessária para que o RTTI consiga “enxergar” as propriedades. Veja, abaixo, a mesma regra de validação após empregar este recurso:

uses
  TypInfo;
 
procedure TBoleto.ValidarDados;
var
  ListaPropriedades: TPropList;
  Contador: integer;
  Preenchido: boolean;
  Valor: variant;
begin
  // Preenche uma lista com as propriedades do objeto
  GetPropList(TypeInfo(TBoleto), tkAny, @ListaPropriedades);
 
  // Executa um loop nas propriedades do objeto
  for Contador := Low(ListaPropriedades) to High(ListaPropriedades) do
  begin
    // Obtém o valor da propriedade
    Valor := GetPropValue(Self, ListaPropriedades[Contador]^.Name);
 
    // Valida o valor, exigindo que seja diferente de 0 e diferente de vazio
    Preenchido := (VarToStr(Valor) <> EmptyStr) and (VarToStr(Valor) <> '0');
 
    // Se não estiver preenchido, exibe uma mensagem indicando o nome da propriedade
    if not Preenchido then
    begin
      ShowMessage('Valor não preenchido: ' + ListaPropriedades[Contador]^.Name);
      Abort;
    end
  end;
end;

Com esse código, as validações serão executadas para todas as propriedades que um objeto possui, independentemente da quantidade. Logo, novas propriedades adicionadas à classe serão automaticamente consideradas na validação sem a necessidade de alterar o código. Atente-se aos métodos GetPropList (para preencher a lista de propriedades) e GetPropValue (para obter o valor de uma propriedade), que são essenciais para trabalhar com RTTI.

 

Apesar de já funcionar como desejamos, um pequeno problema surge com essa instrução:

ShowMessage('Valor não preenchido: ' + ListaPropriedades[Contador]^.Name);

O valor de Name sempre será o nome literal da propriedade. Portanto, caso a data de vencimento não esteja preenchida, essa será a mensagem retornada para o usuário:

Valor não preenchido: DataVenc

O mesmo acontece para a descrição do documento:

Valor não preenchido: DescDoc

Podemos alterar o nome das propriedades para “DataVencimento” e “DescricaoDocumento”, mas não é possível adicionar espaços ou caracteres especiais. De qualquer forma, a mensagem ficará “estranha” aos olhos de quem utiliza o sistema ou, na melhor das hipóteses, apenas cômico. Precisaríamos, então, de algum recurso para indicar que aquela propriedade possui uma descrição específica e acessá-la durante as validações.
Pois bem, no Delphi, essa demanda é preenchida com um recurso conhecido como Custom Attributes, que possibilita incorporar “características” à propriedades para depois acessá-las via RTTI. Adicionaremos, então, uma descrição mais clara para cada propriedade, resultando em mensagens mais amigáveis.
Para criar um atributo, seguimos o mesmo padrão de criação de uma classe, salvo a exceção de que essa estrutura deve obrigatoriamente herdar de TCustomAttribute:

type
  TDescricao = class(TCustomAttribute)
  private
    FDescricao: string;
  public
    constructor Create(const Descricao: string);
    property Descricao: string read FDescricao;
  end;

Estrutura bem simples. O construtor do atributo recebe um parâmetro que será atribuído a um membro privado:

{ TDescricao }
 
constructor TDescricao.Create(const Descricao: string);
begin
  FDescricao := Descricao;
end;

O próximo passo é adicionar o atributo na linha superior de cada propriedade, informando a descrição como parâmetro:

published
  [TDescricao('Descrição do documento')]
  property DescDoc: string read FDescDoc write FDescDoc;
 
  [TDescricao('Código da instituição bancária')]
  property CodigoBanco: integer read FCodigoBanco write FCodigoBanco;
 
  [TDescricao('Valor do boleto')]
  property ValorEfetivo: real read FValorEfetivo write FValorEfetivo;
 
  [TDescricao('Data de vencimento do boleto')]
  property DataVenc: TDateTime read FDataVenc write FDataVenc;

O terceiro passo é tirar proveito dos recursos avançados de RTTI, consumindo classes específicas para acessar as informações que desejamos. Essas classes, bem como o conceito de Custom Attributes, estão disponíveis desde a versão 2010 do Delphi.
Confira as alterações no método “ValidarDados”:

procedure TBoleto.ValidarDados;
var
  Contexto: TRttiContext;
  Tipo: TRttiType;
  Propriedade: TRttiProperty;
  Atributo: TCustomAttribute;
  Valor: variant;
  Preenchido: boolean;
begin
  // Cria o contexto do RTTI
  Contexto := TRttiContext.Create;
 
  // Obtém as informações de RTTI da classe TBoleto
  Tipo := Contexto.GetType(TBoleto.ClassInfo);
 
  // Executa um loop nas propriedades do objeto
  for Propriedade in Tipo.GetProperties do
  begin
    // Obtém o valor da propriedade
    Valor := Propriedade.GetValue(Self).AsVariant;
 
    // Valida o valor, exigindo que seja diferente de 0 e diferente de vazio
    Preenchido := (VarToStr(Valor) <> EmptyStr) and (VarToStr(Valor) <> '0');
 
    // Se não estiver preenchido, entra no loop dos atributos
    if not Preenchido then
 
      // Executa um loop nos atributos da propriedade
      for Atributo in Propriedade.GetAttributes do
 
        // Verifica se o atributo é do tipo TDescricao
        if Atributo is TDescricao then
        begin
          // Exibe a mensagem com a descrição que definimos para a propriedade
          ShowMessage('Valor não preenchido: ' + (Atributo as TDescricao).Descricao);
          Abort;
        end;
  end;
end;

As classes que iniciam com “TRtti” – como TRttiContext, TRttiType e TRttiProperty – fazer parte da unit RTTI.pas e são exclusivas para trabalhar com acesso às informações de uma classe em tempo de execução. Veja que, quando o valor de uma propriedade não está preenchido, utilizamos o TRttiType.GetAttributes para ler os atributos daquela propriedade e acessar o texto que configuramos através do nosso Custom Attribute, ou melhor, TDescricao. Observe também que o código espera que uma propriedade possa ter vários atributos – por isso executamos um loop em GetAttributes.
Um detalhe muito importante é que, com essa técnica, as properties não precisam obrigatoriamente ser published. Na verdade, se mantermos elas com essa visibilidade, o compilador levantará o seguinte Warning:

W1055 PUBLISHED caused RTTI ($M+) to be added to type 'TBoleto'

Configure-as com visibilidade public para evitar este aviso do compilador.

 

Bom, entendo que agora já temos a solução, mas gostaria de ir um pouco mais além com o Custom Attributes.
Criaremos um tipo de atributo, chamado TValidador, que possui uma função própria de validação, recebendo a propriedade (do tipo TRttiProperty) como parâmetro.

type
  TValidador = class(TCustomAttribute)
  private
    FDescricao: string;
  public
    constructor Create(const Descricao: string);
    property Descricao: string read FDescricao;
 
    // Função própria para validação do valor
    function ValidarValor(Propriedade: TRttiProperty; Objeto: TObject): boolean;
  end;
 
implementation
 
{ TValidador }
 
constructor TValidador.Create(const Descricao: string);
begin
  FDescricao := Descricao;
end;
 
function TValidador.ValidarValor(Propriedade: TRttiProperty; Objeto: TObject): boolean;
var
  Valor: variant;
begin
  // Obtém o valor da propriedade
  Valor := Propriedade.GetValue(Objeto).AsVariant;
 
  // Valida o valor, exigindo que seja diferente de 0 e diferente de vazio
  result := (VarToStr(Valor) <> EmptyStr) and (VarToStr(Valor) <> '0');
end;

Já que movemos a validação para o próprio atributo, o método “ValidarDados” naturalmente ficará menor:

procedure TBoleto.ValidarDados;
var
  Contexto: TRttiContext;
  Tipo: TRttiType;
  Propriedade: TRttiProperty;
  Atributo: TCustomAttribute;
begin
  // Cria o contexto do RTTI
  Contexto := TRttiContext.Create;
 
  // Obtém as informações de RTTI da classe TBoleto
  Tipo := Contexto.GetType(TBoleto.ClassInfo);
 
  // Executa um loop nas propriedades do objeto
  for Propriedade in Tipo.GetProperties do
  begin
    // Executa um loop nos atributos da propriedade
    for Atributo in Propriedade.GetAttributes do
 
      // Verifica se o atributo é do tipo TDescricao
      if Atributo is TValidador then
 
        // Chama o método de validação do próprio atributo
        if not TValidador(Atributo).ValidarValor(Propriedade, Self) then
        begin
          // Exibe a mensagem com a descrição que definimos para a propriedade
          ShowMessage('Valor não preenchido: ' + (Atributo as TValidador).Descricao);
          Abort;
        end;
  end;
end;

 

Isso aí pessoal! Apresentei 4 formas de validar o conteúdo das propriedades de uma classe, mas, claro, recomendo as duas últimas, principalmente pela possibilidade de compartilhar este código com outras classes da arquitetura, inclusive usando Generics!
Vale destacar que o exemplo deste artigo é apenas uma parcela do que podemos alcançar com o RTTI. Aproveite e acesse o Wiki da Embarcadero na seção Working with RTTI e conheça mais sobre este recurso!

 

Grande abraço!


Compartilhe!
Share on FacebookTweet about this on TwitterShare on LinkedInShare on Google+Pin on PinterestEmail this to someone

19 comentários

    1. Thank you so much, Mr. Daniele! It’s an outstanding pleasure to receive a comment from you in my blog post!
      I really admire your work and your expertise in Delphi!
      I will take a look at the project you mentioned! 🙂

  1. Boa Tarde. O atributo TValidador substitui o TDescricao na declaração das propriedades? Isso não ficou bem claro pra mim no artigo. Poderia me ajudar pois estou fazendo testes com suas dicas. Obrigado.

    1. Olá, Pedro, boa noite!
      Tem razão. Eu deveria ter deixado isso mais claro mesmo. Peço desculpas!
      Exatamente. Na última parte do artigo, substituímos TDescricao por TValidador, mas nada impede que você tenha vários atributos para uma propriedade só. Por exemplo:

      [TValidador('Descrição do documento')]
      [TTamanhoMimimo(20)]
      [TFormatadorCaixaMista]
      property DescDoc: string read FDescDoc write FDescDoc;

      Abraço!

  2. Parabéns em compartilhar conhecimentos, isso só te engrandece como ser humano. Com atitudes como essa poderemos ter um mundo muito melhor.
    Para um Pais imenso existem muitas pessoas que te agradecem pela sua atitude em compartilhar conhecimentos.
    Meu muito Obrigado.

    1. Eu que agradeço pelas palavras, Alexandre.
      São comentários como o seu que me motivam a continuar este trabalho de compartilhamento de conhecimento, colaborando cada vez mais para a comunidade de programadores.
      Muito obrigado pelo comentário! Abraço!

  3. Grande André,
    Trabalhei contigo no setor SAJ na Softplan e sempre soube que eras um ótimo profissional.
    Pelo o que vejo és também um ótimo professor.
    Grande abraço pra você e desejo cada vez mais sucesso.

    1. Fala, Jonatas!
      Lembro de você! Conversamos bastante sobre algumas codificações no sistema e linguagens de programação.
      Agradeço pelo elogio, meu caro. Sucesso pra você também!
      Abraço!

  4. André, tudo bem? Estou colocando em prática as suas dicas, mas quando estou no método ValidarValor da TValidarValor:

    function TValidador.ValidarValor(Propriedade: TRttiProperty): boolean;
    var
      Valor: variant;
    begin
      // Obtém o valor da propriedade
      Valor := Propriedade.GetValue(Self).AsVariant;
    
      // Valida o valor, exigindo que seja diferente de 0 e diferente de vazio
      result := (VarToStr(Valor) <> EmptyStr) and (VarToStr(Valor) <> '0');
    end;
    

    A variável Valor está assumindo o valor do parâmetro que passei na propriedade:

       [TValidador('Código da Empresa')]
       property Codemp: string read FCodemp write SetCodemp;
    

    Minha dúvida é: o código seria realmente esse?

    Propriedade.GetValue(Self).AsVariant;
    
    1. A principio eu fiz essa mudança:

      if (VarToStr(Propriedade.GetValue(Self).AsVariant) = EmptyStr) then
              //if not TValidador(Atributo).ValidarValor(Propriedade) then
              begin
                showmessage('Valor não preenchido: ' + TValidador(Atributo).Descricao);
                Abort;
              end;
      
    2. Olá, Danilo. O código é esse mesmo!
      A mudança que você fez é justamente o que o método ValidarValor faz: obtém o valor da propriedade como Variant e depois converte o valor (usando VarToStr) para testar se é diferente de vazio e diferente de zero.
      Veja que utilizo um “not” na chamada do método:

      if not TValidador(Atributo).ValidarValor(Propriedade) then

      Ou seja, se o retorno da função for falso, significa que o valor é vazio ou zero, portanto, exibe a mensagem para o usuário.

      Abraço!

    3. Olá André, mas a questão é que na função TValidador.ValidarValor, pelo menos aqui comigo está retornando o valor da FDescricao entende? E não da propriedade, estranho!

  5. Olá André, qual seria a melhor maneira de não validar alguns dos atributos do objeto pensado que nem todos necessitam desta validação, e se possível sem precisar ter de valida-los individualmente.

    1. Olá, Adailson, como vai?
      Não tenho certeza se compreendi a sua pergunta, mas as propriedades dos objetos são validadas somente se possuírem atributos. Portanto, caso alguma delas não precise de validação, basta não associar atributos à elas. Vale destacar também que atributos não são exclusivos de propriedades. Podemos incluir atributos na declaração de classes, como no exemplo abaixo:

      [TAtributo]
      TPessoa = class
      ...

      Com essa possibilidade, podemos definir classes que serão validadas e classes que não precisam de validação.

      Abraço!

  6. Bom dia, gostei muito do seu artigo. Trabalho algum tempo em um framework de persistência em Delphi e utilizo muito o RTTI. Pois bem, estou com um problema em pegar os valores via RTTI de uma property da minha classe que é do tipo TStringList. Tentei:

    lQuery.Params.ParamByName(LowerCase(propRtti.Name)).AsString := propRtti.GetValue(pObjeto).AsVariant;

    ou

    lteste:= TStringList(propRtti);
    lQuery.Params.ParamByName(LowerCase(propRtti.Name)).AsString := lteste.CommaText;

    Onde lteste é do tipo TStringList. Não importa o que eu adicione no campo, o sistema grava 84220224.

    1. Olá, Osvaldo, tudo bem?
      Para obter o conteúdo de propriedades que são objetos, é necessário utilizar a função AsObject no GetValue. Em seguida, basta testar se o objeto é da classe desejada e, em caso positivo, acessar a propriedade. Veja o exemplo abaixo:

      var
        Contexto: TRttiContext;
        Tipo: TRttiType;
        Propriedade: TRttiProperty;
        Valor: TObject;
      begin
        Contexto := TRttiContext.Create;
        Tipo := Contexto.GetType(TClasse.ClassInfo);
      
        for Propriedade in Tipo.GetProperties do
        begin
          if Propriedade.PropertyType.TypeKind = tkClass then
          begin
            Valor := Propriedade.GetValue(Objeto).AsObject;
            if Valor is TStringList then
              ShowMessage((Valor as TStringList).CommaText);
          end;
        end;
      end;

      Abraço!

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Preencha o campo abaixo * Time limit is exhausted. Please reload CAPTCHA.