Múltiplas tentativas de execução em Delphi (Retry)

Existem momentos na execução do código que é previsível que ele falhe. Não estamos falando de um simples “try except”, mas de problemas que fogem ao controle do desenvolvedor, que vai além de sua capacidade de atuar dentro do fluxo de operação e que muitas vezes tem forte relação com questões de infra-estrutura.

Em um modelo tradicional de aplicações Enterprise Desktop – o que acredito que seja a massiva categoria dos programas desenvolvidos em Delphi – é muito difícil encontrar situações onde haja a necessidade de realizar múltiplas tentativas de execução. Com isso, refiro-me ao famoso “retry”, onde existe a tentativa de execução de um trecho de código e, caso haja qualquer tipo de problema, ela é refeita tantas vezes quanto necessário – ou parametrizado – como uma espécie de “tudo bem, mas vamos tentar de novo, na esperança que agora vai…”.

Exemplos desses casos são acessos a dispositivos periféricos, dependências de rede, e outras dependências que fogem ao cerne do trinômio cpu-memória-hd. Esses, por sua natureza e histórico de evolução, mostram-se tão maduros que pouco ou nada deixam a desejar hoje em dia quanto à possibilidade de falhas. Elas existem? Claro, sem sombra de dúvidas. Mas desenvolver sistemas pensando nisso pode ser, na atualidade, um gasto – não se engane, tudo custa alguma coisa – sendo que existem diversos outros pontos maiores de preocupação e que vão demandar muito mais tempo e atenção.

Por outro lado, ao analisarmos problemas como requests que trafegam pela Internet, qual seria a taxa disso? Ridiculamente comum. Com o crescimento de sistemas distribuídos e crescente adoção de micro serviços, a comunicação quase sempre é feita através de web-services REST. Isso tudo acaba trafegando via HTTP através da Internet, onde tudo pode ocorrer, desde um nó de computador desligado, quanto problemas atmosféricos prejudicando a qualidade do sinal – quem viveu a época dos discadores modem sabe bem o que é isso.

Existem diversos Frameworks em outras linguagens que fornecem facilidades para esses tipos de situação, contudo, em Delphi, não vejo isso tão disseminado.

Já há algum tempo transitou em nosso meio um artigo sobre uma classe helper que disporia facilmente a qualquer objeto a possibilidade de realizar esse retry: https://www.danielespinetti.it/2017/06/simple-retry-mechanism-in-delphi.html

Contudo, não sou adepto aos helpers do Delphi. Não vejo problema nenhum em helpers – em diversas outras linguagens isso funciona muito bem -, apenas não gosto como o Delphi gerencia isso. Isso basicamente porque se houver a utilização de mais de uma classe helper na mesma unit, uma delas já não vai funcionar. Ou seja, não há como criar uma estratégia sadia para isso.

Abaixo encontra-se a implementação de uma classe para realizar múltiplas tentativas de execução de código e que, a meu ver, funcionará tão satisfatoriamente quanto um helper – talvez não de forma tão elegante – mas com certeza muito flexível:

A classe TRetrier implementa a interface IRetrier. Assim, fica muito fácil utilizá-la até porque facilita o gerenciamento de memória.

Outro ponto é o tipo de dados record chamado TRetryExecutionResult. Ele é um tipo de dados preparado para receber informações e identificar:

