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.

Labirintos com Delphi

Uma das coisas boas da programação é a diversão que ela proporciona. Só o ato de desenvolver sistemas já é, muitas vezes, divertido, e desenvolver aplicações que geram momentos marcantes é muito bom.

Em determinado momento tive a curiosidade de pesquisar sobre algorítmos de criação de labirintos (em inglês, mazes). Fiquei surpeso com a quantidade de algorítmos diferentes existentes para isso. Percebi que existem, aliás, diferentes tipos de labitintos, dos quais nunca havia ouvido falar a respeito.

Fui levado a diferentes literaturas, das quais, indico Mazes for Programmers, de Jamis Buck. Para aqueles que de alguma forma não conseguem adquirir o livro, sugiro a leitura do conteúdo no site do próprio autor, que já elucidará o suficiente o neófito de criação de labirintos – http://weblog.jamisbuck.org/under-the-hood.

Abaixo vamos desenvolver labirintos utilizando-se três diferentes tipos de algorítmos – e acredite, existem muito mais tipos.

Antes de começarmos

Existem diferentes tipos de labirintos. O labirinto tradicional consiste de um quadrado ou retângulo, cujo interior é divido de forma a criar as passagens do labirintos. Todavia, existem diversos outros tipos, como labirintos circulares, triangulares, hexagonais, multidimensionais, etc. Estaremos aqui nos referindo apenas aos labirintos tradicionais.

Assim, trataremos cada labirinto como uma matriz de dimensões N x N, com todas as paredes em todas as células, conforme a figura abaixo:

Binary Tree

O algorítmo de árvore binária para desenvolvimento de labirintos é talvez o mais simples de todos. Ele baseia-se no conceito:

  • Escolha duas direções que não sejam opostas. Ex: Norte e Leste. Poderia ser Norte e Oesta, como Sul e Leste. O importante é não se oporem.
  • Todas as céulas encostadas nas paredes dessas direções devem possuir uma passagem entre sí. Imagine que a escolha tenha sido Norte e Leste. Nesse caso, todas as células encostadas nas paredes Norte e Leste deverão estar ligadas:

  • Uma vez isso feito, para cada célula do labirinto, que não sejam essas células já abertas, faça uma escolha entre essas duas direções, e crie uma passagem nela.

Pronto, está criado seu labirínto. Você terá algo como:

Devido essa simplicidade e uniformidade, sem a existência de nenhuma dependência para a operação de cada célula, esse algorítmo é facilmente portado em uma lógica para processamento paralelo, além de utilizar o mínimo de memória, visto que não armazena nenhuma informação.

Sidewinder

O seu entendimento é um tanto confuso, mas depois que “pegamos o jeito”, vemos a sua engenhosidade.

A primeira coisa é entendermos que para o funcionamento desse algorítmo, vamos precisar criar diferentes “ambientes”. Chamo aqui de ambiente um dado corredor, dentro do labirinto.

Considere o labirinto 3 x 3 abaixo:

Um ambiente, é um corredor, conforme pode ser observado abaixo, grifado em vermelho:

Não importa se esse corredor possui apenas 1 casa, ou várias casas. Essa diferenciação é o que dará a “sensação” de um labirinto.

Partindo da posição inicial (0, 0 – primeira célula na esquerda, no topo), vamos linha a linha, de cima para baixo, com a seguinte lógica:

  • Se for a primeira linha, abra passagem por ela toda. Isso mesmo, a primeira linha inteira é um único ambiente. Isso é necessário para que seja possível interligar todos os ambientes.
  • Para as próximas linhas, sorteie um número, que seja entre 1 ao total de células disponíveis da linha, e abra o caminho entre essas células. Como é? Isso mesmo. Entre 1 ao total de células disponíveis da linha.

Vamos partir do segunda passo, quando já temos a primeira linha aberta:

Temos que a segunda linha possui 3 células disponíveis. Assim, vou fazer um sorteio entre 1 à 3, pois 3 é o total de células disponíveis. Vamos dizer que o número sorteado foi 2. Assim, tenho que as duas primeiras células terão passagem:

  • A próxima etapa a ser feita, é criar uma passagem para a linha de cima. Para isso, vamos sortear novamente, mas agora apenas entre as células do ambiente criado, ou seja, entre a primeira e segunda célula. Vamos dizer que, novamente, o número sorteado foi 2 – não se preocupe agora com quais números são sorteados. Isso realmente não importa. Depois você verá que qualquer número daria certo, embora produza um desenho de labirinto diferente.

