首页 > 代码库 > 利用AOP实现空模式的无缝使用 创世纪代码应用之一:一行代码让接口框架RUN起来

利用AOP实现空模式的无缝使用 创世纪代码应用之一:一行代码让接口框架RUN起来

这是我开播第一篇,朋友们多多支持、捧场,谢谢。

引子

地是空虚混沌.渊面黑暗. 神的灵运行在水面上。  
神说、要有光、就有了光。  
神看光是好的、就把光暗分开了。  
神称光为昼、称暗为夜.有晚上、有早晨、这是头一日。  

——引至《圣经.神创造天地》  


关键词:null,AOP,Spring.Net框架,空模式,面向接口编程,单元测试,方法拦截器

摘要:在我们编程的时候很难离开null,它给我们带来了很多麻烦。本文从新的视角利用AOP无缝使用空模式部分解决了这个问题,最重要的是可以使得我们的程序尽早运行起来……

 

问题由来

null在所有编程语言里面都是一个非常重要的概念,在不同的场景下有不同的含义。我们编程的时候很难离开它,但是它也给我们带来了很多麻烦。就像数学里面的0一样,只要是除法运算,必须保证分母不能为0,否则推导出的结论可能就是错误的。

我们可能不止一次编写如下语句:

if(employee!=null)……

或者

if(employee==null)……

没有办法,只要employee是null,如果不判断而直接使用了employee的任何成员都会产生异常,因为null是没有任何成员的。这样到处逻辑判断的代码不仅影响理解,而且如果忘记判断还可能会引起异常。有没有什么方法解决这个问题?

空模式概念

可以使用空模式对象代替null,避免大量的if(xx==null)判断语句,增强程序的健壮性。

如果我们在实现employee类型的时候按照同样的接口定义一个NullEmployee类型,使他的所有方法都返回缺省值,所有的集合属性都尽可能是不包含元素的空集合。那么在返回employee类型的方法没有查到符合条件的雇员时,返回的就不是null,而是NullEmploy的一个实例。那么这个实例虽然像普通雇员一样执行了所有的代码,但是对结果没有什么本质的影响。

