在老早以前咱就非常重视软件的测试——特别是自动化的测试。首先是单元测试,再就是业务上、界面上的一些逻辑方面的验证。无论是哪种,都对模块之间的低耦合性要求非常之高。
假如我们有一个比较简单的系统,分为UI层A,业务层B,数据访问层C,业务层B还要和外部系统D通信。以对B进行单元测试为例,在这种情况下,设计软件的时候我们必须要考虑到,B不能依赖于A、C和D,更甚者,B中的任何一个类都不应该依赖于B中的其他类。
举一个简单的例子吧,假如我们在B层有一个ATM类,其中有一个bool ValidateCard(string cardID, string password)方法,UI层会调用这个方法判断用户提供的卡号和密码是否匹配;然后,我们的ATM类会:
- 调用C层的UserRepository类去数据库中取与卡号匹配的用户;
- 将取出来的用户传给D系统的UserService服务的bool ValidateUser(Int64 userId)判断这个用户是否在银行的黑名单里面;
- 调用C层的CardRepository类去数据库中取该卡片的密码,并与UI层提供的密码进行匹配。
酱紫的话,我们应该如何设计B这个类呢?
方案一:
public class ATM
{
public bool ValidateCard(string cardId, string password)
{
UserRepository user_repository = new UserRepository();
CardRepository card_repository = new CardRepository();
UserService user_service = new UserService(ATMHelper.UserServiceAddress);
Int64 user_id = user_repository.GetUserIdByCardId(cardId);
if (user_id <= 0) return false; //Not found in database
user_service.OpenConnection();
if (!user_service.ValidateUser(user_id))
return false;
string correct_password = card_repository.GetPassword(user_id);
return correct_password == password;
}
}
酱紫显然是不行的,因为模块之间的耦合性太强了,如果没有C和D,B完全无法工作。这样会导致以下两个问题:
- 假如我们给ValidateCard做单元测试,那么得先把数据库和D系统搭建好。
为此,我们必须先在数据库里添加一些模拟的数据,并且保证UserRepository和CardRepository的正确性,同时还要保证D系统能正常工作,才能对B进行单元测试。这样显然是不合理的:假如我们的数据库服务器down掉了,我们的领导会在报告上看到B的单元测试通不过,会以为B有问题。
实际上,我们单元测试的环境不一定会有数据库和D系统,即使有,我们必须花费很大的代价去维护数据库与程序之间的一致性。假如我们的方法会往数据库里写数据,那么我们每次运行单元测试前还必须将数据库恢复到最初始的状态。
- 假如我们有两种客户,一种使用数据库存储数据,另一种使用Excel存储数据;这样的话,要么要在Repository相关的类中进行这样的判断,要么在业务层中进行判断。
这样的坏处是,我们必须向每个客户提供这两种逻辑(万一有第三个客户要从别的系统取呢?);其次,Repository的每一个类也不得不进行这种逻辑判断。
- B要负责管理C的实例对象。
B必须要知道如何生成C(即C的构造方法的参数列表),如果C的构造方式发生变化,不得不修改B中相关的所有代码。其次,例子中每访问一次这个方法,B就会生成其他三个类的实例。如果我们希望一个类是单例的,或者实例对象的数量要收到约束,那么不得不修改B或C的代码。
方案一完全被否定了,那么就再想一个方法吧。使用工厂+接口,这样乃就没话说了吧?
方案二:
public bool ValidateCard(string cardId, string password)
{
IUserRepository user_repository = ClassFactory.CreateUserRepository();
ICardRepository card_repository = ClassFactory.CreateCardRepository();
IUserService user_service = ClassFactory.CreateUserService(ATMHelper.UserServiceAddress);
Int64 user_id = user_repository.GetUserIdByCardId(cardId);
if (user_id <= 0) return false; //Not found in database
user_service.OpenConnection();
if (!user_service.ValidateUser(user_id))
return false;
string correct_password = card_repository.GetPassword(user_id);
return correct_password == password;
}
我们这里提供的例子是一个最简单的工厂,不过这样子倒是会有不少的帮助。
借助接口,我们已经把B层和C层完全剥离开。在我们为B写单元测试的时候,我们可以定义模拟的Repository,比如MockUserRepository,让它实现IUserRepository接口,我们在MockUserRepository里直接返回一些模拟数据(或者从XML读取一些模拟数据),这样子的话B就不会依赖与C以及数据库了:我们只需要写好ClassFactory的逻辑,就能达到单元测试的目的。
不过,这也有很多问题,具体实现中会逐渐遇到:
- 工厂类会非常复杂。
- 要进行模块替换时,需要重新编译整个项目文件。
可以看出,无论采用哪种工厂方法还是有一个共同的问题:我们的类还是会依赖于工厂类,还是必须编写程序来维护类的实例化过程和相关的依赖,而且这些依赖隐藏在工厂类的具体实现中,使得结构不清晰,也不利于维护。
使用术语来说,上面的这些实现方式都是一种“拉”(pull)的模式,在需要时我们从工厂中“创造”一个实例对象;为了使类之间的依赖完全消除,依赖注入采用的是“推”(push)模式,让其他的类(也就是依赖注入的框架)把依赖“注入”进来,我们完全不管如何实例化这些对象。下面是例子:
public class ATM
{
private readonly IUserRepository _userRepository;
private readonly ICardRepository _cardRepository;
private readonly IUserService _userService;
public ATM(IUserRepository userRepository, ICardRepository cardRepository, IUserService userService)
{
this._userRepository = userRepository;
this._cardRepository = cardRepository;
this._userService = userService;
}
public bool ValidateCard(string cardId, string password)
{
Int64 user_id = _userRepository.GetUserIdByCardId(cardId);
if (user_id <= 0) return false; //Not found in database
_userService.OpenConnection();
if (!_userService.ValidateUser(user_id))
return false;
string correct_password = _cardRepository.GetPassword(user_id);
return correct_password == password;
}
}
这种情况下,依赖注入就突现出它的作用了。
这里我写了一个最简单的范例,支持向接口注册类(单例或非单例)的具体实现。这只是一个很简单的小范例,它只能支持默认构造方法,对于多层依赖只能通过属性注入的方式实现。由此可以看出依赖注入的框架编写起来并不简单,因此在企业中通常是使用现成的成熟的框架。
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Linq;
using System.Reflection;
using System.Text;
namespace CommonTools
{
/// <summary>
/// Provides simple factory to create new instance of registered interface, or get the singleton instance.
/// </summary>
public class IocClassFactory
{
//This dictionary stores registered interface type and singleton object.
private ConcurrentDictionary<Type, InternalClassReferences> _classReferences;
private static object _lockObject = new object();
private static Type[] _emptyParameterTypes = { };
private static Object[] _emptyParameterObjects = { };
//This class itself is singleton
private static IocClassFactory _instance;
/// <summary>
/// Gets the unique instance of IocClassFactory.
/// </summary>
public static IocClassFactory Instance
{
get
{
lock (_lockObject)
{
if (_instance == null)
_instance = new IocClassFactory();
return _instance;
}
}
}
private IocClassFactory()
{
this._classReferences = new ConcurrentDictionary<Type, InternalClassReferences>();
}
public void RegisterInterface<T, R>() where R : class, new()
{
RegisterInterface<T, R>(false);
}
public void RegisterInterface<T, R>(bool m_isSingleton) where R: class, new()
{
System.Type t = typeof(T);
if (this._classReferences.ContainsKey(t))
{
lock (_lockObject)
{
InternalClassReferences reference = this._classReferences[t];
reference.IsSingleton = m_isSingleton;
this._classReferences[t] = reference;
}
}
else
this._classReferences.TryAdd(t, new InternalClassReferences(m_isSingleton, typeof(R)));
}
public void RemoveInterface<T>()
{
InternalClassReferences reference;
this._classReferences.TryRemove(typeof(T), out reference);
}
/// <summary>
/// Gets the instance of the T interface type. Depending on the RegisterInterface method, returns a new object or existing singleton object.
/// </summary>
/// <typeparam name="T">The type of the interface to resolve.</typeparam>
/// <returns>A new instance of existing instance of T interface.</returns>
public T Resolve<T>() where T : class
{
System.Type t = typeof(T);
if (this._classReferences.ContainsKey(t))
{
lock (_lockObject)
{
InternalClassReferences reference = this._classReferences[t];
if (reference.IsSingleton)
{
if (reference.SingletonObject != null)
return (T)reference.SingletonObject;
else
{
ConstructorInfo constructor = reference.Constructor;
T instance = (T)constructor.Invoke(_emptyParameterObjects);
reference.SingletonObject = instance;
this._classReferences[t] = reference;
return instance;
}
}
else
{
ConstructorInfo constructor = reference.Constructor;
Object new_object = constructor.Invoke(_emptyParameterObjects);
//reference.Objects.Add(new_object);
return (T)new_object;
}
}
}
else
throw new ArgumentException("The interface or type has not been registered.", t.Name);
}
internal class InternalClassReferences
{
public bool IsSingleton;
public object SingletonObject;
public System.Type RegisteredType;
public ConstructorInfo Constructor;
public InternalClassReferences(bool m_isSingleton, Type m_registeredType)
{
this.IsSingleton = m_isSingleton;
SingletonObject = null;
RegisteredType = m_registeredType;
System.Type[] array = {};
Constructor = m_registeredType.GetConstructor(array);
//if (m_isSingleton)
// Objects = null;
//else
// Objects = new List<object>();
}
}
}
}
使用这个类,我们可以使用代码将具体的类型绑定到接口,比如:
IocClassFactory.Instance.RegisterInterface<IUserRepository, UserRepository>();
在需要这个接口的实例对象时,使用下面的代码就行了:
IUserRepository user_repository =
IocClassFactory.Instance.Resolve<IUserRepository>();
依赖注入的具体内容在这个系列的下一篇日志中说明。