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.

Deixe um comentário

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