Um pouco sobre programação orientada a aspectos (AOP)
Ao longo dos últimos anos, a medida em que o hardware foi se tornando um recurso cada vez mais barato e poderoso, o foco das linguagens de programação tem se voltado para clareza ao invés de performance.
Onde antes era necessário, por exemplo, ponderar com muito cuidado qual o melhor algoritmo para iterar uma lista, hoje esta é uma questão secundária. O que isto propicia a nós, desenvolvedores, é a possibilidade de focar nosso tempo e esforço em questões de clareza e manutenibilidade em nossos códigos.
Com estas novas preocupações em mente, surgiu o paradigma orientado a objetos, que visa traduzir da forma mais natural possível as entidades reais de nosso negócio, para classes e instâncias em nosso código.
Porém, um grande problema que aparece quando se desenvolve um software orientado a objetos é o espalhamento de responsabilidades em comum ao longo de várias classes. Alguns exemplos clássicos deste problema são:
- Log;
- Autenticação;
- Abrir/Fechar conexões;
- Tratamentos de exceção;
Estas preocupações normalmente encontram-se espalhadas ao longo do código de diversas classes, mesmo que o comportamento desejado seja exatamente igual em todas elas.
Para abordar este problema de espalhamento, surgiu o paradigma de programação orientada a aspectos. Este visa complementar a programação orientada a objetos, eliminando em grande parte o espalhamento de código para preocupações comuns em diversos pontos do código. A seguir, tentarei mostrar, utilizando um exemplo prático, as vantagens que o uso de aspectos podem trazer para a clareza do código desenvolvido.
Aprendendo por meio de exemplos
O problema imediato que o espalhamento de código similar gera é: para cada modificação que seja necessária neste código espalhado, será necessário modificá-lo manualmente em cada ponto – como nós, desenvolvedores, sabemos, tarefas manuais como esta estão fadadas a gerar erros ou inconsistências.
A seguir, implementaremos uma classe de conta corrente, que possibilitará depósitos e saques, começando, naturalmente, pelos testes:
[TestMethod]
public void WithdrawShouldDecreaseBalance()
{
Account account1 = AccountBuilder.New
.BelongingTo("MACSkeptic")
.WithInitialBalance(200.95M)
.IdentifiedBy(773)
.Instance;
Assert.AreEqual(100.95M, account1.Withdraw(100).CurrentBalance);
Assert.AreEqual(0.0M, account1.Withdraw(100.95M).CurrentBalance);
Assert.AreEqual(-10.0M, account1.Withdraw(10.00M).CurrentBalance);
}
[TestMethod]
public void DepositShouldIncreaseBalance()
{
Account account1 = AccountBuilder.New
.BelongingTo("MACSkeptic")
.WithInitialBalance(-50.42M)
.IdentifiedBy(666)
.Instance;
Assert.AreEqual(-0.42M, account1.Deposit(50).CurrentBalance);
Assert.AreEqual(79.58M, account1.Deposit(80).CurrentBalance);
}
Para atender ao comportamento descrito acima, implementaremos a classe “Account”, denotada a seguir:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Codevil.MACSkeptic.PostSharpExample.Entities
{
public class Account
{
public long Number { get; set; }
public string Owner { get; set; }
public decimal CurrentBalance { get { return this.currentBalance; } }
private decimal currentBalance;
public Account(long number, string owner)
: this(number, owner, 0)
{
}
public Account(long number, string owner, decimal initialBalance)
{
this.Number = number;
this.Owner = owner;
this.currentBalance = initialBalance;
}
public Account Deposit(decimal howMuch)
{
Console.WriteLine(string.Format(
"New deposit transaction on account {0}.
Balance before operation is ${1}.",
this.Number,
this.CurrentBalance));
Console.WriteLine(string.Format(
"Depositing ${0} on account {1}.", howMuch, this.Number));
this.currentBalance += howMuch;
Console.WriteLine(string.Format(
"Finished deposit transaction on account {0}.
Balance after the operation is ${1}.",
this.Number,
this.CurrentBalance));
return this;
}
public Account Withdraw(decimal howMuch)
{
Console.WriteLine(string.Format(
"New withdraw transaction on account {0}.
Balance before operation is ${1}.",
this.Number,
this.CurrentBalance));
Console.WriteLine(string.Format(
"Withdrawing ${0} on account {1}.", howMuch, this.Number));
this.currentBalance -= howMuch;
Console.WriteLine(string.Format(
"Finished withdraw transaction on account {0}.
Balance after the operation is ${1}.",
this.Number,
this.CurrentBalance));
return this;
}
}
}
Como pode ser visto, nossa conta bancária tem basicamente dois métodos, responsáveis por depositar (Deposit) e sacar (Withdraw) dinheiro. Por enquanto ainda não há tratamento de saldo nas saques, caso se tire mais dinheiro do que o disponível, o resultado será uma conta com saldo negativo.
Logo já conseguimos notar que o log (sendo realizado no próprio console, para manter o foco no que realmente interessa neste artigo) de operaçòes de depósito e saque é bem parecido, gerando um código claramente repetitivo.
Uma possível refatoração nos possibilita eliminar um pouco de repetição, fazendo uso da refatoração “extrair método”, temos:
public Account Deposit(decimal howMuch)
{
this.LogTransactionStarting("deposit");
this.LogTransactionDetails("Depositing", howMuch);
this.currentBalance += howMuch;
this.LogTransactionFinished("deposit");
return this;
}
public Account Withdraw(decimal howMuch)
{
this.LogTransactionStarting("withdraw");
this.LogTransactionDetails("Withdrawing", howMuch);
this.currentBalance -= howMuch;
this.LogTransactionFinished("withdraw");
return this;
}
private void LogTransactionStarting(string action)
{
Console.WriteLine(string.Format(
"New {2} transaction on account {0}.
Balance before operation is ${1}.",
this.Number,
this.CurrentBalance,
action));
}
private void LogTransactionDetails(string action, decimal howMuch)
{
Console.WriteLine(string.Format(
"{2} ${0} on account {1}.", howMuch, this.Number, action));
}
private void LogTransactionFinished(string action)
{
Console.WriteLine(string.Format(
"Finished {2} transaction on account {0}.
Balance after the operation is ${1}.",
this.Number,
this.CurrentBalance,
action));
}
O código já ficou mais limpo e com menos repetições. Porém, os métodos de “Deposit” e “Withdraw”, e mais especificamente a classe “Account” não deveríam estar preocupados em logar as ações. Usando apenas conceitos de orientação a objetos, o máximo que conseguiríamos fazer para melhorar o código acima seria mover os métodos de log para outra classe. Esta ação melhoraria nosso código, mas ainda assim teríamos as chamadas aos métodos de log na classe “Account”, tirando o foco no negócio, tanto durante a codificação, quanto quando se venha a ler o código no futuro.
Há uma conhecida frase sobre classes em orientação a objetos que diz que se você precisa utilizar “e” ao definir o que sua classe faz, provavelmente ela está aglomerando muitas responsabilidades. No caso, nossa classe de conta faz transações e gera logs de transações.
Neste ponto, podemos notar a natureza do log que precisamos. Temos:
- Um evento de log no início dos métodos de depósito e saque;
- Um evento de log com detalhes específicos sobre o depósito e saque sendo realizado no momento;
- Um evento de log ao térmido dos métodos de depósito e saque.
Uma ferramenta bastante competente em auxiliar no uso de aspectos em .NET é o PostSharp, cujo slogan, “make sense, not code” (faça sentido, não faça código), diz bastante sobre a idéia central sobre a qual se sedimenta o paradigma de aspectos. Para a sequência deste artigo será necessário o download do PostSharp (utilizaremos a versão 1.5), que pode ser realizado clicando aqui (é necessário registrar-se no site da ferramenta. O registro é gratuito e pode ser feito aqui).
Após fazer a instalação do PostSharp, devemos adicionar as referências às bibliotecas PostSharp.Laos e PostSharp.Public em nosso projeto, conforme imagem a seguir:

