A área de tecnologia se expande cada vez mais, tornando-se um mercado bastante aquecido para…
Como melhorar o design das suas classes adotando AOP
Os tempos mudaram bastante. E as necessidades dos nossos usuários também. Houve um tempo em que uma tela de login era suficiente. Hoje nós falamos em autenticação e permissões em todos os lugares do sistema.
Houve um tempo em que registrar o log das alterações em tabela era suficiente. Hoje, nós queremos fazer mapa de calor de onde o usuário mais olha, mais clica, pra criar interfaces mais inteligentes. Houve um tempo em que apenas os dados da tabela de “Clientes” eram o mais importante. Hoje, quase todo dado importa.
Como a arquitetura do nosso software irá suportar tudo isso?
Você sabe que SOLID são princípios de design para o desenvolvimento orientado a objetos. A primeira letra desse mnemônico significa “Single Responsability Principle” (SRP) ou princípio da responsabilidade única. De maneira bem resumida, este princípio de design prega que uma entidade de código (método ou classe) deve ter apenas uma única responsabilidade.
Agora imagine o seguinte cenário: você precisa gerar log de todos os formulários que forem abertos, armazenando além do nome do formulário, o usuário, suas permissões e período (manhã, tarde ou noite).
Onde você faria este código?
- Mapearia todos os botões/menus de abertura de formulários e colocaria ali a função de log?
- Colocaria a função de log na “Fábrica de forms”?
- Colocaria a função de log em um dos níveis de herança dos forms?
E se o cenário mudar? E se agora, ao invés de apenas fazer o log, você também necessite fazer a autorização do usuário? E o que fazer caso você precise mapear o tempo gasto na execução de cada rotina do form? Como fazer isso, mantendo a qualidade do design do seu código?
Percebe que em todos esses cenários você estaria violando o SRP?
Os princípios de design de código são pouco suscetíveis aos jeitinhos. Isso quer dizer que quando falamos que uma entidade de código deve ter apenas uma responsabilidade, é uma mesmo. Sob a pena de termos um design frágil e código repetido se não seguirmos esse princípio.
Você deve concordar comigo que auditoria e/ou controle de permissões é algo que está fora do escopo de um Produto, apesar de ser um aspecto da entidade.
Vamos pensar um pouco. Voltando ao form, as suas responsabilidades seriam:
- Apresentar os dados para o usuário;
- Oferecer elementos de interface para que o usuário possa interagir com os dados.
Essas responsabilidades básicas são o que nós chamamos de “Requisitos Funcionais”. É aquilo que define o propósito de um objeto. E se o teu form está fazendo algo além disso, tem alguma coisa errada.
Já o log, a autenticação, as métricas de performance estão fora do objetivo principal do form, embora sejam do interesse dele, ou façam parte – de forma transversal – de alguma de suas características. A isto nós chamamos de “Requisitos Sistêmicos – ou não funcionais”.
Você pode até dizer para eu não ser tão fundamentalista em relação a SRP. Mas já imaginou a quantidade de código repetido que você teria de escrever para implementar os requisitos não-funcionais em todos os forms da sua aplicação, já que cada um tem suas particularidades?
É aqui que entra a AOP – Aspect Oriented Programming ou Programação Orientada a Aspectos. De forma bem clara, a AOP é uma das estratégias que o desenvolvedor tem de adicionar ao objeto esses interesses transversais, sem poluir o código com classes estranhas ou comportamento alienígena. Ou seja: você consegue adicionar ao objeto os seus “Requisitos não funcionais” sem adicionar dependência ou codificação.
Como a AOP funciona de modo geral?
Como todo bom paradigma, AOP possui alguns elementos que a define. Para entende-los melhor, vamos tentar sair um pouco dos forms e tomar como exemplo uma classe TProduto qualquer. Assim nós teríamos:
- Aspect: São como classes. “Controle de transação”, por exemplo, poderia ser um aspecto do nosso programa. Afinal, existem vários modelos de controle transacional e eu os coloco dentro de uma mesma unidade de código. Ex.: TControleTransacionalAspect
- Join Point: São os comportamentos da classe alvo que apresentam aquele aspecto. Por exemplo, o método “salvar” precisa de um controle transacional. Salvar é um Join point. Ex.: TProduto.Salvar;
- Point Cut: seria a forma de você dizer ao compilador que aquele determinado método (TProduto.Salvar) executa o TControleTransacionalAspect. Poderia ser um arquivo de configurações, uma anotação…
- Advice: Seria o método que executa ou antes, ou durante, ou depois e ou em caso de erro do método TProduto.Salvar.
- Depois de todo esse código ser escrito, ele precisa ser mesclado ao programa principal. Este é o papel do Weaver. Ele funciona como uma espécie de compilador que mescla os dois códigos em um só bytecode.
- Linguagem de componente: O código original, que efetivamente “salva” o produto.
Para que fique ainda mais claro, quando você atribui um aspecto a um objeto, você está dizendo que:
O objeto TProduto está atrelado aspecto TControleTransacionalAspect. E por isso, toda vez que o método TProduto.Salvar é chamado, o código do aspecto TControleTransacionalAspect é executado ANTES da chamada a TProduto.Salvar; DEPOIS da chamada de TProduto.Salvar e no caso de TProduto.Salvar lançar uma EXCEÇÃO.
Mas eu posso ter tudo isso em Delphi?
Sim! Ou quase isso.
Em Delphi não temos um compilador que leia uma definição de linguagem de aspectos e então crie código capaz de ser mesclado com o código do produto final. Mas temos algo que, ao meu ver, é tão interessante quanto: System.Rtti.TVirtualMethodInterceptor.
Como você sabe, diferente dos métodos comuns, quando escrevemos um método virtual, o compilador não escreve um código que o acesse diretamente. O endereço dos métodos virtuais é armazenado em um espaço especial de cada classe, chamado de VMT. Desta forma, em tempo de execução, é possível determinar qual função deve ser chamada pelos métodos sobrescritos, levando em consideração a instância criada.
Como você pode imaginar, isso gera um gasto a mais para processamento. Ao invés de acessar um método diretamente, é preciso procurar em um array o endereço do método e então executá-lo.
Em tempos passados isso gerava uma série de problemas. Executáveis maiores, lentidão de execução. Mas esses problemas não são relevantes hoje. Temos mais disco e memória disponíveis e os processadores atuais são capazes de “prever” os caminhos a serem executados. Então, fora raras exceções, os métodos virtuais não impactariam na performance do seu sistema.
Aliás, o tema “performance” sempre vem à tona quando falamos em Clean Code, OOP e assim por diante. Minha visão é: precisamos medir o trade-off. Será que realmente vale à pena escrever um código cheio de code smells, violando os princípio de desing, tornando a sua manutenção muito mais complexa apenas para ganhar alguns clock’s a mais?
Mas eu vou ter que implementar tudo do zero?
Não! A comunidade Delphi tem construído vários materiais que vem suprir as necessidades do programador moderno. Temos frameworks de injeção de dependência, inversão de controle, testes unitários (com mock e stubs).
Entre eles eu gostaria de destacar um framework simples, open source e que viabiliza exatamente a programação orientada a aspectos em Dephi: o Aspect4Delphi, do Ezequiel Juliano.
O próprio readme do projeto tem um exemplo de implementação do Aspect4Delphi. Eu também tenho alguns exemplos de implementação que utilizei em palestras e que você pode utilizar pra entender como funciona a implementação dos aspectos em delphi:
Dicas
Para que a implementação Orientada a Aspectos não gere mais dor de cabeça do que solução, é preciso que você siga algumas dicas básicas:
Utilize fábricas!
Todo mundo já nos avisou: utilize fábricas para criar os seus objetos. Mas não raramente não seguimos essa recomendação. Afinal, tão mais fácil fazer um create, não é mesmo.
Mas e agora que você deseja aplicar um aspecto a um objeto qualquer? Terá que mapear em todos os lugares onde o objeto é instanciado e então fazer a alteração. Não é mesmo?
Se você trabalha com fábricas de objeto, o único ponto em que terá de fazer a alteração é a própria fábrica!
Utilize frameworks de inversão de controle
DSharp e Spring4D são bons exemplos de frameworks que permitem que você insira os aspectos conforme já demonstramos. Mas é preciso lembrar também que ambas alternativas também tem a sua própria implementação para aspectos.
Use testes automatizados!
Seus aspectos vão mudar conforme o sistema cresce. E eles estarão espalhados pelo sistema, vestindo as classes. Você precisa ter certeza que uma alteração não irá quebrar o sistema inteiro. Testes automatizados te ajudam ter certeza que suas alterações não irão impactar o sistema.
Deixe os aspectos simples!
O problema de ter aspectos complexos é que a probabilidade de surgirem erros é bem maior. Compartilhe as responsabilidades com outras classes. E se possível, execute o código em threads distintas. Assim você pode não afetar a execução do sistema.
Utilize exceções específicas
Já é uma boa prática da vida nunca levantar exceções genéricas. Se você precisa lançar uma exceção, tipifique ela. Crie um tipo básico para os aspectos (O A4D tem uma classe básica para exceções) e herde as derivações à partir dela. Será mais fácil você encontrar erros – e gerenciá-los – se usar tipos específicos de exceções.
Conclusão
Com certeza esse [não tão] breve artigo não esgota o assunto de AOP. Vale lembrar que ela não está limitada à linguagem Delphi. É possível implementar AOP em Java e C# também. Espero que esse artigo te motive a buscar um pouco mais sobre o assunto e ajude a melhorar o design das suas classes.
Até breve!
Comments (0)