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

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

跟踪问题

经过观察,你发现方法Do执行缓慢,可能有性能问题,由于这是一个线上的版本,你没法进行Debug,因此你经过日志的形式来追踪执行步骤:github

class Foo1
{
    void Do() {
        //日志记录开始
        //性能监控开始
        DoSomething();
        //日志记录结束
        //性能监控结束
    }
}复制代码

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

class Foo2
{
    void Handle() {
        //日志记录开始
        //性能监控开始
        DoSomething();
        //日志记录结束
        //性能监控结束
    }
}复制代码

上述两段代码,虽然看起来很丑,但毕竟是能解决问题,可是代码的风格不怎么让人舒服:安全

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

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

class Bar
{
    void Do() {
        common.BeginLog();
            common.BeginWatch();
                common.BeginTransaction();
                    foo.Do();
                common.Commit();
            common.EndWatch();
        common.EndLog();
    }
}复制代码

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

AOP面向切面编程的引入

什么是AOP?工具

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

若是你是第一次接触『面向切面编程』,可能这些概念太过复杂和笼统,我建议先翻阅相关书籍、博客,最好对AOP有必定的了解。性能

什么是『切面』?测试

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

  • 在现实生活中,一个大西瓜,一刀切下去,一分为二,这破坏了它的总体性,瓜瓤彻底暴露在你面前,而后你能够为所欲为的吃它,但不切开,你确定吃不了它。
  • 在计算机世界,一个类(对象)包裹了若干方法,它像西瓜那样也是一个总体,你将方法做为一个切入点,一刀切下去,方法彻底暴露在你面前,你能够按照你所需的要求进行拦截,但不切开,你确定拦截不了。固然,计算机世界里确定不会让你拿一把刀来切一个对象,这是一个更加抽象的概念,下面会阐述

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

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

class Order
{
    public virtual void Add() {
        Console.WriteLine("新增订单");
    }
}

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

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

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

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通常是经过框架(好比Spring)在运行时动态建立的,因此叫动态代理对象。客户端不知道真正调用的对象实际上是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类,指定须要被代理的类的方法,以及拦截策略:

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);
    }
}复制代码

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

public interface IInvocationHandler
{
    void PreProcess();
    object Invoke(object proxy, MethodInfo method, object[] args);
    void PostProcess();
}复制代码

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

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方法进行拦截,即执行先后进行日志的打印,你能够这样来使用:

Proxy.Instance.SetTarget(repository)
    .SetMethod("Test")
    .SetArgs(new object[] {})
    .SetInvocationHandler(new LogInvocationHandler())
    .Invoke();复制代码

小结

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

欢迎关注个人公众号:

相关文章
相关标签/搜索