Unity应用架构设计—AOP思想的实践

思考并回答以下问题:

  • AOP的应用场景是什么?
  • AOP在进行真正的调用前、后执行一段中间逻辑。怎么理解?
  • 什么是切面?

本章涵盖:

  • 跟踪问题
  • AOP面向切面编程的引入
  • 在Unity中使用AOP思想

想象一下,当程序所有的业务逻辑都完成的时候,你可能还来不及喘口气,紧张的测试即将来临。你的Boss告诉你,虽然程序没问题,但某些方法为什么执行这么慢,性能堪忧。领会了Boss的意图之后,漫长的排查问题开始了。你会写日志,或者是其他工具来追踪原因。那么如何以一种优雅的形式,并且不侵入业务代码的形式来跟踪呢?这正是本文的内容。

跟踪问题

通过观察,你发现方法Do执行缓慢,可能有性能问题,因为这是一个线上的版本,你无法进行Debug,所以你通过日志的形式来追踪执行步骤:

1
2
3
4
5
6
7
8
9
10
11
class Foo1
{
void Do()
{
//日志记录开始
//性能监控开始
DoSomething();
//日志记录结束
//性能监控结束
}
}

看起来不错,解决问题之后,测试又发现另一个方法Handle貌似也有问题,然后你一样画葫芦,虽然麻烦了一点,但总归是有解决方案:

1
2
3
4
5
6
7
8
9
10
11
class Foo2
{
void Handle()
{
//日志记录开始
//性能监控开始
DoSomething();
//日志记录结束
//性能监控结束
}
}

  • 代码没有重用,比如日志记录,性能监控,它们实则是做了一样的事情。
  • 日志记录,性能监控代码侵入了核心的业务逻辑,造成了混乱。

知道了问题之后,第二种风格的代码出现了:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Bar
{
void Do()
{
common.BeginLog();
common.BeginWatch();
common.BeginTransaction();
foo.Do();
common.Commit();
common.EndWatch();
common.EndLog();
}
}

看似是个不错的方案,但实际上还是没解决本质问题。虽然将日志,监控放到了Common中,但每个方法还是要写这一大堆和业务无关的代码,这压根什么也没解决,这个方法的层次结构如下图所示:

AOP面向切面编程的引入

什么是AOP?

  • 名称来源->『Aspect Oriented Programming』的缩写,中文翻译即『面向切面编程』。
  • 应用场景->为日志记录,性能监控,安全控制,事务处理,异常处理等与具体业务逻辑无关,却又需要在全局范围执行的功能提供了一种良好的重用并且和业务逻辑解耦。
  • 核心理念->AOP的思想是围绕着切面进行的,所谓的『切面』就是对目标对象的某种操作进行拦截,在系统其他部分调用目标对象的某种操作时拦截这些调用,并在进行真正的调用前、后执行一段中间逻辑。
  • 实现方式->AOP的实现方式被分为『静态织入』和『动态织入』。采用『静态织入』方式通过扩展编译器对代码的中间语言IL插入代码,从而对目标对象进行拦截。『动态织入』则在运行时创建动态代理对象来拦截。

什么是『切面』?

『面向切面编程』总共6个字,想必最难理解的还是『切面』两字吧。

在现实生活中,一个大西瓜,一刀切下去,一分为二,这破坏了它的整体性,瓜瓤完全暴露在你面前,然后你可以随心所欲的吃它,但不切开,你肯定吃不了它。

在计算机世界,一个类(对象)包裹了若干方法,它像西瓜那样也是一个整体,你将方法作为一个切入点,一刀切下去,方法完全暴露在你面前,你可以按照你所需的要求进行拦截,但不切开,你肯定拦截不了。当然,计算机世界里肯定不会让你拿一把刀来切一个对象,这是一个更加抽象的概念,下面会阐述。

所以『切面』是一种横向的拦截,而非纵向继承的机制。

使用纵向继承的方式来拦截方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Order
{
public virtual void Add()
{
Console.WriteLine("新增订单");
}
}