Assim, como a segunda célula foi sorteada, fazemos uma passagem na segunda célula para a linha de cima:

Pronto. Agora vamos para o próximo passo.

  • Faça novamente a primeira e segunda etapa, para as células restantes da mesma linha.

Hein?

Ok, vou explicar. No nosso exemplo, temos apenas 3 colunas, por isso não é tão perceptível isso, mas o labirinto poderia ser de uma dimensão, 10 x 10 por exemplo. Nós tratamos apenas a primeira e segunda célula. Ainda faltam todas as outras células para a mesma linha. Como no nosso exemplo temos apenas 1 coluna restante, o sorteio vai entre 1 e 1. Assim, temos que a terceira coluna apenas – a única que restou – é a sorteada, e ela também recebe a abertura da passagem para a linha de cima, porque é a única célula a ser sorteada. Assim:

Perceba que o nosso labirinto já está ganhando forma.

  • Repita o mesmo passo inicial da linha 2 para os restantes das linhas. Vamos imaginar que, conforme o sorteio, na terceira linha a primeira célula seja a única escolhida, e depois, as dúas últimas células foram escolhidas. O desenho ficaria mais ou menos assim:

Pronto!

Está finalizado o labirinto, através do algorítmo sidewinder.

Recursive Backtracking

Na minha opnião, o melhor dos três labirintos. Seu desenho é muito mais agradável, e parece-se muito com aqueles labirintos dos almanaques da turma da Mônica – você sabe do que estou falando!.

Ele é um pouco mais complexo, mas nada que seja fora do comum.

  •  Escolha qualquer célula do labirinto randomizamente. Essa passa a ser a célula  ativa. Marque ela como uma célula visitada.
  • Partindo da posição da célula ativa, escolha um lado. Se esse lado ainda não foi visitado, cave uma passagem para ele. Essa célula imediata ao lado escolhido passa a ser a célula ativa.
  • Comece novamente o processo para a célula ativa.
  • Na escolha dos lados, se a célula do lado escolhido já foi visitada, escolha outro lado. Se todos os lados foram visitados, volte para a célula anterior. Ela se torna a célula ativa.
  • Continue o processo até que não existem mais lados disponíveis quando a célula ativa for a primeira célula escolhida, randomicamente.

Um ponto a prestar atenção aqui é que, como ele utiliza-se da recursividade, é possível que ele cause um estouro na stack caso suas dimensões sejam muito grande, então fique muito com isso. Uma possibilidade é alterar a directiva $MAXSTACKSIZE, para interferir na forma como o Delphi solicita a memória. Para saber mais a respeito, click aqui.

Legal, mas cadê a porta?

Já tinha percebido que não havíamos criado nenhuma porta de entrada ou de saída em nossos labirintos? Pois bem, isso é de fácil solução, mas deixo aqui como um exercício para você pensar um pouco.

Conclusão

Espero que você tenha gostado de aprender sobre desenvolvimento de labirintos – mazes – no Delphi, tanto quanto gostei de compartilhar isso com você.

Abaixo está um link para o projeto que desenvolvi para a geração de labirintos.

Link

Obtenha o exemplo do projeto aqui!

Projeto Migration for Delphi (M4D)

M4D

No final do ano passado comecei a desenvolver um projeto open-source, que disponibilizasse à comunidade Delphi a mesma facilidade que os desenvolvedores PHP possuem com frameworks como Laravel e Zend, e mesmo o pessoal de Rails, em realizar atualizações de sistemas controladas por migrações.

Não trata-se de migração de versões do Delphi, mas sim de controle de atualizações de seus próprios sistemas.

Assim, foi criado o projeto M4D (Migration for Delphi), que está disponível gratuitamente a todos que tiverem interesse em usufruir de seus recursos em: https://bitbucket.org/migration4d/m4d

Dentro do site do projeto existem links com vídeos explicativos a respeito do que é uma migração, como instala-la dentro do Rad Studio e como  utilizar os seus recursos. Existe também uma breve documentação com as classes existentes no projeto. Além disso, fiz questão de gravar os mesmos vídeos em português para a comunidade BR de desenvolvedores.

