这是我开播第一篇,朋友们多多支持、捧场,谢谢。
引子
地是空虚混沌.渊面黑暗. 神的灵运行在水面上。
神说、要有光、就有了光。
神看光是好的、就把光暗分开了。
神称光为昼、称暗为夜.有晚上、有早晨、这是头一日。
——引至《圣经.神创造天地》
关键词: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时,使用空模式对象代替后会发生什么有趣的事情。