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.