Espero que todos gostem do projeto, assim como gostei de desenvolvê-lo.

Abraços, e boas migrações!

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.

O princípio da responsabilidade única é fantástico… mas você ainda precisa de usabilidade!

O princípio da responsabilidade única é um grande princípio do design de software. Basicamente, ele afirma que toda classe ou módulo deve possuir responsabilidade por apenas uma única parte do sistema.

Essa é uma ideia realmente poderosa, pois quanto menor a responsabilidade de uma classe ou módulo, menor a chance de ocorrer alterações nessas classes ou módulos.

Introdução

Conforme os sistemas vão sendo desenvolvidos e começam a ganhar uma grande proporção, eles costumam ficar “rígidos”. Isso significa que eles não aceitam alterações, mesmo que pequenas, sem que outras partes do sistema sejam afetadas.

Costumo dizer que sistemas, em seu relacionamento entre os elementos que constituem seu corpo, são como teias de aranha. Não há como “mexer” em uma parte da teia sem que toda a teia sofra os impactos. Isto, é claro, para os sistema com alto acoplamento, ou seja, aqueles sistemas onde existe uma alta dependência entre os módulos – e módulos aqui significam classes, units, packages, etc.

A engenharia de software, ao longo do tempo, nos ensinou que existem certas práticas e formas de pensar que ajudam muito a desenvolver sistemas com qualidade, que sejam flexíveis às mudanças. Além do princípio da responsabilidade única, temos diversos outros que nos ajudam a desenvolver sistemas melhores, como o princípio do aberto e fechado, e o princípio de inversão das dependências.

Com o princípio da responsabilidade única, temos a divisão do nosso sistema em diversos micro-elementos, o que garante que, caso haja uma alteração – e acredite, ela vai existir – iremos alterar apenas o elemento necessário para atingir o objetivo da alteração, sendo que o restante permanecerá intacto. Isso é  ótimo porque cada nova alteração gera um impacto e possíveis problemas podem aparecer em virtude disso. Assim, quanto menos alterações, menos “bugs”.

O princípio da responsabilidade única é uma dessas abordagens que melhoram a forma como desenvolvemos sistemas. Ela faz com que criemos “granularidade”, ou seja, tornemos os elementos do nosso sistema tão pequenos que o design do software seja favorecido com os benefícios da “reutilização”, “baixo acoplamento”, ou baixa dependência e fornece uma abertura para “extendermos esses elementos” ao invés de alterá-los.

Um exemplo prático encontra-se abaixo:

Essa é a forma que a maioria dos sistemas são desenvolvidos. A classe TArquivo possui todas as funcionalidades envolvendo a manipulação desse arquivo. E qual o problema disso? Afinal de contas, não está, de fato, de forma organizada, sendo que o objeto agrupa todas as suas funcionalidades? Não é esse um dos principais objetivos da orientação à objetos?

De fato, possui uma organização, o que é bom, mas possui mais do que isso. A classe TArquivo ficou com diversas responsabilidades. Ela é responsável por carregar o arquivo, limpar o arquivo, enviar o arquivo e validar o arquivo. Qualquer alteração em qualquer uma dessas funcionalidades pode comprometer as outras funcionalidades.

Uma outra forma de fazermos a mesmo coisa seria:

Perceba que criamos uma classe para cada funcionalidade. O que fizemos aqui foi distribuir a responsabilidade que antes estava apenas com a classe TArquivo para diversas outras classes. Assim, caso precise alterar a forma que os arquivos são validados, a alteração irá ocorrer apenas no código responsável por isso, sem interferir no código restante. Isolamos a alteração para impedir qualquer impacto negativo sobre o restante do código.

Caso precisemos criar outras funcionalidades, elas terão sua própria classe, impedindo que essas classes atuais sejam modificadas (opa! seguimos o princípio do aberto/fechado naturalmente!)

Há como melhorar ainda mais o código acima, utilizando interfaces e tornando as classes ainda mais genéricas e reutilizáveis, mas o importante aqui é a visualização de como podemos alterar nossa forma de pensar sistemas e separar cada “responsabilidade” em seu devido lugar. Ao fazer isso, o design da sua aplicação assume características de uma boa implementação, porque espontaneamente leva à utilização de outros bons princípios de design.

