Olá, pessoal, como estão?
Recebi uma dúvida bem interessante dos leitores Cassiano e Jean Alysson 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?
Introdução
Uma das recomendações mais importantes do Clean Code é evitar o número excessivo de parâmetros em um método, conforme já comentei no 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 do Cassiano e do 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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.
Explorando o RTTI
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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:
1 |
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:
1 |
Valor não preenchido: DataVenc |
O mesmo acontece para a descrição do documento:
1 |
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.
Custom Attributes
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
:
1 2 3 4 5 6 7 8 |
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:
1 2 3 4 5 6 |
{ 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:
1 2 3 4 5 6 7 8 9 10 11 12 |
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
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; |
Um pouco mais de RTTI
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:
1 |
W1055 PUBLISHED caused RTTI ($M+) to be added to type 'TBoleto' |
Configure-as com visibilidade public para evitar este aviso do compilador.
Um pouco mais de Custom Attributes
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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!
Hi André! Nice article 🙂 Validate BO is a common task in today applications. If you want you can take a look here https://github.com/bittimeprofessionals/delphi-entities-validators . Any contributor is welcome.
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! 🙂
Boa tarde!
Excelente artigo André! Aprendi mais um pouco sobre RTTI!
Obrigado!
Muito obrigado, Elton!
Pretendo aprofundar um pouco mais em artigos futuros!
Abraço!
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.
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:
Abraço!
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.
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!
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.
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!
André, tudo bem? Estou colocando em prática as suas dicas, mas quando estou no método ValidarValor da TValidarValor:
A variável Valor está assumindo o valor do parâmetro que passei na propriedade:
Minha dúvida é: o código seria realmente esse?
A principio eu fiz essa mudança:
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:
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!
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!
Não entendi muito bem, Danilo. Vou entrar em contato com você.
Abraço!
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.
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:
Com essa possibilidade, podemos definir classes que serão validadas e classes que não precisam de validação.
Abraço!
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:
ou
Onde lteste é do tipo TStringList. Não importa o que eu adicione no campo, o sistema grava 84220224.
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:
Abraço!
André, parabéns pelo artigo, muito bom mesmo.
Eu tenho uma dúvida que estou me batendo há dias e não consigo progredir.
Como verificar se está vazio um field que é do tipo de uma outra classe? Os fields do tipo primitivo é sussa, mas do tipo de outra classe não consegui fazer e não achei nada na net.
Desde já agradeço.
Boa tarde, Aleandro!
Ótima questão. Não cheguei a fazer o teste, mas você pode comparar o tipo da propriedade com tkClass e adicionar uma verificação com Assigned:
Caso esse código não resolva a sua necessidade, envie um e-mail para “[email protected]” com mais detalhes, pode ser?
Abração!
Muito interessante o post André. Acrescentou bastante. Mas tenho uma curiosidade que gostaria que me fosse satisfeita.
Segue o cenário: Existe uma tabela no banco que pode ter campos inseridos conforme necessidade de parametrização exigido por um determinado cliente. Existe um programa que faz essa parametrização e, um parâmetro específico permite criar esses campos em uma determinada tabela.
Então eu tenho uma classe no meu sistema que representa essa tabela com todos os campos que são fixos, mas, como estou criando campos na tabela de forma parametrizada eu preciso criar as propriedades na classe que representam esses campos também.
Então a pergunta é: É possível criar uma propriedade em uma classe em tempo de execução?
PS: Estou usando mapeamento (ORM) que vc me ajudou a desenvolver.
Olá, Denerson, boa noite! Peço desculpas pela demora.
Infelizmente não é possível criar uma propriedade em tempo de execução, já que elas fazem parte da estrutura da classe.
No seu caso, eu tentaria trabalhar com listas ou coleções de campos, preenchendo-as de acordo com os campos existentes. Como os campos da tabela são dinâmicos, o mapeamento da classe também deve ser dinâmico.
Abraço!
Infelizmente só consegui sucesso com o primeiro exemplo.
Quando uso o RTTI a variável “Atributo” fica vazia e não entra no IF…
Olá, Alexssandro!
O IF verifica se o “Atributo” é do tipo
TDescricao
. Se não está entrando no IF, é possível que, neste momento, a variável “Atributo” seja de outro tipo.Experimente colocar um breakpoint nesse IF e usar a janela de Evaluate/Modify (Ctrl + F7) para investigar os detalhes dessa variável.
Abraço!