class OrderExt : Order
{
public override void Add()
{
//开启事务
BeginTransaction();
base.Add();
//提交事务
Commit();
}
void BeginTransaction() { }
void Commit() { }
}

缺点上面已经提到过了,不再重复。

使用AOP横向拦截方法,通过动态代理类来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Order
{
public virtual void Add()
{
Console.WriteLine("新增订单");
}
}

class TransactionUtility
{
public void BeginTransaction() { }
public void Commit() { }
}

class OrderProxy
{
public Order o;
public TransactionUtility u;
public void Add()
{
u.BeginTransaction();
o.Add();
u.Commit();
}
}

当然这个OrderProxy一般是通过框架在运行时动态创建的,所以叫动态代理对象。客户端不知道真正调用的对象其实是OrderProxy。整个过程如下所示:

1.) Target:目标类,需要被代理的类
2.) Join Point:连接点,指那些可能被拦截到的方法。
3.) Point Cut:切入点,已经被增强的连接点
4.) 切面类:提供了事务管理,日志记录,性能监控等公共操作的类
5.) Advice:通知点,用来增强代码
6.) Weaving:织入,将Advice应用到目标对象Target,是创建新的代理对象Proxy的过程
7.) Proxy:代理类
8.) Aspect:切面,是切入点PointCut和通知Advice的结合,2点确定一条线,多条线组合成面

在Unity中使用AOP思想

很遗憾,在Unity中没有好的AOP框架,虽然.NET有很多AOP框架,但考虑到Unity的跨平台,很多技术并不兼容。所以我以另一种形式间接的实现了AOP。

理解了AOP之后,实际上我只关注两点:

  • 既然没有框架来动态创建代理对象,那我只能自己创建。
  • 代理对象是用来拦截方法的,你需要告诉代理对象怎么来拦截方法,所以通过委托的形式来指定策略。

定义一个Proxy类,指定需要被代理的类的方法,以及拦截策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Proxy
{
public static Proxy Instance = new Proxy();

private IInvocationHandler _invocationHandler;
private object _target;
private string _method;
private object[] _args;

private Proxy()
{
}

public Proxy SetInvocationHandler(IInvocationHandler invocationHandler)
{
_invocationHandler = invocationHandler;
return this;
}

public Proxy SetTarget(object target)
{
_target = target;
return this;
}

public Proxy SetMethod(string method)
{
_method = method;
return this;
}

public Proxy SetArgs(object[] args)
{
_args = args;
return this;
}

public object Invoke()
{
var methodInfo = _target.GetType().GetMethod(_method);
return _invocationHandler.Invoke(_target, methodInfo, _args);
}
}

拦截策略是个公共接口,提供策略:

1
2
3
4
5
6
public interface IInvocationHandler
{
void PreProcess();
object Invoke(object proxy, MethodInfo method, object[] args);
void PostProcess();
}

实现接口,就可以自定义拦截方法,一个日志的策略如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class LogInvocationHandler:IInvocationHandler
{
public void PreProcess()
{
LogFactory.Instance.Resolve<ConsoleLogStrategy>().Log("Pre Process");
}

public object Invoke(object target, MethodInfo method, object[] args)
{
PreProcess();
var result= method.Invoke(target, args);
PostProcess();
return result;
}

public void PostProcess()
{
LogFactory.Instance.Resolve<ConsoleLogStrategy>().Log("Post Process");
}
}

假设你需要对repository对象的Test方法进行拦截,即执行前后进行日志的打印,你可以这样来使用:

1
2
3
4
5
Proxy.Instance.SetTarget(repository)
.SetMethod("Test")
.SetArgs(new object[] {})
.SetInvocationHandler(new LogInvocationHandler())
.Invoke();

小结

AOP思想是非常重要的重构手段,以不侵入的形式解耦业务逻辑和拦截方法。本质上是以横向扩展的形式替换了传统的纵向继承方式来实现。遗憾的是,在Unity中并没有好的AOP框架,我按照AOP的思想,简化了实现模式,以曲线的形式实现对方法的拦截。

0%