Responsabilidade Única vs Usabilidade

Conforme formos aplicando o princípio da responsabilidade única, estaremos criando milhares de classes, todas minúsculas. É esse mesmo o objetivo. Alguns irão reclamar que toda essa multidão de classes é desnecessária. Eu penso diferente. Acho que ter 10 ou 100 classes não é problema. O compilador ou o interpretador não liga para isso. Claro que quanto mais classes, mais difícil fica a organização delas dentro do projeto, mas os benefícios dessa prática são inúmeras vezes maiores do que esse problema.

Contudo, existe um aspécto inerente ao princípio da responsabilidade única que é importante. Chama-se usabilidade. E aqui refiro-me à usabilidade do código-fonte pelos desenvolvedores. A não ser que você desenvolva sozinho  e tenha ótima memória, quando desenvolvemos um conjunto de classes, desenvolvemos em um ambiente com a coparticipação de diversas pessoas. Desenvolvemos pensando na manutenção futura que esse sistema terá. Não há como negligenciar o fato de que uma enorme quantidade de classe exige um esforço muito maior para o aprendizado do código, um esforço para a memorização de toda a estrutura.

Assim, conforme o número de classes vai aumentado, a usabilidade vai diminuíndo.

Conhecendo o padrão de projetos Facade (ou Fachada)

Facade é um padrão de projeto há muito já conhecido. O seu objetivo é fornecer uma interface simplificada para um sub-conjunto de interfaces, ou seja, ele agrupa em uma única interface as funcionalidades distribuídas em outras interfaces.

Quando você utiliza sua máquina de lavar roupas, você está utilizando um Facade. O simples fato de escolher o nível de água, a programação da roupa – roupa de cama, toalha, ciclo rápido, etc – e apertar o botão para iniciar a lavagem dispara internamente, uma série de execuções dentro do equipamento. A máquina irá abrir o mecanismo de entrada de água, utilizar o dispenser de sabão e amaciante quando necessário, iniciar a rotação, etc. Todas essas operações ficam acessíveis através da interface com o usuário: botão de escolha dos níveis de água, tipo de lavagem e botão iniciar.

Essa interface é a fachada da máquina de lavar. É através dessa fachada (facade) que é possível ter acesso às reais funcionalidades da máquina de lavar. A fachada em sí, muitas vezes não faz nada, mas ela centraliza a ação do usuário em uma única interface. Ela facilita o uso do subconjunto de funcionalidades.

Quando falamos de facade em software, falamos de uma classe que servirá como a fachada, a interface para a utilização de outras classes, essas as quais serão a verdadeiras responsáveis pelo funcionamento do sistema.

Unindo o design com a usabilidade

É possível unir um bom design mantendo a facilidade de uso. Como vimos, o padrão facade nos ajuda a unir em uma única interface toda a granularidade da prática do princípio da responsabilidade única.

Podemos refatorar nosso código dos exemplos anteriores para que fique assim:

Passamos todas as funcionalidades para a classe facade. Não estamos ferindo o princípio da responsabilidade única porque a responsabilidade da classe facade é justamente servir de fachada, de uma “interface” que facilite a utilização de nossas outras classes. Mantivemos o princípio da responsabilidade única e ainda facilitamos a utilização de todo esse mecanismo, visto que os “clientes dessas classes”, ou seja, os outros desenvolvedores envolvidos, e até o criador das classes depois de certo tempo, terão estrema facilidade em utilizar os recursos de manipulação de TArquivo pois todas as funcionalidades desejáveis estão disponíveis na classe facade.

Novamente aqui, preferí dar o foco nas responsabilidades e não na melhor forma de implementar o desacoplamento. O melhor nesse caso seria utilizarmos interfaces que nos ajudassem a diminuir as dependências de classes.

Uma implementação com as interfaces ficaria assim:

As interfaces garantem que eu posso trocar as classes que manipulam TArquivo sem alterar a estrutura já existente. É uma das formas de codificar para abstrações. Mas isso, já é um outro assunto.

Conclusão

É possível desenvolver sistemas com designs altamente maleáveis e que ao mesmo tempo sejam simples e fáceis de se utilizar. A preocupação com a manutenção e a simplicidade do código é uma das chaves para o sucesso no desenvolvimento de sistema.

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.