在网上看到很多文章提到面向方面编程(Aspect-Oriented Programming),但一直没有搞清楚这样做有什么好处,为什么要使用AOP呢?
问题: 尽管面向对象编程与面向过程相比减少了代码的重复,但是它仍然留下了大量的重复代码。面向对象设计有助于最小化应用程序逻辑的代码重复,但是对于实现横切关注点的代码仍然很难实现模块化,例如日志,虽然我们可以使用类似于log4Net这样的类库来灵活的记录日志, 但是记录日志的代码却遍布于程序之中,其中有大量的重复。
使用AOP却可以很好的解决这方面的问题。
举个例子来说明一下这个问题:
假设有一个账户类,里面有存钱和取钱的简单操作,如下
/// <summary>
/// 账户类
/// </summary>
public class Account
{
/// <summary>
/// 余额
/// </summary>
public decimal Balance { get; set; }
/// <summary>
/// 存钱
/// </summary>
/// <param name="amount"></param>
public void Deposit(decimal amount)
{
this.Balance += amount;
}
/// <summary>
/// 取钱
/// </summary>
/// <param name="amount"></param>
public void Withdraw(decimal amount)
{
this.Balance -= amount;
}
/// 账户类
/// </summary>
public class Account
{
/// <summary>
/// 余额
/// </summary>
public decimal Balance { get; set; }
/// <summary>
/// 存钱
/// </summary>
/// <param name="amount"></param>
public void Deposit(decimal amount)
{
this.Balance += amount;
}
/// <summary>
/// 取钱
/// </summary>
/// <param name="amount"></param>
public void Withdraw(decimal amount)
{
this.Balance -= amount;
}
}
现在需要记录一下日志,简单的方法如下,现在只是在Console中输出日志,实际项目中可能使用其它的日志模块来记录。
/// <summary>
/// 存钱
/// </summary>
/// <param name="amount"></param>
public void Deposit(decimal amount)
{
Console.WriteLine("Start Deposit: " + amount.ToString());
this.Balance += amount;
Console.WriteLine("End Deposit");
/// 存钱
/// </summary>
/// <param name="amount"></param>
public void Deposit(decimal amount)
{
Console.WriteLine("Start Deposit: " + amount.ToString());
this.Balance += amount;
Console.WriteLine("End Deposit");
}
如果所有的方法都需要记录日志,则所有的方法需要像上面一样加上这些代码。
这种方法有很多问题:
- 有大量的代码重复,不利于代码的长期维护。
- 核心代码和辅助日志代码混合在一起,日志代码分散了对方法核心逻辑的注意力,影响了方法的可读性。
- 如果需要移除日志代码,则面临着不小的工作量。
- 如果需要记录更多的信息,例如类名,则需要手工添加漏掉的类名。
- 如果决定记录异常,也面临着同样的问题,必须在很多地方重复记录。
使用像log4Net的类库来记录日志,虽然可以通过更改配置来关闭日志功能而不用修改任何代码,但是核心代码和辅助日志代码仍然混合在一起。
使用AOP后, 我们可以将横切代码分离到几个方面中,而且很容易将这些方面应用于需要它们的类和方法上,除此之外, AOP还有大量的功能可以将业务规则从核心逻辑中分离出来。
下面来使用AOP来分离日志
修改后的代码如下
public interface IAccount
{
decimal Balance { get; set; }
void Deposit(decimal amount);
void Withdraw(decimal amount);
}
{
decimal Balance { get; set; }
void Deposit(decimal amount);
void Withdraw(decimal amount);
}
public class Account : Business.IAccount
{
/// <summary>
/// 余额
/// </summary>
public decimal Balance { get; set; }
/// <summary>
/// 存钱
/// </summary>
/// <param name="amount"></param>
public void Deposit(decimal amount)
{
this.Balance += amount;
}
/// <summary>
/// 取钱
/// </summary>
/// <param name="amount"></param>
public void Withdraw(decimal amount)
{
this.Balance -= amount;
}
{
/// <summary>
/// 余额
/// </summary>
public decimal Balance { get; set; }
/// <summary>
/// 存钱
/// </summary>
/// <param name="amount"></param>
public void Deposit(decimal amount)
{
this.Balance += amount;
}
/// <summary>
/// 取钱
/// </summary>
/// <param name="amount"></param>
public void Withdraw(decimal amount)
{
this.Balance -= amount;
}
}
除了Account类实现接口IAccount外,什么都没有变化。
日志功能的实现如下:
public class ConsoleLogAspect : IMethodInterceptor
{
public object Invoke(IMethodInvocation invocation)
{
Console.WriteLine("-- Start {0} --", invocation.Method.Name);
object returnValue = invocation.Proceed();
Console.WriteLine("-- End {0} --", invocation.Method.Name);
return returnValue;
}
{
public object Invoke(IMethodInvocation invocation)
{
Console.WriteLine("-- Start {0} --", invocation.Method.Name);
object returnValue = invocation.Proceed();
Console.WriteLine("-- End {0} --", invocation.Method.Name);
return returnValue;
}
}
客户端程序
class Program
{
static void Main(string[] args)
{
IApplicationContext ctx = ContextRegistry.GetContext();
IAccount account = (IAccount)ctx.GetObject("MyAccount");
account.Balance = 10;
account.Deposit(15);
account.Withdraw(20);
Console.ReadKey();
}
{
static void Main(string[] args)
{
IApplicationContext ctx = ContextRegistry.GetContext();
IAccount account = (IAccount)ctx.GetObject("MyAccount");
account.Balance = 10;
account.Deposit(15);
account.Withdraw(20);
Console.ReadKey();
}
}
注意:代码中客户端程序只使用接口,不使用Account类
配置文件内容, 通过配置文件来配置IAccount。
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects"/>
</context>
<objects xmlns="http://www.springframework.net" >
<object id="ConsoleLogAspect" type="Spring.Aop.Support.NameMatchMethodPointcutAdvisor, Spring.Aop">
<property name="Advice">
<object type="Aspects.ConsoleLogAspect, Aspects" />
</property>
<property name="MappedNames">
<list>
<value>Deposit</value>
<value>Withdraw</value>
</list>
</property>
</object>
<object id="MyAccount" type="Spring.Aop.Framework.ProxyFactoryObject">
<property name="Target">
<object type="Business.Account, Business" />
</property>
<property name="InterceptorNames">
<list>
<value>ConsoleLogAspect</value>
</list>
</property>
</object>
</objects>
</spring>
</configuration>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects"/>
</context>
<objects xmlns="http://www.springframework.net" >
<object id="ConsoleLogAspect" type="Spring.Aop.Support.NameMatchMethodPointcutAdvisor, Spring.Aop">
<property name="Advice">
<object type="Aspects.ConsoleLogAspect, Aspects" />
</property>
<property name="MappedNames">
<list>
<value>Deposit</value>
<value>Withdraw</value>
</list>
</property>
</object>
<object id="MyAccount" type="Spring.Aop.Framework.ProxyFactoryObject">
<property name="Target">
<object type="Business.Account, Business" />
</property>
<property name="InterceptorNames">
<list>
<value>ConsoleLogAspect</value>
</list>
</property>
</object>
</objects>
</spring>
</configuration>
运行结果:
日志已经被记录下来了。
对比一下使用AOP的好处:
- 核心代码中不在包含记录日志的代码, 类更专注于它的职责。
- 如果想修改日志的记录方式, 不需要修改Account类,只需要修改相应的配置文件和日志的实现,修改工作量小。
- 使用这种方式可以动态的修改业务规则而不用修改Account类, 也不用重新编译发布。例如Account.WithDraw()方法的核心是减少账户余额,假设程序布署后客户又来一新的需求,当账户余额小于50元时,发邮件通知客户,此时只需要像添加日志一样实现这一功能,配置到程序上去就好了。最适合移入方面的业务规则是那些在实现核心逻辑的同时也需要实现的二级逻辑的规则。
- 实现AOP的同时需要依赖注入, 这又会减少模块间的依赖。
- 依赖注入要用到Ioc容器, 这个又可以代替工厂类,这算是又一个好处吧
大家觉得呢?
下载 AOP Demo文件