更多此模式的论述参看《敏捷软件开发:原则、模式与实践(C#版)》的NULL OBJECT模式章节内容 ,不过我们是利用AOP无缝的使用从而达到我们的目的,并且大大提升了他的地位,在这一篇是绝对的第一主角。

对于空模式,我认为应该在源代码层面就杜绝其实例状态改变的可能,做法如下:方法如果有返回值则返回空集合或者缺省值,否则保持为空方法体;属性的set,get原则和上面相同。这样就算修改了空模式对象的属性,返回的还是缺省值。

在下面的实现中,我们仅仅利用了Aop拦截方法执行权的功能。而具有这个功能的框架非常多,其中Spring是近年来比较流行的轻量级框架,使用方法也非常简单,最关键的是它是开源的。

使用Spring框架实现的目的:

1为了扩展无缝使用空模式的应用场景;

2提供一个自动定义空模式类型的通用解决方案。

 

优点:

  • 使用缺省值暂时作为填充代码,使得设计成果早日运行起来 ,以后逐步使用实际代码填充;

  • 可以跨平台使用,这个原理在Java上面可以非常容易的实现;

  • 不需要定义空模式类型,使用更简单;

  • 代码少,逻辑清晰;

缺点:

  • 使用场景有限制,如果不是无参数构造器的集合或者接口,返回的还是null;

  • 需要依赖Spring框架;

  • 如果返回的空模式对象传入了第三方代码,而此代码在if(xx==null)情况下使对象本身或相关对象的状态有了改变,可能会有不合常规的结果。原因是一般为null时什么都不做,而这个第三方代码显然违背了常理;这样造成的bug很难发现。

原理

AOP(面向切面编程)的原理非常简单:可以在我们调用方法的前面,后面或者异常的时候添加一些验证、日志等等功能。用代码演示如下:

public void Method(int arg){

        //里面为实现实际功能的代码

}

//下面的方法一般与上面的不在同一个类里面。

public void Login(){

        //里面为实现登录功能的代码

}

public void Logoff(){

        //里面为退出功能的代码

}

使用AOP包装包含Method方法的对象后,我们执行Method方法的时候实际执行的是类似如下代码:

public object Invoke(IMethodInvocation invocation) {
         //这个拦截方式的功能最强大,其他拦截方式与本文无关,不作解释。
         xx.Login();//调用方法前插入的功能

         var obj=invocation .Proceed();//调用Method方法

         xx.Logoff();//调用方法后插入的功能

         return obj;

    }
invocation里面包含了调用Method方法的必须元素:返回类型,名称,参数列表等等信息。
而前面的代码之间是平等的顺序调用关系,三个方法是互不干扰的。我们这里则与平常的使用方式大不相同。我们取出包含的返回类型信息,然后判断为接口或集合则返回空模式对象。根本不调用var obj=invocation .Proceed();从而实现Spring创建的代理返回空模式对象的功能。

 

使用场景

  假设我们需要开发一套公司管理软件,功能是可以查询公司名称,员工情况。很明显,最少需要两个类型:公司和员工。这里处于演示目的,采用的语言为c#,不考虑设计是否符合真实逻辑。我们定义接口如下:

  public interface ICompany {

  string Name { get; }

  IList<IEmployee> Employees { get; }

  IEmployee GetEmployee(string name);

  string GetEmployeeStatus(IEmployee employee);

}

public interface IEmployee {

  string Name { get; }

  double Salay { get; }

  int Id { get; }

}

这里仅仅定义了两个接口,还没有写一行真正意义可以运行的代码,就认为这是一个构想中的空公司吧。如果细心,你会发现ICompany定义里面有多处IEmployee接口的使用,这就是面向接口编程实际应用。我们测试一下是否符合要求:

 [TestClass]

public class CompanyTest{

  [TestMethod]

  public void TestNullCompany(){

    var nullCompany = AdapterFactory.CreateNullInstance<ICompany>();

    TestNullCompany(nullCompany);

    TestNullEmployee(nullCompany);

  }

 

  private void TestNullEmployee(ICompany company){

    var nullEmployee = company.GetEmployee("Tom"); //无中生有的职工,空职工

    Assert.IsNotNull(nullEmployee); //空职工也是职工.

    Assert.IsNull(nullEmployee.Name); //空职工没有名字,合理.

    Assert.IsTrue(nullEmployee.Id == 0); //连身份号也是0,合理

    Assert.AreEqual(nullEmployee.Salay, 0); //果然,没有薪水.

  }

 

  private void TestNullCompany(ICompany nullCompany){

    Assert.IsNotNull(nullCompany); //虽然是空公司,还是公司。

    Assert.IsNull(nullCompany.Name); //空壳公司没有名分,合理.

    Assert.AreEqual(nullCompany.Employees.Count, 0); //没有职工,合理.

  }

}

在上面的代码中,真正有意义的只有这一句:

var nullCompany = AdapterFactory.CreateNullInstance<ICompany>();

调用一个工厂方法,创建一个实现了ICompany接口的nullCompany对象,其余测试代码是对此对象状态的判断。看到没有,我们仅仅定义了两个接口,连实现接口的类型都没有实现,可是已经可以对其逻辑进行判断和验证了。这就像设计一座大桥,需要首先根据图纸尺寸建造一个缩小版模型,然后用这个模型进行地震、风力、浪涌、撞击、共振等等破坏性实验,根据结果调整设计。而不能等大桥建造好了使用实物进行上述的实验。这个nullCompany对象虽然什么都不能做,但是它已经运行起来了,这才是非常重要的。

实现功能的后台代码

下面就让我们看看AdapterFactory类型这个幕后大哥的真容吧。

public static class AdapterFactory {
  /// <summary>
  /// 创建实现传入接口的实例,实际执行的代码为传入的对象。
  /// </summary>
  /// <typeparam name="T">创建实例需要实现的接口</typeparam>
  /// <param name="inst">与接口签名对应的对象</param>
  /// <param name="aroundInterceptor">拦截方式</param>
  /// <returns>实现了接口的实例</returns>
  private static T CreateAdapter<T>(object inst = null, IMethodInterceptor aroundInterceptor = null) {
    var objects = new[] { inst };
    if (inst == null)
      objects = new object[0];
    return (T)CreateAdapter(objects, aroundInterceptor, new[] { typeof(T) });
  }
  /// <summary>
  /// 创建实现传入接口的实例,实际执行的代码为传入的对象。
  /// </summary>
  /// <param name="inst">与接口签名对应的对象</param>
  /// <param name="aroundInterceptor">拦截方式</param>
  /// <param name="interfacTypes">创建实例需要实现的接口</param>
  /// <returns>实现了接口的实例</returns>
  private static object CreateAdapter(IEnumerable<object> inst, IMethodInterceptor aroundInterceptor, Type[] interfacTypes) {
    var proxy = new ProxyFactory(interfacTypes);
    if (inst != null)
      proxy.Target = inst;
    if (aroundInterceptor != null)
      proxy.AddAdvice(aroundInterceptor);
    var instance = proxy.GetProxy();
    return instance;
  }
  /// <summary>
  /// 利用接口创建一个空实例。
  /// </summary>
  /// <typeparam name="T">空实例实现的接口</typeparam>
  /// <returns>实现了T接口的空实例</returns>
  public static T CreateNullInstance<T>(){
    var type = typeof(T);
    if (!type.IsInterface)
      return default(T);
    return CreateAdapter<T>(null, new ReturnNullPattern());
  }
  /// <summary>
  /// 利用接口创建一个空实例。
  /// </summary>
  /// <param name="type">name="T">空实例实现的接口</param>
  /// <returns>实现了type接口的空实例</returns>
  public static object CreateNullInstance(Type type) {
    return CreateAdapter(null , new ReturnNullPattern(),new []{type});
  }
}
真正的核心是CreateAdapter方法,原理非常简单,使用Spring AOP的代理工厂创建一个包含实现接口的代理对象。其他方法都是对这个方法的包装,需要说明的只有CreateNullInstance<T>()里面的最后一句:
return CreateAdapter<T>(null,newReturnNullPattern());
它需要一个ReturnNullPattern类型的实例,此类型的代码如下:

初始版本拦截器

public class ReturnNullPattern : IMethodInterceptor {
  public virtual object Invoke(IMethodInvocation invocation) {
    var methodInfo = invocation.Method;
    var returnType = methodInfo.ReturnType;
    return NullInstance(null, returnType);
  }
  protected object NullInstance(object v, Type type){
    if (v != null) return v;
    if (type.IsInterface) {
      return AdapterFactory.CreateNullInstance(type);
    }
    if (type.GetInterfaces().Contains(typeof(IEnumerable))
        && type.IsClass) {
      return CreateNullCollection(type);
    }
    if (!type.IsClass){
      try{
        v = Activator.CreateInstance(type);
      }
      catch{
        
      }
    }
    return v;
  }
  private object CreateNullCollection(Type type){
    try {
      return Activator.CreateInstance(type);
    }
    catch{
        return null;
    }
  }
}
此类型实现了IMethodInterceptor接口,也就意味着代理对象nullCompany调用任意成员的时候都会执行此类型的Invoke(IMethodInvocation invocation)方法。注意这里拦截后与普通AOP编程的不同,它没有调用真正执行代码的方法,因为现在这个方法还不存在呢。invocation参数里面包含了调用信息,我们这里只需要使用它的返回类型。根据类型返回其缺省值,如果是接口则返回一个空模式的对象。如下:
if (type.IsInterface) {
      return AdapterFactory.CreateNullInstance(type);
    }
因此,把参数、返回值的类型用接口表示是非常关键的,这也符合近年来面向接口编程的趋向。其他代码非常简单,就不需要特别说明了。

小结:

一个方法的典型定义如下:ReturnType MethodName(Arg1Type arg1,……)本篇通过利用返回类型也就是ReturnType构建空模式对象的返回值,代替null,解决了对象为空时调用成员发生异常的问题。附带加强了读者面向接口编程的意识。当然,有时候返回null也是有必要的,我们可以在接口的对应成员上面添加一个特性,在拦截器里面判断是否有此特性,从而确定返回null还是空模式对象。限于篇幅,这个就留给读者自行完成了。下一篇则接着介绍如果执行方法的传入参数也就是arg1,……为null时,使用空模式对象代替后会发生什么有趣的事情。

 

利用AOP实现空模式的无缝使用 创世纪代码应用之一:一行代码让接口框架RUN起来