依赖注入(Dependency Injection)模式的学习(2)——初窥门径

在老早以前咱就非常重视软件的测试——特别是自动化的测试。首先是单元测试,再就是业务上、界面上的一些逻辑方面的验证。无论是哪种,都对模块之间的低耦合性要求非常之高。

假如我们有一个比较简单的系统,分为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>();

依赖注入的具体内容在这个系列的下一篇日志中说明。

✏️ 有任何想法?欢迎发邮件告诉老夫:daozhihun@outlook.com