Result – Resultado. Por se tratar de um tipo TValue (Para saber mais, acesse: http://edgarpavao.com/2017/07/01/um-pouco-sobre-o-tvalue/), ele tem flexibilidade suficiente para retornar diversos tipos de dados.

Error – Informa se houve algum tipo de erro.

ErrorCode – Código do erro, caso haja algum tipo de erro, o codigo de erro pode ser informado para identificar e dar uma resposta posterior.

StopExecution – Indica se deve impedir a “re-execução” do código. Existem alguns tipos de cenários onde, mesmo que haja a possibilidade de utilizar o “retry”, não é viável que isso seja feito. Assim, é possível evitar essa re-execução alimentando essa informação. Imagine que você está se comunicando com uma API e essa API retorna um erro de acesso negado. Não adianta ficar tentando vária vezes com as mesmas credenciais. Outra estratégia deve ser vista.

A interface IRetry nos fornece o método Retry, que possue 4 parâmetros:

AFunc – É a função que será executada. Ela retorna justamente o tipo TRetryExecutionResult, que orientam o funcionamento da classe de múltiplas execuções.

AQtyTimesToTry – É a quantidade de tentativas caso hajam falhas de execução. Um valor de 3, indica que o código deverá ser executado 3 vezes.

ADelay – É o tempo em milissegundos que serão aguardados até a próxima execução, caso hajam falhas.

AErrorOutputMessage – Código que será executado caso hajam problemas. Ele é executado quanto o retorno da função (AFunc) demonstra que ocorreu um erro e também caso a quantidade de tentativas já tenha sido atingida.

TRetryExecutionErrorCode é um valor string que retorna o código do erro, e TRetryExecutionTimesExceeded um valor booleano, que retorna quando a quantidade de vezes de tentativas de execução já foram alcançadas. Dessa forma, é possível criar uma saída personalizada para cada erro dentro da execução. Exception é a própria exceção caso ela ocorra na execução do código. Caso contrário será nil.

Assim, basicamente basta passar para a classe o código que será executado, a quantidade de vezes que deverá ser re-executado em ocorrência de erros, quanto tempo aguardar entre cada execução e a procedure de tratamento de erro caso necessário.

Veja abaixo o exemplo de consumo dessa classe de Retry:

O CodigoExecucao é um simples código de consumo de um webservice. Caso haja problemas (StatusCode <> 200) ele retorna essa informação.

SaidaMensagensErro é a procedure de tratamento de erros, caso ocorrem. Perceba que se o StatusCode for diferente de 200, CodigoExecucao vai pontuar isso e SaidaMensagemErro terá essa informação para utilizar em seu fluxo de tratamento.

No final, basta chamar Retrier.Retry e você terá a garantia de execução quantas vezes desejar.

A abordagem aqui é um pouco mais complexa comparada àquela dada por Daniele Spinetti, contudo, é mais flexível e íntegra porque prevê os tratamentos de erros provenientes de suas execuções, e pronta para ser utilizada em projetos profissionais.

Identificou onde poderia utilizar uma abordagem dessa em seus projetos? Avise-me nos comentários. Gosto muito de saber que essas informações foram úteis não só a mim, mas a todos vocês.

Programação Orientada à Aspecto

O padrão de design SOLID possui grandes regras para o desenvolvimento de  aplicações com qualidade. Um de seus predicados mais válido é o princípio da responsabilidade única, que determina a separação das funcionalidades de forma que cada objeto tenha apenas uma única responsabilidade, indo ao encontro de seu outro predicado, o do aberto/fechado, que roga a ideia de “fechar” a classe para qualquer alteração, mas faze-lo de forma que fique “aberto” para a extensão.

Olhando de perto o princípio da responsabilidade única, percebemos que, quanto menos funcionalidades as classes possuírem, menor será a chance de precisarmos alterá-las. Assim, entende-se que vale mais termos muitas classes fazendo pequenas coisas do que ter poucas classes fazendo muitas coisas. Isso causa uma pulverização de classes dentro do projeto. A menos que você tenha um custo por quantidade de classes, nenhum problema. Todavia, temos situações rotineiras que obrigam uma classe possuir “obrigações” que não condizem com sua responsabilidade, mas que são igualmente importantes, e que não podem ser desprezadas.

Vamos imaginar uma simples classe cuja obrigação seja  simplesmente imprimir uma mensagem na tela:

Estamos criando algo bem simples porque o foco não é desenvolvermos uma funcionalidade aprimorada de impressão. Veja que a responsabilidade da classe é simplesmente imprimir um texto na tela, o que ela faz.

O problema

Ocorre que, uma alteração da regra de negócio tornou necessário que todos os acessos à essa funcionalidade de impressão sejam logados em um arquivo texto e que apenas algumas pessoas possam imprimir mensagens na tela. Logo, temos que adicionar ao projeto um mecanismo de log assim como um mecanismo de segurança para impedir que um usuário não qualificado imprima mensagens na tela.

Inicialmente, devemos identificar o usuário:

em seguida, vamos desenvolver nosso mecanismo de log:

Novamente aqui o foco não é o mecanismo de log em si, e sim apenas a sua existência. Basicamente o log grava uma mensagem em um arquivo texto, conforme a constante CAMINHO_ARQUIVO (Log.txt).

Agora vamos desenvolver nosso mecanismo de segurança:

A regra é bem simples. Cada usuário terá seu nível de segurança conforme definido na propriedade NivelAcesso da classe de usuários. Novamente aqui, o importante é a existência da necessidade do controle, e não exatamente qual o critério de definição do nível de acesso.

Agora que as classes estão definidas, podemos reimplementar nossa classe de impressão de forma que atenda à regra de negócio:

Criamos o objeto de log e o objeto de validação do acesso. Caso o usuário não tenha acesso de nível 2 ou superior, o acesso será negado. Caso o usuário o tenha, será liberado. Nos dois casos, será feito o log da mensagem quando o acesso estiver garantido ou de uma informação de tentativa de acesso negada, caso o acesso for negado.

A regra de negócio foi atendida, mas caímos em um problema: ferimos o princípio da responsabilidade única (e outros, como o da inversão de dependência, mas isso é outra história). A classe, além de imprimir a mensagem, também se responsabilizou por logar a operação e garantir a segurança de acesso. Além disso, é muito provável que o log deva ser utilizado em outras partes do sistema, assim como a validação do acesso. É possível que ainda tenhamos que adicionar rotinas de tratamento de exceções que igualmente irão aumentar a complexidade da classe. Isso é o que chamamos de crosscutting concerns, ou procupações que estão em diferentes partes do sistema.

Crosscutting Concerns

Crosscutting concerns são, em uma tradução livre, preocupações transversais. São funcionalidades necessárias no sistema que afetam outras funcionalidades, invariávelmente. No nosso exemplo, temos a funcionalidade de impressão. Quando adicionamos a funcionalidade de log e de controle de acesso, inevitavelmente fizemos que a funcionalidade de impressão fosse impactada por essas duas outras funcionalidades. De certa forma, a funcionalidade de impressão não tem qualquer relação com a funcionalidade de log ou de controle de acesso, mas a regra de negócio forçou com que fosse afetada, para poder atender às outras duas  novas funcionalidades.

Uma abordagem orientada à aspectos

Uma das soluções possíveis é utilizar os princípios do paradigma da Programação Orientada à Apecto. Ela prega que devemos separar as responsabilidades que estão em diferentes locais do sistema de forma que elas fiquem agrupadas e possam ser utilizadas sempre que necessário, sem prejudicar o design do projeto.

A orientação à objetos realiza o agrupamento do código de forma que ele fique organizado. Assim, é possível classificar o código em unidades funcionais (classes) e reutilizá-las de forma estruturada. A orientação à aspecto funciona ainda como uma dimensão adicional da abstração, fornecedor uma organização para as responsabilidades que são crosscutting.

Bom lembrar que a orientação à objetos não atrapalha a orientação à aspecto, e vice-e-versa. Elas se complementam de forma que tudo funcione adequadamente.

Mas antes de adentrarmos realmente na solução do problema, precisamos saber mais sobre como TVirtualMethodInterceptor pode nos ajudar.

Proxies dinâmicos com TVirtualMethodInterceptor

A classe TVirtualMethodInterceptor é realmente muito interessante. Ela intercepta a chamada de um método virtual de uma classe e controla o comportamento dessa classe, além de outros apectos relacionados ao método.

Ela funciona como um proxy dinâmico, causando um efeito “man-in-the-middle”. Pelo método ser virtual, é possível interceptar sua chamada e interferir no seu funcionamento, sem que o método tenha qualquer consciência disso. Ele, de fato, não participa do processo. É a sua chamada que é interceptada, tendo o interceptador o controle sobre o método.

Escreví um post exclusivamente sobre o TVirtualMethodInterceptor no passado, por isso, você deve lê-lo aqui, caso não tenha conhecimento a respeito dele.

A Solução

Com a utilização de TVirtualMethodInterceptor, é possível separarmos as resposabilidades de log e segurança da classe de impressão em outra classe, deixando a classe de impressão com apenas a responsabilidade de imprimir a mensagem na tela, satisfazendo o princípio da responsabilidade única.

Vamos voltar a nossa implementação da classe de impressão para sua simplicidade anterior:

Veja que ela não faz qualquer referência ao usuário, como da primeira vez. A diferença aqui, é que agora, o método “Imprimir” é um método virtual. Isso é muito importante para o funcionamento da interceptação.

Vamos redesenhar nosso projeto para utilizarmos a classe interceptadora. No FormCreate, vamos identificar o usuário. Novamente aqui, estamos simplificando todo o processo porque o importante é verificarmos o funcionamento da interceptação do código, e não a melhor forma de gerenciar a sessão do usuário:

O que fizemos foi criar a classe de interceptação do método virtual, e atribuir as funcionalidade para antes da execução do método (OnBefore) e depois da execução do método (OnAfter ).

OnBefore

Antes de executarmos o método, verificamos se o “usuário da sessão” possui o direito de acesso necessário para sua execução. Caso tenha, nada acontece, porém, se não possuir o nível de acesso necessário, o log grava a tentativa de acesso negada, impede a execução do método (através de DoInvoke := False) e rompe com o fluxo de execução chamando a exceção.

OnAfter

Como esse evento ocorre logo após a execução do método, o que podemos fazer aqui é logar a utilização do método, apenas. De qualquer forma, atendemos aos requisitos de log.

Colocando tudo para funcionar

Tudo o que precisamos fazer agora é chamar o método Imprimir da classe de impressão. Quando ele for chamado, será interceptado e todas as retinas de verificação de segurança de acesso e de logs serão executadas.

Conclusão

Com a estratégia de utilizar proxies dinâmicos, conseguimos simplificar a implementação da funcionalidade de impressão. Conseguimos ainda atender aos princípios da responsabilidade única, assim como o do aberto/fechado.

Evidentemente, a forma como pensamos o projeto é muito mais importante do que a forma como o codificamos. A estratégia de separar as responsabilidades é o que garante a simplicidade que leva ao sucesso. Ao pensarmos nos aspectos do sistema, pudemos separá-los, e desenvolver com um design muito mais interessante.

Caso ainda tenha dúvidas, pode encontrar o código-fonte do projeto de teste aqui.

Caso de Estudo – Performance em Plano de Contas

Introdução

Dentro da teorida da contabilidade, o controle contábil de qualquer entidade é realizado através da análise das contas contábeis. Essas contas nada mais são do que depósitos de valores, estruturados em um formato de árvore – ou seja, uma estrutura onde uma conta contábil é “pai”  de outra conta contábil, que por sua vez pode ser pai de outra, assim, sucessivamente.

A estruturação das contas em um formato de árvore é o que chamamos de plano de contas. Ele dá a flexibilidade necessária para a organização dessas contas, conforme a necessidade de cada entidade.

Cada conta contábil possui a finalidade de reter valores. Esses valores podem ser de duas naturezas diferentes: valores de débitos, ou valores de créditos. As naturezas dos valores são significativas porque, embora não podemos dizer que uma natureza é positiva e a outra negativa, quando existe uma operação de naturezas diferentes, elas se subtraem, ao contrário das operações de naturezas iguais, que se somam.

Além da natureza, uma conta contábil ainda possui uma “meta informação”, que seria o tipo da conta contábil. Contas contábeis que possuem “filhas” são chamadas de contas sintéticas, e as contas contábeis que não possuem “filhas”, são chamadas de contas analíticas. Além do carácter classificatório, essa característica interfere no funcionamento da lógica contábil, pois apenas contas analíticas podem participar de operações contábeis – também chamadas de lançamentos contábeis – enquanto contas sintéticas não podem, servindo apenas para a formação da estrutura da organização do plano de contas.

Uma vez que as contas sintéticas não participam de operações contábeis, elas não recebem valores de débitos nem de créditos, todavia, seu valor corresponde ao somatório dos valores de todas as suas contas filhas.

Outra característica das contas contábeis diz respeito ao seu nível dentro da estrutura do plano de contas. O nível corresponde ao grau de aprofundamento dentro do plano de contas, onde, na seguinte estrutura abaixo, temos contas de diferentes níveis.

  • Conta 1
    • Conta 1.1
      • Conta 1.1.1
      • Conta 1.1.2
    • Conta 1.2
      • Conta 1.2.1
  • Conta 2
    • Conta 2.1
      • Conta 2.1.1

Conforme a estrutura acima, podemos dizer que a Conta 1 e a Conta 2 são de grau 1 (ou nível 1). A Conta 1.1, Conta 1.2, Conta 2.1, são de grau 2, e as contas Conta 1.1.1, Conta 1.1.2, Conta 1.2.1 e Conta 2.1.1, são de grau 3.

Problema

Como os valores das contas contábeis sintéticas são determinados pela somatória dos valores de suas contas “filhas”, não há uma forma de buscar seu valor, a não ser somando os valores de cada conta filha. Contudo, em uma estrutura de árvore onde os níveis das contas não são fixos, normalmente chegando ao grau 5 ou 6, essa tarefa torna-se interessante, porque a busca desses valores deve ser flexível suficiente para encontrar os valores adequados, na ordem correta.

Imagine que no exemplo do plano de contas àcima, seja necessário descobrir qual o valor da  conta “Conta 1”. Para conseguirmos isso, teríamos que somar o valor de todas as suas contas filhas. Porém, as suas contas filhas também são contas sintéticas, onde, para descobrirmos seu valor, precisaríamos somar todos os valores de suas contas filhas (no caso, das contas “netas”). Isso pode ocorrer sucessivamente, até o momento onde não hajam mais contas filhas. De qualquer forma, não existe a possibilidade, partindo-se da primeira conta, de saber exatamente quantas são as contas filhas, sem que para isso, se percorra conta a conta em busca de seus relacionamentos.

Em um cenário onde todas as informações dos valores das contas são armazenados em um banco de dados – imagino que o cenário mais comum entre os sistemas dessa natureza – temos um grande problema quando precisamos fazer um relatório que exponha os valores atuais de todas essas contas.

Primeira solução

Por se tratar de uma estrtura em árvore, que precisa ser percorrida, uma boa estratégia é a utilização de uma função recursiva que busque os valores das contas.

Primeiramente, ela identifica se é uma conta analítica. Se for, pega o valor do seu saldo. Para isso, temos que realizar a consulta no banco da dados em uma tabela que contenha essa informação e retornar esse valor.

Se for uma conta sintética, identifica todas as suas contas filhas e chama a sí mesmo, para cada conta filha, somando os resultados. Isso fará com que cada conta filha execute a mesma lógica, retornando os valores se forem analíticas, ou buscando também em suas contas filhas, se forem sintéticas.

Problemas com a primeira solução

Embora essa abordagem seja muito prática, pois em poucas linhas de código temos todos os valores necessários, ela não é performática, porque para cada busca do saldo, é necessário realizar uma chamada ao banco de dados. Em um plano de contas que contenha 200 contas analíticas de grau 5, por exemplo – o que é um cenário muito otimista – teríamos milhares de consultas no banco de dados para a formação dos valores. Isso mesmo, milhares, porque para cada conta do grau 5, teriamos as consultas, depois as teríamos novamente para as contas de grau 4, e depois para as contas de graus 3, e assim até o primeiro grau.

Longe de mostrar-lhes isso na prática, já é possível identificar um grande problema de performance, pois se é certo evitarmos o máximo possível as chamadas aos banco de dados, evitar milhares delas é mais certo ainda.

Segunda solução

Assim que identificamos a quantidade de consultas necessárias com a abordagem da recursividade, notamos a necessidade de diminuir o máximo possível essas consultas.

Aqui, desde já, peço desculpas aos puristas de banco de dados, mas se existe um motivo que me faz duplicar informações, esse motivo é a performance que essa duplicação pode causar – e somente em casos extremos e realmente necessários.

Uma nova abordagem seria criar uma tabela específica para esse relatório, cujo saldo já fosse armazenado correto nas contas sintéticas a cada nova movimentação. Se essa tabela existisse, o relatório seria praticamente instantâneo, porque com apenas uma única consulta, em uma única tabela, com filtros simples, teríamos todas as informações do relatório.

Problemas com a segunda solução

Porém, para que isso funcione, o mecanismo dos lançamentos contábeis precisaria atualizar constantemente essas tabelas, a cada nova movimentação, assim como para cada estorno. Sempre que houvesse algum lançamento contábil, essa tabela deveria ser atualizada.

Algo não cheira bem com essa estratégia. Embora ela seja funcional, ela pode ser catastrófica se não perfeitamente executada. Sem contar que é um design feio para uma aplicação.

De qualquer forma, poderia ser executada criando-se os mecanismos necessários dentro da aplicação – muito mais fácil se for orientado a objetos, visto que provavelmente apenas um ponto seria alterado – ou implementando em banco de dados o que chamamos de “views materializadas“.

Novamente, embora seja funcional, não é uma boa abordagem, porque além de duplicar as informações do banco de dados, qualquer problema de implementação dessa abordagem levará a sérios problemas que não serão claros no primeiro momento, levando a informações erradas em uma ferramenta de tomada de decisão.

Terceira solução

Uma outra abordagem possível é partir de uma perspectiva down-top, ou seja, partir das contas filhas para as contas pais. Para isso, vamos seguir os seguintes passos:

Primeiro passo:  Montar uma lista de contas com todas as contas existentes e criar uma lista auxiliar que servirá como um índice para a lista de contas principal.

A ideia por trás disso é a realização de uma única consulta no banco de dados para obtenção das informações das contas. Assim, com uma única consulta, montaremos uma lista de contas na memória.

Vamos utilizar as seguintes estruturas:

TContaContabil representa todas as informações necessárias para o relatório.

TListaConta será nossa lista de contas.

TIndiceConta representa as informações necessárias para criarmos um índice da lista.

TListaIndiceConta é a lista de TIndiceConta. É o próprio índice em sí.

Mas por que o índice? Bom, essa é a grande jogada para termos desenpenho. Uma vez que eu tenha a lista de contas, ter uma lista auxiliar cuja ordenação possa ser sequencial, mas ainda mantendo uma referência com a lista principal que mantém sua ordenação natural, faz com que consiga utilizar algorítmos de alta performance na lista auxiliar, quando necessário.

A primeira coisa é gerar a lista de contas. Ao mesmo tempo em que uma conta é adicionado nessa lista, você deve também adicioná-la no índice:

A ordenação do índice é muito importante, porque é através dela que poderemos utilizar a busca binária.

Agora já existe uma lista com as informações necessárias, corretamente ordenadas.

Segundo passo: Buscar o valor dos lançamentos contábeis e já vinculá-lo com as contas contábeis, utilizando a performance da busca binária.

A busca binária é algorítmo excelente para termos performance em buscas. Para saber mais a respeito, click aqui!

A busca binária que utilizaremos aqui será implementada manualmente, visto as necessidades particulares dessa situação. Por isso, não estaremos utilizando a busca binária nativa do próprio Delphi, mas sim uma implementação específica:

As informações das contas precisam ser atualizadas. Cada cenário é um cenário específico, mas imaginando um onde a busca das contas seja feita obtendo essas informações do banco de dados, temos algo como:

O código acima simplesmente alimenta a lista já existente com as informações dos saldos das contas, sendo que o saldo de cada conta foi obtido por uma consulta no banco de dados. Assim, temos uma única select trazendo o saldo de todas as contas, mas o relacionamento com a nossa lista principal é realizado através da busca binária, nos dando uma enrome performance nesse relacionamento.

Terceiro passo: Montar a lista dos saldos iniciais – mesma operação anterior, só que com os valores anteriores ao período.

O terceiro passo é igual ao segundo, sendo que os valores dos saldos iniciais (ou o saldo anterior ao período) que também somam-se ao valor total da conta contábil devem ser adicionados na lista. Não irei repetir o processo aqui porque a lógica é a mesma, alterando-se apenas a forma como obtem esses valores.

Quarto passo: Partindo dos filhos, montar através de uma função recursiva, todos os valores das contas pais, somando-se sempre o valor atual com o valor dos filhos.

Uma vez que você possui uma lista com os valores de cada conta analítica já alimentados, é possível, utilizando-se da recursividade, alimentar o valor de cada conta pai, visto que os valores das contas pai é o somatório dos valores das contas filhas. Assim, temos:

Pronto.

Conclusão

A utilização da busca binária foi, sem dúvida, o grande fator para o aumento da performance. Trouxemos o custoso processo de vínculo dos valores com suas devidas contas para a memória e tratamos isso em alta performance. Assim, um processo que chegava ao ponto de durar alguns minutos, pôde ser realizado em apenas alguns segundos.

Alta Performance com Busca Binária

Um dos métodos mais comum de realizar uma busca em uma lista de opções, é procurar item a item, até encontrar a opção que corresponde à aquela que está sendo procurada. Essa metodologia de busca é conhecida como bubble search. Embora funcional, ela é horrível para a performance, porque faz com que cada elemento dentro do conjunto a ser procurado, precise ser testado, até que a opção correta seja encontrada. Assim, em uma lista de 100.000 opções por exemplo, caso a última opção seja a correta, todas as 100.000 opções precisam ser testadas.

Em uma implementação Delphi, isso geralmente está vinculado a um laço de repetição como “for” ou “while”:

 

Embora esse seja um caso necessário em alguns cenários, obviamente não é algo a ser desejado.

Quando os valores de uma lista possam ser ordenados de alguma forma, é possível criarmos alguns algorítmos otimizados para a busca desses valores. Existem diversos algorítmos recomendados para cada situação. Falaremos agora um pouco da busca binária.

Busca Binária

Binary Search
Mecanismo de uma busca binária

Quando pudermos ordenar uma lista de elementos, é possível utilizarmos a busca binária. Ela possui esse nome porque sempre que a condição do retorno é testada, ela descarta metade da lista em cada teste. Assim, ela considera que existam dois lados: o lado correto, e o lado errado (2 = BInário).

Vamos entender melhor.

Imaginemos um conjunto de números dispersos que vão de 1 a 100. A primeira coisa a ser feita, é ordenar esses números de alguma forma. No nosso exemplo, vamos ordená-los de forma crescente (1, 5, 7, 10…100). A ordenação é o passo mais importante, porque ela que cria a condição necessária para o funcionamento da busca binária.

Uma vez com a lista ordenada, vamos utilizar a seguinte lógica para buscarmos nossos valores. Sempre que formos procurar um valor, iremos sempre ao meio da lista de verificaremos se o valor do meio é o valor que procuramos. Se for, está finalizado. Caso não for, verificaremos, conforme a ordenação dada (no nosso caso, ordem crescente de números), se o número que procuramos está antes ou depois do meio. Caso esteja antes, sabemos que podemos descartar da busca a metade para frente. Caso esteja depois, sabemos que podemos descartar o início da lista até a metade. Agora, teremos apenas metade da lista para procurar novamente.

Esse trecho da metade da lista restante forma uma nova lista, menor, é verdade, mas a qual podemos aplicar a mesma lógica. Assim, para essa nova lista, resultado da lista anterior, aplicamos o mesmo princípio: vamos à metade dessa nova lista e verificamos se o valor do meio é o valor que procuramos. Se for, encontramos o elemento. Se não for, verificamos se o número que procuramos está antes ou depois do meio. Se estiver antes, a metade posterior é descartada. Se estiver depois, a metade anterior é descartada.

Essa lógica é aplicada continuamente  às novas “sublistas” até encontrarmos o resultado.

O interessante desse algorítmo é que, não importa o tamanho da lista, o valor será encontrado em até 8 etapas. Isso mesmo. Mesmo que a lista tenha 1.000.000 de elementos, em até 8 etapas, o resultado correto é retornado. Isso é fantástico quando falamos em desempenho, porque 8 etapas de processamento é muito pouco para qualquer computador processar e
retornar o valor correto.

Implementando uma busca binária

Podemos implementar uma busca binária em Delphi facilmente, utilizando o recurso da recursividade. Como teremos ao máximo 8 chamadas para o mesmo método, não haverá risco de estouro de pilha (stack overflow). Veja o código abaixo:

 

 

A primeira coisa a fazermos é checar se o valor a ser encontrado está é algum dos limites da lista. Se não for, encontramos o meio e verificamos se o elemento do meio é o elemento que procuramos. Se não for, verificamos se está para frente ou para trás da lista. Conforme o caso, chamamos novamente a função BuscaBinaria, mas alterando o “range” do valor inicial e valor final da lista. Essa é a forma de limitarmos as buscas para a metade anterior ou posterior da lista.

Lembrando que a chamada inicial da busca ocorre passando o valor inicial como o primeiro elemento da lista, e o valor
final como o último elemento da lista.

Em poucas linhas você tem uma excelente ferramenta de busca de alta performance.

Busca Binária nativa no Delphi

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Generics.Collections.TArray.BinarySearch

O Delphi possui uma ferramenta nativa para buscas binárias. Utilizando-se do método BinarySearch da classe TArray, é possível chegar ao mesmo resultado.

Veja o código abaixo:

 

A primeira coisa que fizemos aqui foi criar uma lista aleatória de 0 à MAX (ou 100.000). Depois definimos qual seria o valor a ser encontrado na lista. A variável “Indice” é alimentada pelo terceiro parâmetro do método, um parâmetro out que retorna o índice em que está o elemento (ou o índice mais próximo a ele). O último parâmetro do método é um método anônimo para a definição da ordenação da lista (lembra que a ordenação é muito importante para a busca binária?).

O método retorna “true” se encontrou o elemento ou “false” se ele não foi encontrado.

Muito simples não é?

Conclusão

A busca binária é uma execelente ferramenta para encontrar valores dentro de um conjunto que possa ser ordenado de alguma forma. O Delphi possui essa ferramenta implementada de forma nativa e bem flexível, pois implementa a
funcionalidade com o uso de genérics, dando a possibilidade de alterar o tipo e a metodologia de busca conforme o caso necessário. Todavia, ainda sim existem alguns casos mais complexos onde será necessário o desenvolvimento de uma busca binária própria para o problema em questão, então é importante conhecer o mecanismo de funcionamento dessa busca, para conseguir extrair o máximo de proveito de sua utilização.

Utilizando interfaces em classes com herança diferente de TInterfacedObject

TInterfacedObject é realmente últil quando precisa-se implementar uma classe com uma interface. Isso porque ele cuida de toda a operação da contagem de referências automaticamente (ARC – Automatic Reference Count). Graças ao ARC, o desenvolvedor não precisa se preocupar em desalocar a interface da memória, porque isso será feito automaticamente pelo compilador.

Porém, existem casos onde a classe que precisa implementar a interface já herda de outra classe, diferente de TInterfacedObject. O que fazer?

Se simplesmente adicionarmos as interfaces na declaração da classe, obteremos a mensagem de erro na compilação:

[dcc32 Error] Unit2.pas(22): E2291 Missing implementation of interface method IInterface.QueryInterface
[dcc32 Error] Unit2.pas(22): E2291 Missing implementation of interface method IInterface._AddRef
[dcc32 Error] Unit2.pas(22): E2291 Missing implementation of interface method IInterface._Release

Isso já nos dá a dica necessária para implementarmos nossa própria versão do TInterfacedObject na nossa nova classe.

Todas as vezes que criamos um novo objeto interfaceado, o compilador automaticamente chama o método _AddRef. Todas as vezes que o compilador entende que o objeto não será mais utilizado (por uma chamada de Objeto := nil, por exemplo), o compilador automaticamente chama o método _Release. Com esses dois métodos, o compilador consegue gerenciar a quantidade de referências utilizadas dessa interface e saber quando o objeto será destruído da memória. Existe ainda o método QueryInterface, que é utilizado todas as vezes onde é necessário saber se o método implementa uma interface (lembra a declaração da GUID na interface?). Uma vez implementado esses 3 métodos, já é possível utilizar o novo objeto.

Vamos entender individualmente cada um deles, mas antes, vamos falar um pouco sobre o ARC:

ARC

ARC ou Automatic Reference Counting é uma das diferentes formas de gerenciamento de memória. Ela é baseada na contagem de referências dentro do programa. Toda vez que um novo objeto (ou interface, no caso do Delphi) é instanciada, uma nova referência é adicionada. Sempre que o compilador entende que o objeto não será mais utilizado, seja por explicitamente trocar a referência do objeto (Obj := nil, por exemplo), ou pelo próprio término do escopo (o término de uma função, onde o objeto foi declarado), a referência é decrementada. Assim, quando a referência cai a zero, significa que a instância pode ser destruída da memória, porque não será mais utilizada.

ARC é utilizado em linguagens como Objective-C (e Swift). O Delphi o implementou como gerenciador das interfaces. Ao contrário do garbage colector, o ARC não precisa de um processo em background para gerenciar a limpeza dos objetos da memória, porque sabe, no momento do último release, quando o objeto pode ser destruído da memória.

Existe uma grande discussão entre as vantagens e desvantagens de se utilizar ARC ou Garbage Collector. Cada uma possui as suas características e suas vantagens. Cabe a cada desenvolvedor se identificar com cada uma.

_AddRef

Sempre que um novo objeto é instanciado, uma nova referencia à interface é incrementada, pela chamada de _AddRef. Assim, o compilador sabe a quantidade correta, de quantas instâncias ainda estão sendo utilizadas, antes de destruir da memória a referência da interface.

Para implementar o método _AddRef:

Aqui basicamente utilizamos AtomicIncrement para incrementarmos a variável FRefCount. AtomicIncrement é “thread-safe” e “platform-safe”, se assim podemos dizer. Isso significa que o funcionamento independe da utilização de threads ou se o software é VCL ou Firemonkey (rodando em Android, iOS, etc).

_Release

Sempre que o compilador entende que uma instância de uma interface perdeu sua referência, seja explicitamente pela chamada de Interface := nil, ou pelo término do escopo, onde a interface não será mais necessária, o compilador chama automaticamente o método _Release. Quando a quantidade chega a zero, o compilador sabe que pode desalocar da memória a interface, porque ela não será mais necessária. Assim, o desenvolvedor não precisa se preocupar com retira-la manualmente, através da chamada de Free ou outra semelhante.

Para implementar o método _Release:

Aqui também, decrementamos a variável através de AtomicDecrement.

QueryInterface

QueryInterface é mais complexa. Ela determina se o objeto implementa uma determinada interface.

A implementação de QueryInterface é:

Feito isso, qualquer objeto estará apto a funcionar como interfaces.

Exemplo completo

Abaixo encotra-se um exemplo de como funciona a interface. Para que você consiga certificar-se que realmente funcionou, habilite ReportMemoryLeaksOnShutdown para True.

Projeto

 

Unit

Conclusão

Você pode obter um detalhamento maior através da própria implementação de TInterfacedObject em System.pas.

Conforme o anúncio do RoadMap 2017/2018 feito pela embarcadero em https://community.embarcadero.com/article/news/16519-rad-studio-roadmap-may-2018, eles estão estudando utilizar ARC nas próprias aplicações Windows. Hoje, para aplicações Firemonkeys, dependendo da plataforma utilizada, todos os objetos são gerenciados pelo ARC, e não apenas as interfaces. Espero que isso concretize-se e nos dê a facilidade de traballhar com mais essa tecnologia em nossas aplicações legadas.

Como é a Performance da RTTI?

Trabalhando em um determinado projeto, o primeiro caminho que tomei foi utilizar as funcionalidades da RTTI, porque realmente gosto como ela funciona. Todavia, deparei-me com a possibilidade de realizar a mesma coisa de uma forma mais simples, e sem a necessidade de nenhum processamento em runtime, visto que rodaria através do  código compilado. A primeira coisa que pensei foi: claro que vou alterar para a forma mais simples, mas qual seria o prejuízo na performance, se optasse pela RTTI? Isso foi algo que não consegui responder no primeiro momento.

Foi então que fiz um teste simples. Testaria as duas formas e mediria a performance, para saber o quanto a RTTI (por utilizar o processamento em runtime) prejudicaria a performance em rotinas diversas vezes executadas.

Segue a implementação do teste:

A diferença das duas formas de execução é obtenção do método e sua execução pela RTTI, ao contrário do cast diretamente para a interface e sua execução.

Veja que a utilização da RTTI, por sua própria natureza, utiliza muito mais código e muito mais processamento, porque o fluxo para obter o mesmo resultado é muito mais longo, embora muito mais flexível. Mas qual seria o resultado?

Realmente fiquei muito impressionado. Só pela leitura do código, imaginei que a RTTI fosse muito mais lento, mas não foi isso o que aconteceu. Ela deveras possui uma performance inferior, mas nada a se envergonhar.

  • Para 100.000 iterações, as duas rodaram em menos de 1 segundo.
  • Para 1.000.000 iterações,  pela interface foi menos de 1 segundo, enquanto pela RTTI durou próximo à 6 segundos.

A partir daí, o tempo de diferença aumenta exponencialmente, lógico.

Quero apenas chamar a atenção para o fato de que dificilmente rodamos rotinas mais de 100.000 vezes. Quando necessário, a melhor performance deve ser utilizada, mas acredito, embasado na evidência àcima, que na grande maioria dos casos, pode-se utilizar a RTTI sem prejuízo algum da performance, que seja sensível ao usuário.

Tem uma visão diferente? Comente abaixo e vamos aprender ainda mais juntos.

Run-time Type Information (RTTI)

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti

No momento da compilação do programa, o compilador reúne informações a respeito de todos os elementos do sistema, como classes, records, enumeradores, e outras estruturas, e disponibiliza essas informações para utilização em tempo de execução (run-time). Imagine que, enquanto o software estiver em execução, é possível saber quantos métodos as classes possuem, quantas propriedades, variáveis, etc. Imagine ainda que essas informações são tão detalhadas que é possível identificar todos os parâmetros de entrada e saída do método e seus tipos. Imagine tambem que é possível invocar os métodos (procedures e funções), quando necessário.

O conceito pode ser claramente entendido quando você utilizar os recursos, porque será notável o poder que essa funcionalidade disponibiliza ao desenvolvedor. Em outras linguagens de programação, o mesmo conceito possui o nome de reflection.

Algo parecido era utilizado desde o início do Delphi, para captar informações para o object inspector, por exemplo, mas não era tão avançado e documentado como as alterações que houveram na RTTI no Delphi 2010.

Junto com as alterações da RTTI, foi adicionado o recurso de atributos, que é a capacidade de adicionar suas próprias informações (metadados) às classes. Veremos isso detalhadamento depois.

Classe vs Instância

É possível obter tanto informações da classe (informações da abstração) e outras estruturas quanto informações do objeto instanciado. Existe uma diferênça significativa nisso, porque quando trabalhamos com as instâncias, temos o poder de alterar os valores atuais daquela instância.

TRTTIContext

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiContext

TRttiContext é um record fundamental de onde todas as informações podem ser obtidas. A própria documentação da embarcadero o define como o pilar da Rtti. Ele é o responsável pela exposição das informações para utilização. Uma curiosidade é que apenas uma única instância de TRttiContext é realmente criada dentro da aplicação (e liberada no término da aplicação).

O seu uso ficará mais claro quando iniciarmos os exemplos, todavia atente-se que todo o uso da RTTI deve provir dele, e você NÂO DEVE INSTANCIAR AS OUTRAS CLASSES DA RTTI DIRETAMENTE! Tudo deve vir através da TRttiContext.

TRttiType

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiType

Essa classe é responsável por todos os objetos que retornam informações da Rtti. Nunca crie uma classe da Rtti diretamente. Sempre a obtenha através da TRttiContext.

Nossa classe de exemplo

Vamos declarar uma classe de exemplo para podermos obter informações dela em tempo de execução:

TRttiField

Documentação: http://docwiki.embarcadero.com/Libraries/Berlin/en/System.Rtti.TRttiField

A classe TRttiField é a classe responsável pelas informações das variáveis (fields) de uma classe ou record. Através dela é possível identificar quais campos existem, seus tipos e valores, além de poder alterar esses valores em tempo de execução, via Rtti, dos objetos instanciados:

Obs: Também é possível obter as informações dos campos através de GetField(‘NomeDaVariavel’).

TRttiProperty

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiProperty

A classe TRttiProperty é responsável por trazer as informações das propriedades. Com ela é possível obter informações de quais propriedades existem na estrutura (classe ou record), quais seus tipos, quais seus índices, seus métodos get e set, valor de retorno (para os objetos instanciados), entre outras:

Obs: Também é possível obter as informações dos campos através de GetProperty(‘NomeDaPropriedade’).

TRttiMethods

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiMethod

A classe TRttiMethods é a responsável por trazer as informações dos métodos (procedures e funções) da estrutura (classe ou record). Uma particularidade dos métodos é a existência das informações dos parâmetros dos métodos. Através da RTTI é possível obter informações da quantidade de parâmetros, quais seus tipos, etc:

Existe uma diferenciação na chamado dos métodos entre GetMethods e GetDeclaredMethods. Uma classe pode ser parte de uma linha de herança. Para esses casos, GetMethods busca todos os métodos da classe, inclusive aqueles declarados nas classes superiores.  GetDeclaredMethods, ao contrário, retorna apenas os métodos declarados na classe que está sendo verificada, indiferentemente se ela herda de outra classe ou não.

Tente trocar essa chamada no código de exemplo e veja como o retorno é afetado.

 

TRttiMethod.MethodKind

Documentaçãohttp://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiMethod.MethodKind

Existe muitas vezes a necessidade de obtermos a informação do típo do método (função, procedure, etc), que pode ser feito através do retorno de TRttiMethod.MethodKind. As opções podem ser as abaixo (conforme enumerador TMethodKind):

mkProcedure – O método é uma procedure.
mkFunction – O método é uma função.
mkDestructor – O método é um destrutor de classe.
mkConstructor – O método é um construtor de classe.
mkClassProcedure – O método é uma class procedure (procedure estática).
mkClassFunction – O método é uma class function (função estática).
mkClassConstructor – O método é um class constructor (construtor estático).
mkOperatorOverload – O método é uma sobrecarga de um método de operações (matemáticas, casts, etc)
mkSafeProcedure – O método é uma procedure segura.
mkSafeFunction – O método é uma funçção segura.

Documentação do enumerador TMethodKind: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.TypInfo.TMethodKind

 

TRttiMethod.CallingConvention

Documentação: http://docwiki.embarcadero.com/Libraries/Seattle/en/System.Rtti.TRttiMethod.CallingConvention

É possível inclusive obter a convenção da chamada do método através de TRttiMethod.CallingConvention. Ela pode ser:

  • ccReg – O método utiliza a convenção de chamada register.
  • ccCdecl – O método utiliza a convenção de chamada cdecl.
  • ccPascal – O método utiliza a convenção de chamada pascal.
  • ccStdCall – O método utiliza a convenção de chamada stdcall
  • ccSafeCall – O método utiliza a convenção de chamada safecall.

(conforme enumerador System.TypInfo.TCallConv: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.TypInfo.TCallConv)

Quando você declara uma função ou procedure, você pode definir uma convenção de chamada, usando uma das diretivas register, pascal, cdecl, stdcall, safecall, e winapi.

As convenções de chamadas definem a ordem em que os parâmetros serão passados para o método. Eles ainda afetam a forma como os parâmetros são retirados da pilha da memória (quer saber mais sobre isso, click aqui), o uso dos registradores e a forma de tratamento das exceções. A convenção padrão (quando você não define alguma explicitamente) é a register.

Para detalhes maiores sobre as convenções das chamadas, você pode acessar diretamente a documentação oficial da Embarcadero: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Procedures_and_Functions_(Delphi)#Calling_Conventions

e

http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Calling_Convention_Options_(BCC32)

 

TRttiMethod.CodeAddress

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiMethod.CodeAddress

Caso necessário, você pode obter o ponteiro para o endereço da memória onde o método foi alocado com  TRttiMethod.CodeAddress.

Evocando dinamicamente um método através da RTTI

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiMethod.Invoke

É possível evocarmos um método de uma instância de forma simples. A partir do momento que temos nosso método identificado, podemos simplesmente chamar Metodo.Invoke:

Repare que é possível passar os parâmetros da função, assim como receber o retorno e utilizá-lo posteriormente.

TRttiArrayType

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiArrayType

Você também pode obter informações de arrays através da classe TRttiArrayType:

TRttiRecordType

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiRecordType

É possível obter informações de records, assim como das classes. Vamos primeiramente declarar um record:

Agora vamos obter as informações dele através da RTTI:

Como pode ser observado, é muito parecido com o que já acontece nas classes.

TRttiPackage

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiPackage

Você pode obter inclusive informações das packages do delphi através da classe TRttiPackage. Essas informação serão apenas das packages de run-time atualmente carregadas na aplicação. Por padrão, ela retorna a própria aplicação:

 

TRttiOrdinalType

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiOrdinalType

É possível obter informações dos tipos ordinais, como Integer, Byte e Word, por exemplo:

Atributos

Documentação: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Attributes_and_RTTI

Talvez esse seja o recurso mais conhecido da RTTI, porque através dele é possível criar seus próprios metadados das suas estruturas e recuperar isso posteriormente em tempo de execução. Os atributos não alteram a funcionalidade da classe, e sim criam informações extras que podem ser utilizadas:

Diretiva de compilação {$M} ou {$TYPEINFO}

Documentação: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Run-Time_Type_Information_(Delphi)

A diretiva de compilaçãao {$M} ou {$TYPEINFO} controla a geração das informações de metadados para a utilização da RTTI. Basicamente ela habilita ({$M+} ou {$TYPEINFO ON}) ou não ({$M-} ou {$TYPEINFO OFF}) a geração das informações de metadados. As informações de metadados são as informações da Rtti, que são utilizadas em todas as classes da Rtti. Sem elas, TRttiMethods.GetDeclaredMethods não retornaria nada, por exemplo.

Obs: Quando declaramos um método público ou published, o compilador automaticamente habilita a geração dessas informações para utilização pela RTTI.

Diretiva de compilação {$METHODINFO}

Documentação: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/METHODINFO_directive_(Delphi)

A diretiva de compilação {$METHODINFO} funciona apenas quando a diretiva {$TYPEINFO} estiver habilitada ({$TYPEINFO ON}).

Ela pode ser:

  • {$METHODINFO ON} – Quando está habilitada.
  • {$METHODINFO OFF} – Quando está desabilitada.

Quando está habilitada, ela produz informações adicionais para os métodos de uma interface. Provavelmente foi desenvolvida para ser utilizada em camadas mais baixas das ferramentas de redes da Embarcadero, como o DataSnap, por exemplo, visto que a documentação não diz muito a respeito, embora recomende “raramente” utilizá-la.

Diretiva de compilação {$RTTI}

Documentação: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/RTTI_directive_(Delphi)

A ideia por tras da directiva {RTTI} é habilitar ou desabilitar as funcionalidades extendidas da RTTI.

A princípio, os métodos obtidos através de GetMethods não trazem as informações dos métodos protected e private. Ao declarar a directiva {RTTI}, estamos determinando que o compilador também adicione essa e outras informações aos metadados do fonte.

A RTTI pode ser definida como:

onde:

  • INHERIT – especifica que as configurações da RTTI serão iguais às das suas classes base:
  • EXPLICIT – especifica que as configurações da RTTI serão sobrepostos (override) nos métodos herdados.
  • Visibility-Clause – especifica se serão os métodos, propriedades e ou campos que serão herdados/sobrepostos. Ainda é possível determinar para eles, quais serão afetados através de suas visibilidades: [vcPrivate],[vcProtected], [vcPublic], [vcPublished].

Você pode herdar apenas alguns métodos ou propriedades, através de:

Conforme documentação do produto

Caso você queira sobrepor as configurações de algum método herdado, é possível:

Conforme documentação do produto

Isso fará com que as configurações da RTTI da classe base sejam substituídas pelas explicitamente definidas na directiva. No caso acima, somente os métodos e propriedades públicas terão as funcionalidades da RTTI extendidas.

É possível ainda desabilitar todas as funcionalidades extendidas herdadas da classe base:

Conforme documentação do produto

Nesse caso, estamos sobrepondo as configurações da herança e determinando que nenhuma informação extendida da RTTI será emitida para essa classe.

Todas essas opções da directiva dificilmente serão utilizadas, mas elas existem para dar flexibilidade nos casos onde são necessários.

Diretiva de compilação {$WEAKLINKRTTI }

Documentação: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/WEAKLINKRTTI_directive_(Delphi)

Por padrão, as informações necessárias sobre os métodos para a utilização da RTTI são geradas para dentro da aplicação (claro, dependendo das diretivas, conforme vimos anteriormente). Quando a funcionalidade de invocação de métodos da RTTI é utilizada, é necessário que essas informações de métodos estejam compiladas dentro do binário, para seu correto funcionamento. Todavia, existem casos onde a aplicação não faz uso das chamadas dinâmicas de métodos pela RTTI. Assim, você pode utilizar {$WEAKLINKRTTI  ON} para determinar que essas informações de métodos não sejam gerados para dentro da aplicação, impedindo que essas informações “encham” o executável. Mas lembre-se que isso somente é válido para os casos onde não exista a chamada dinâmica dos métodos (invoke).

Escopo da diretivas

Todas as diretivas acima possuem escopo local, ou seja, produzem efeitos apenas onde são declaradas (para as classes, por exemplo). Existe, todavia, uma forma de habilitar a diretiva para todo o projeto. Basta colocá-la logo antes da cláusula uses no DPR. Assim, o escopo da diretiva será para todo o projeto. O mesmo pode ser feito para as units, que terão escopo dentro daquela unit.

Tratamento de excessões com RTTI

A RTTI adicionou alguns tipos de exceções que podem ocorrer quando trabalhamos com suas classes:

EInsufficientRtti

EInsufficientRtti é disparado quando a RTTI precisa de mais informações geradas para poder retornar as solicitações de informações, mas por alguma razão (provavelmente pelo uso das diretivas) essas informações não foram geradas pelo compilador.

ENonPublicType

ENonPublicType é disparado quando as informações “não-públicas” são solicitadas mas a RTTI não pode fornecer ou não possui essas informações privadas.

EInvocationError

EInvocationError é disparado quando ocorre um erro ao invocar um método através da RTTI. Isso geralmente ocorre devido a passagem de parâmetros, seja pela quatidade, seja pelo tipo passado.

EArgumentException

Embora não seja exclusivo da RTTI, EArgumentException é disparado caso haja algum problema na criação (instanciação) do atributo.

O problema dos metadados da RTTI

Embora todas as informações produzidas para a utilização da RTTI sejam úteis em muitos casos (principalmente no desenvolvimento de vários frameworks), a sua geração produz um excesso de informação que “incha” o executável. Assim, quanto mais informações produzidas pela RTTI, maior o tamanho do executável. Atente-se para o fato de que as informações serão geradas (conforme a determinação das diretivas, é claro) mesmo se as informações não forem utilizadas, por isso, o recomendado é desabilitar essas informações se você não fizer uso. Mas fique atento porque, mesmo que você não faça uso, caso algum framework que você utilize o faça, ele passará a não funcionar caso a geração dessas informações sejam desabilitadas.

Outras funcionalidades

A API da RTTI é muito extensa, e não caberia demonstrar aqui todas suas possibilidades. Os pontos aqui demonstrados são uma boa base para conhecer a extensão das funcionalidades da RTTI. Assim você pode explorá-la mais profundamente sempre que necessário para obter as informações necessária para o seu projeto.

Conclusão

Como você observou nos exemplos acima, A RTTI foi muito bem estruturada para obter as informações necessárias das estruturas do código fonte. Tudo segue a mesma padronização, o que torna a curva de aprendizado muito rápido. Através dessa funcionalidade, você pode produzir software mais versáteis e menos acoplados (mais independentes de estruturas fixas), melhorando significativamente sua capacidade de desenvolvimento.

TComponent.FreeNotification

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Classes.TComponent.FreeNotification http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Classes.TComponent.Notification

Existem alguns casos onde um componente precisa saber quando outro componente foi destruído. Isso ocorre principalmente na construção de componentes, mas pode ser utilizados em qualquer outro caso em que a lógica se aplique.

Utilizamos o método FreeNotification para realizar a essa tarefa de forma muito simples no Delphi.

Basicamente, FreeNotification registra o componente em uma lista que será disparada no momento de sua destruição, alertando todos os outros componentes alí registrados que ele foi destruído.

Como exemplo, vamos criar a classe TTeste abaixo:

TTeste é uma classe que herda de TComponent e reimplementa o método virtual Notification:

Depois disso, vamos olhar o código abaixo:

O que foi feito aqui em cima foi adicionar Teste (TTeste) na lista de componentes a serem notificados quando Shape for destruído. No momento que chamamos Shape.Free, o método Notification de TTeste é executado. Assim, TTeste pode tratar como cuidará dessa informação.

Isso geralmente é utilizado quando existem referências em um objeto de outro objeto. Comumente vemos essa situação em componentes que referenciam popups menus ou datasets. Assim, caso o popup menu ou o dataset seja destruído, é possível alterar a referência para nil, evitando apontar para um endereço de memória que não mais está utilizável.

Esse tratamento não é necessário quando o componente está no mesmo form (dependendo da lógica) ou quando ambos componentes possuem o mesmo owner. Isso devido a forma como o Delphi trabalha com a memória. Componentes do mesmo form são destruídos na destruição do form e componentes que possuem o mesmo dono (owner) são destruídos na destruíção do owner. Além disso, Notification não é executado na destruição do owner, porque trata-se de uma liberação natural da memória, e um evento esperado.

Tem outra ideia onde isso possa ajudar no desenvolvimento de aplicações? Compartilhe conosco seus pensamentos.

Áreas de memória no Delphi

Documentação: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Memory_Management

Uma aplicação em Delphi possui basicamente 3 áreas de memória com a qual pode trabalhar: Memória Global ou “segmento de dados”, Stack e Heap. Cada qual possui sua utilizada, seu objetivo e sua particularidade.

Para trabalhar com o Windows, o Delphi utiliza uma versão modificada do FastMM como seu gerenciador de memória. Com o surgimento do FireMonkey, outras plataformas além do Windows foram integradas ao produto. Assim, um novo gerenciador de memória foi adicionado para as novas plataformas (MacOS, IOS, Android e Linux) chamado POSIX.

A documentação da Embarcadero quanto a esse assunto é um pouco decepicionante, porque data de um tempo onde apenas havia o suporte para Windows 32 bits.

Memória Global

Armazena os valores de todas as variáveis globais estáticas. É alocada no momento da criação da aplicação e desalocado no momento do seu término. O seu conceito, funcionamento e utilização é bem simples, ficando para a Stack e Heap os maiores detalhamentos.

Stack (ou pilha)

Área da memória que armazena os valores de todas as variáveis de escopo local das funções e procedures. Quando uma função ou procedure é chamada, ela cria uma pilha com as informações das variáveis, que é liberada pela finalização da função ou procedure, ou por uma otimização do compilador. Isso faz com que cada chamada de uma função ou procedure tenha sua própria pilha. Cada thread da aplicação possui sua prória área de “stacks”.Em aplicações multi-thread, o gerenciamento da memória não cria qualquer problema, porque não trata-se de uma memória compartilhada.

A stack existe para um melhor gerenciamento da memória, visto que ela é liberada assim que não for mais necessária. Contudo, o seu funcionamento propiciona a facilidade de não gerenciar essa memória, que é feita automaticamente pelo compilador. Aliás, você não aloca manualmente a memória de um parâmetro de uma função ou quando declara que “I” é do tipo Integer:

A alocação e a desalocação da memória ocorre conforme a regra do FIFO (PEPS), ou first in, first out (por isso é chamado de pilha!).

Ela é utilizada em:

  • Variáveis locais de métodos, procedures e funções.
  • Parâmetros dos de métodos, procedures e funções e seus respectivos retornos.
  • Chamadas de API (do Windows, por exemplo).
  • Tipos Records, desde que eles não estejam alocados globalmente ou que sua alocação tenha sido feita explicitamente de forma dinâmica, como por exemplo, através de New e Dispose.

Exceção

Existe uma exceção, contudo. Long strings, wide strings, dynamic arrays, variants, e interfaces não são alocadas em uma stack (pilha), e sim na Heap (ver adiante), mesmo que variáveis locais, fugindo da lógica. Isso porque tratam-se de tipos dinâmicos, cuja a alocação do tamanho não é determinado no momento da chamada do função ou procedure, e sim com a evolução do código dentro delas.

$MINSTACKSIZE e $MAXSTACKSIZE e aplicações Win32.

Essas duas directivas informam qual o tamanho mínimo e máximo da pilha. O padrão é 16K como mínimo, e 1MB como máximo. Esses dois valores podem ser alterados através dessas directivas. Quando uma aplicação é iniciada, é garantido que o mínimo dos tamanhos estejam disponíveis, de outra forma, o Windows irá sinalizar essa situação e ocorrerá um erro no início da aplicação. A tamanho máximo de uma pilha nunca deve ser ultrapassado, visto que isso ocasionaria o famoso erro de StackOverflow. Partindo do mínimo, quando a aplicação precisa de maior quantidade de espaço dentro da pilha, é realizado o incremento de 4K a cada nova necessidade, até que o máximo disponível seja atingido. A verificação dessa ocorrência é totalmente automática.

Heap

Heap é uma área de memória da aplicação (processo) e não da thread. Ela é utilizada quando não é possível determinar o tamanho do espaço a ser alocado antes da real utilização desse espaço. Variáveis dinâmicas criadas com GetMem ou New são alocados na Heap, até que sejam chamados FreeMem ou Dispose. Além disso, todos os tipos referenciados, como classes por exemplo, também são alocados na Heap.

As aplicações possuem esse espaço de memória para esse propósito porque é custoso ficar solicitando, sempre que necessário, ao sistema operacional a disponibilidade de mais espaço na memória. Assim, a existência de um espaço na memória que possa sempre ser utilizado pela aplicação e que fique ao domínio dela, gera um ganho de performance significativo.

Essa área da memória é utilizando quando:

  • Instanciamos um objeto.
  • Criamos e alteramos elementos dinâmicos, como arrays.
  • Sempre que explicitamente utilizamos funções de alocação de memória com GetMem e New.
  • Trabalhamos com strings, variants e interface.

A alocação da memória Heap não possui uma otimização. Imagine uma área de memória onde sempre que necessário, um espaço é reservado dentro do espaço disponível, mas onde não existe uma sequência lógica para isso, e sim apenas a delimitação de uma “área” para aquela utilização. Com isso, diferente da pilha, quando necessário encontrar um conteúdo na Heap, basicamente é necessário sair verificando cada elemento da Heap até encontrar o correto. Isso obviamente não é o melhor das otimizações.

O compilador pode utlizar tanto a memória RAM quanto a virtual (Disco) para geração da Heap.

Segmento de texto

Como curiosidade, ainda existe o segmento de texto, que basicamente é a área da memória onde o código-fonte (compilado) permanece para a execução da aplicação.

Abaixo encontra-se uma imagem para ajudá-lo no entendimento:

Áreas da memória
Créditos a http://www.tenouk.com/ModuleW.html

Conclusão

Veja que não existe nada de anormal e que tudo segue uma lógica bem simples. Com esse conhecimento é possível desenhar sistemas que aproveitam melhor cada carcterística de alocação de memória e evitar imprevistos.

Utilizando dados JSON em formato TDataSet com TRESTResponseDataSetAdapter

A cada dia que passa estamos tendo mais desenvolvimento de webservices RESTFul, utilizando-se JSON como conteúdo de integração. Para os desenvolvedores Delphi, o que isso significa? Com certeza não significa problema algum.

Acostumados como estamos a trabalhar com datasets, é possível facilmente conectar sua aplicação a um servidor REST e obter as informações em formato tabular, como estivesse abrindo uma query em uma consulta de banco de dados, e simplesmente fazer o uso das informações a partir daí. Parece simples, não? E realmente é.

Vamos utilizar para nossos exemplos, um fake REST server, com informações fictícias de usuários: https://jsonplaceholder.typicode.com/users

Como uma das facilidades de uma ferramenta RAD, o RAD Studio já prepara todo o ambiente com as configurações necessárias para que isso aconteça. No  menu Tools->REST Debugger, coloque o endereço acima em URL e depois click em Send Request. Com isso, todas as informações necessárias foram obtidas do servidor. Click na aba Tabular Data e veja as informações já em formato tabular.

Veja agora que existe um botão com o nome de Copy Components, feito justamente para gerar em memória todos os componentes necessários para a sua aplicação realizar a mesma consulta. Ao clicar, você receberá a mensagem:

The following components have been copied to the clipboard: TRESTClient, TRESTRequest, TRESTResponse, TRESTResponseDataSetAdapter, TFDMemTable

Isso apenas está indicando que foi gerado na memória 5 componentes:

  • TRESTClient
  • TRESTRequest
  • TRESTResponse
  • TRESTResponseDataSetAdapter
  • TFDMemTable

Cole o conteúdo da memória (CTRL + V) em um formulário ou data módulo e veja que todos os componentes vão ser colados.

O três primeiros são os componentes de comunicação com o servidor. O importante quando utilizamos o REST Debugger é que todos esses componentes já vem configurados para o acesso ao webservice, inclusive suas propriedades ContentType, Acept, AceptCharset, URL, etc. Assim, você não precisa se preocupar mais com isso.

O grande responsável pela conversão do JSON para um TDataSet é o TRESTResponseDataSetAdapter, que também já vem todo configurado.

Adicione um TDataSource e ligue-o com a tabela temporária (TFDMemTable). Adicione também um TDBGrid e ligue-o com seu novo datasource. Adicione ainda um botão para o disparo do evento de leitura das informações.

A dica aqui fica na execução da requisição para o webservice, e a abertura do adaptador para obtenção das informações. No evento OnClick do botão, faça:

Execute sua aplicação e veja o resultado ao clicar no botão.

Feito isso, você pode facilmente utilizar as informações do webservice lendo-as através da tabela temporária (TFDMemTable).

Muito legal, não é?

A tabela temporária poderia ser outro dataset, como um TClientDataSet, por exemplo, e não necessariamente o TFDMemTable.

Consegue visualizar o poder que isso traz aos desenvolvedores? O Delphi traz um ótimo recurso de produtividade e adaptabilidade. Sirva-se dele.