A documentação detalhada do PostSharp (em inglês) pode ser conferida clicando-se aqui.
Para nossa classe “Account” iremos utilizar dois aspectos:
- OnMethodBoundaryAspect (ao redor do método): define pontos de entrada ao redor das chamadas de um método. Isto é, imediatamente antes e imediatamente depois de sua chamada. Este aspecto é ideal para o log antes e depois de uma transação em nossa classe “Account”;
- OnMethodInvocationAspect (na chamada do método): define um ponto de entrada assim que um método é chamado, incluindo detalhes como os parâmetros informados nessa chamada específica. Este aspecto é ideal para o log de detalhes de transação em nossa classe “Account”.
Primeiramente, vamos ver como fica a implementação do aspecto “ao redor” na prática:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PostSharp.Laos;
using Codevil.MACSkeptic.PostSharpExample.Entities;
namespace Codevil.MACSkeptic.PostSharpExample.Aspects
{
[Serializable]
public class LogAccountStatusBeforeAndAfterTransaction :
OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionEventArgs eventArgs)
{
Account account = (Account)eventArgs.Instance;
Console.WriteLine(string.Format(
"New {2} transaction on account {0}.
Balance before operation is ${1}.",
account.Number,
account.CurrentBalance,
eventArgs.Method.Name));
}
public override void OnExit(MethodExecutionEventArgs eventArgs)
{
Account account = (Account)eventArgs.Instance;
Console.WriteLine(string.Format(
"Finished {2} transaction on account {0}.
Balance after the operation is ${1}.",
account.Number,
account.CurrentBalance,
eventArgs.Method.Name));
}
}
}
O aspecto “OnMethodBoundary” possibilita a implementação dos métodos “OnEntry” (antes do método) e “OnExit” (após o método). No código acima pode-se reparar que nosso aspecto precisa de um parâmetro especificando qual a ação está sendo tomada e o código para log foi copiado diretamente do que tínhamos antes na classe “Account”. Um detalhe importante é que todas as classes que implementam algum aspecto do PostSharp devem ser serializáveis.
Vamos ver agora como fica o código de nossa classes “Account” fazendo uso deste aspecto:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Codevil.MACSkeptic.PostSharpExample.Aspects;
namespace Codevil.MACSkeptic.PostSharpExample.Entities
{
public class Account
{
public long Number { get; set; }
public string Owner { get; set; }
public decimal CurrentBalance { get { return this.currentBalance; } }
private decimal currentBalance;
public Account(long number, string owner)
: this(number, owner, 0)
{
}
public Account(long number, string owner, decimal initialBalance)
{
this.Number = number;
this.Owner = owner;
this.currentBalance = initialBalance;
}
[LogAccountStatusBeforeAndAfterTransaction]
public Account Deposit(decimal howMuch)
{
this.LogTransactionDetails("Depositing", howMuch);
this.currentBalance += howMuch;
return this;
}
[LogAccountStatusBeforeAndAfterTransaction]
public Account Withdraw(decimal howMuch)
{
this.LogTransactionDetails("Withdrawing", howMuch);
this.currentBalance -= howMuch;
return this;
}
private void LogTransactionDetails(string action, decimal howMuch)
{
Console.WriteLine(string.Format(
"{2} ${0} on account {1}.", howMuch, this.Number, action));
}
}
}
Podemos perceber os atributos adicionados nas linhas 27 e 34, que apenas informam um aspecto sobre o comportamento deste método, cuja implementação não interessa para a classe “Account”.
Agora, vamos criar um aspecto “OnMethodInvocation”, para o log de detalhes de transação:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PostSharp.Laos;
using Codevil.MACSkeptic.PostSharpExample.Entities;
namespace Codevil.MACSkeptic.PostSharpExample.Aspects
{
[Serializable]
public class LogAccountTransactionDetails : OnMethodInvocationAspect
{
public override void OnInvocation(MethodInvocationEventArgs eventArgs)
{
Account account = (Account)eventArgs.Instance;
Console.WriteLine(string.Format(
"{2} ${0} on account {1}.",
eventArgs.GetArgumentArray().First(),
account.Number,
eventArgs.Method.Name));
eventArgs.Proceed();
}
}
}
Novamente, apenas movemos a implementação do método original de log que estava em nossa classe “Account” para dentro de nosso aspecto. O parâmetro “eventArgs” provê acesso, entre outras coisas:
- À instância corrente da classe na qual o aspecto está sendo aplicado;
- Ao método sendo chamado;
- Aos parâmetros passados na chamada do método.
Um ponto importante a se notar no método acima é a chamada “eventArgs.Proceed” (proceder). Este comando diz ao weaver que o método original deve ser executado neste momento, ou seja, o fluxo original do método deve proceder sem alterações. Caso necessário, poderíamos, por exemplo: :
- Modificar os parâmetros que estão sendo passados para o método original (basta passar o novo parâmetro para o método “proceed”);
- Sequer chamar o método original neste ponto (basta omitir a chamada ao método “proceed”).
Com isso, conseguimos mover toda nossa preocupação com o log para os aspectos. Nossa classe “Account” atualizada fica assim:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Codevil.MACSkeptic.PostSharpExample.Aspects;
namespace Codevil.MACSkeptic.PostSharpExample.Entities
{
public class Account
{
public long Number { get; set; }
public string Owner { get; set; }
public decimal CurrentBalance { get { return this.currentBalance; } }
private decimal currentBalance;
public Account(long number, string owner)
: this(number, owner, 0)
{
}
public Account(long number, string owner, decimal initialBalance)
{
this.Number = number;
this.Owner = owner;
this.currentBalance = initialBalance;
}
[LogAccountStatusBeforeAndAfterTransaction]
[LogAccountTransactionDetails]
public Account Deposit(decimal howMuch)
{
this.currentBalance += howMuch;
return this;
}
[LogAccountStatusBeforeAndAfterTransaction]
[LogAccountTransactionDetails]
public Account Withdraw(decimal howMuch)
{
this.currentBalance -= howMuch;
return this;
}
}
}
Antes de mais nada, utilizando uma expressão regular, podemos deixar nosso código ainda mais limpo:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Codevil.MACSkeptic.PostSharpExample.Aspects;
namespace Codevil.MACSkeptic.PostSharpExample.Entities
{
[LogAccountStatusBeforeAndAfterTransaction(
AttributeTargetMembers = "regex:(Deposit)|(Withdraw)")]
[LogAccountTransactionDetails(
AttributeTargetMembers = "regex:(Deposit)|(Withdraw)")]
public class Account
{
public long Number { get; set; }
public string Owner { get; set; }
public decimal CurrentBalance { get { return this.currentBalance; } }
private decimal currentBalance;
public Account(long number, string owner)
: this(number, owner, 0)
{
}
public Account(long number, string owner, decimal initialBalance)
{
this.Number = number;
this.Owner = owner;
this.currentBalance = initialBalance;
}
public Account Deposit(decimal howMuch)
{
this.currentBalance += howMuch;
return this;
}
public Account Withdraw(decimal howMuch)
{
this.currentBalance -= howMuch;
return this;
}
}
}
Repare que agora o código fonte de nossa classe “Account” não está mais poluído com a preocupação de manter um log das transações realizadas.
Quer mais? Ainda podemos fazer uma melhoria final, agora que temos um pouco mais de domínio sobre o funcionamento do PostSharp, podemos eliminar a utilização do aspecto “OnMethodBoundary” utilizado para os logs de início e término da transação. Para isso, basta realizarmos algumas pequenas modificações em nosso aspecto “OnMethodInvocation”, conforme denotado a seguir:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PostSharp.Laos;
using Codevil.MACSkeptic.PostSharpExample.Entities;
namespace Codevil.MACSkeptic.PostSharpExample.Aspects
{
[Serializable]
public class LogAccountTransactionDetails : OnMethodInvocationAspect
{
public override void OnInvocation(MethodInvocationEventArgs eventArgs)
{
Account account = (Account)eventArgs.Instance;
LogTransactionStarting(eventArgs, account);
LogTransactionDetails(eventArgs, account);
eventArgs.Proceed();
LogTransactionFinished(eventArgs, account);
}
private static void LogTransactionDetails(
MethodInvocationEventArgs eventArgs, Account account)
{
Console.WriteLine(string.Format(
"{2} ${0} on account {1}.",
eventArgs.GetArgumentArray().First(),
account.Number,
eventArgs.Method.Name));
}
private static void LogTransactionStarting(
MethodInvocationEventArgs eventArgs, Account account)
{
Console.WriteLine(string.Format(
"New {2} transaction on account {0}.
Balance before operation is ${1}.",
account.Number,
account.CurrentBalance,
eventArgs.Method.Name));
}
private static void LogTransactionFinished(
MethodInvocationEventArgs eventArgs, Account account)
{
Console.WriteLine(string.Format(
"Finished {2} transaction on account {0}.
Balance after the operation is ${1}.",
account.Number,
account.CurrentBalance,
eventArgs.Method.Name));
}
}
}
Repare que primeiro logamos o início da transação (linha 17), depois os detalhes sobre parâmetros envolvidos na transação (linha 18), só então prosseguimos com a chamada do método original (linha 19), e por último logamos o estado final de nossa conta (linha 20). Com isso obtivemos um comportamento idêntico ao que tínhamos anteriormente com dois aspectos, porém utilizando apenas um. Por fim, basta ajustarmos a classe “Account” para não mais utilizar o aspecto “ao redor”:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Codevil.MACSkeptic.PostSharpExample.Aspects;
namespace Codevil.MACSkeptic.PostSharpExample.Entities
{
[LogAccountTransactionDetails(
AttributeTargetMembers = "regex:(Deposit)|(Withdraw)")]
public class Account
{
public long Number { get; set; }
public string Owner { get; set; }
public decimal CurrentBalance { get { return this.currentBalance; } }
private decimal currentBalance;
private const decimal DefaultInitialBalance = 0;
public Account(long number, string owner)
: this(number, owner, DefaultInitialBalance)
{
}
public Account(long number, string owner, decimal initialBalance)
{
this.Number = number;
this.Owner = owner;
this.currentBalance = initialBalance;
}
public Account Deposit(decimal howMuch)
{
this.currentBalance += howMuch;
return this;
}
public Account Withdraw(decimal howMuch)
{
this.currentBalance -= howMuch;
return this;
}
}
}
Ou seja, mesmo em um pequeno exemplo como este, substituímos pelo menos 6 linhas de código “alien” da classe “Account” original (considerando a melhor implementação possível sem aspectos, onde os métodos de log tivessem sido movidos para outra classe) que estavam relacionadas com a preocupação de manter um log de transações. No lugar delas, adicionamos apenas um atributo declarando um aspecto de comportamento esperado quanto às transações. Ressaltando, para evitar mal entendidos: a vantagem não é necessariamente escrever menos código, mas sim separar claramente as responsabilidades e escrever o código onde ele deve estar.
Fico por aqui, espero que tenham gostado e percebido o quanto aspectos podem facilitar o desenvolvimento e melhorar a manutenibilidade de código. Deixo uma recomendação final para que os interessados em usar aspectos em seus projetos leiam com atenção a documentação do PostSharp (ou de qualquer outra ferramenta que venham a usar) para conferir o que pode ser feito em termos de otimização e detalhes sobre performance (o PostSharp oferece vários recursos de inicialização de aspectos em tempo de compilação, diminuindo o overhead em tempo de execução).
Como sempre, o código base deste artigo está disponível no meu github, e pode ser baixado aqui. Sintam-se à vontade para deixar suas dúvidas, críticas e sugestões nos comentários e até a próxima
.
Referências:
Leituras recomendadas: