依赖注入(Dependency Injection)模式的学习(1)——引子

废话不说,直接进入正题。我们来看下面的代码:

    public class UserRepository
    {
        //...
        public User GetUserByUserNameAndPassword(string userName, string passwordMD5)
        {
            //...
        }

        public bool ChangePassword(User user, string oldPassword, string newPassword)
        {
           //...
        }
    }

这是我们定义的数据访问层的类,示例中包含登录和修改密码两个方法。

现在,我们的Controller利用数据访问层的类进行用户的登录和修改密码操作,代码如下:

    [Authorize]
    [InitializeSimpleMembership]
    public class AccountController : Controller
    {

        //  ......
        // (其他代码省略)

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public ActionResult Login(LoginModel model, string returnUrl)
        {
            UserRepository user_repository = new UserRepository();
            User user = user_repository.GetUserByUserNameAndPassword(model.UserName,model.Password);

            if (ModelState.IsValid && user != null)
            {
                Session["AuthenticatedUser"] = user;
                return RedirectToLocal(returnUrl);
            }
            ModelState.AddModelError("", "提供的用户名或密码不正确。");
            return View(model);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult ChangePassword(LocalPasswordModel model, string returnUrl)
        {
            User logined_user = Session["AuthenticatedUser"] as User;
            if (logined_user == null)
                return RedirectToAction("Login", "Account");

            if (model.NewPassword != model.ConfirmPassword)
            {
                ModelState.AddModelError("", "密码和确认密码不匹配。");
                return View(model);
            }

            UserRepository user_repository = new UserRepository();

            if (user_repository.ChangePassword(logined_user, model.OldPassword, model.NewPassword))
                return RedirectToLocal(returnUrl);
            else
            {
                ModelState.AddModelError("", "密码修改失败。旧密码可能不正确。");
                return View(model);
            }
        }
    }

这是一个很常规的实现方式,逻辑层的方法实例化数据访问层的类,然后进行需要的操作。对于中小型系统,这并没有什么不好的地方。但是对于大型的企业级应用,它就呈现出了如下不足:

 维护的困难性。

UserRepository这个类会被很多逻辑(Controller)用到,比如用户设置(UserPreferenceController)、用户管理(UserManagementController)等等。正如上面给的示例一样,每一个用到有关UserRepository的类,都需要负责实例化UserRepository。这样,当UserRepository类的构造方式发生改变时,所有的相关类都需要进行修改。在一个大型系统中,这种Repository类可能多达50个,它们在逻辑层的调用也非常频繁,维护起来是难以想象的。

单元测试的困难性。

由于AccountController直接依赖于UserRepository类,因此需要保证UserRepository类正常工作(同时也需要确保数据库中的数据是正确的)之后,才能对AccountController进行单元测试。

无法支持后期绑定。

由于UserRepository在AccountController中的使用是在编译时确定的,因此我们无法实现后期绑定。假如我们需要用别的实现方式(比如Oracle数据库而不是SQL Server)时,必须修改代码并且重新编译项目。

增强功能会引起大量的修改。

假如我们需要在每个数据访问层的类中添加日志记录功能时(或者需要添加Listener,比如现在能在文本文件中记录日志,我们需要增加对Event Viewer的支持),需要大量修改所有的Repository的实现。

此时,接口就能表现出它的用武之地。我们为UserRepository定义如下的接口:

    interface IUserRepository
    {
        User GetUserByUserNameAndPassword(string userName, string passwordMD5);
        bool ChangePassword(User user, string oldPassword, string newPassword);
    }

相对应地,UserRepostory(代码省略)和AccountController做相应的改变:

    public class AccountController : Controller
    {
        private readonly IUserRepository _userRepository;

        public AccountController(IUserRepository userRepository)
        {
            this._userRepository = userRepository;
        }

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public ActionResult Login(LoginModel model, string returnUrl)
        {
            User user = this._userRepository.GetUserByUserNameAndPassword(model.UserName,model.Password);

            if (ModelState.IsValid && user != null)
            {
                Session["AuthenticatedUser"] = user;
                return RedirectToLocal(returnUrl);
            }

            ModelState.AddModelError("", "提供的用户名或密码不正确。");
            return View(model);
        }
    }

这样,上面所说的问题就可以得到初步解决:

逻辑层的类不需要知道Repository如何实现(实例化),通过接口进行访问。(在此我们还没有给出何处、何时、如何实例化Repostory类,这是下一篇日志要讨论的问题。而且,这也是依赖注入的一个重点内容。)由此看来,更改Repository类的构造方式不会影响到其他层的逻辑。
单元测试、或者这两个类由不同的人开发,可以并行进行。实现AccountController类的人不必等到UserRepository(或其他类)开发完后才进行,可以自行写一个很简单的类并实现IUserRepository接口,返回一些简单的元数据即可进行开发和单元测试。
由于AccountController只是引用的接口,这就意味着其他部分可以进行替换,此时不需要更改AccountController的逻辑。
负责实例化UserRepository类的部分可以提供其他额外的服务,例如控制实力对象的生命周期,决定是否采用单例模式等。
支持后期绑定:应用程序可以在运行时动态决定采用哪一个类来IUserRepository接口。例如我们写了UserRepositorySQL和UserRepositoryOracle实现这两个接口,我们可以根据配置文件决定采用哪个类,从而实现数据库的可配置。
可以使用装饰模式来实现日志记录等额外功能,而不需要更改逻辑层和数据访问层的任何代码。

这种设计软件结构的思路叫做Loosly Coupled Design(松耦合设计),当然并不是说所有的部分都要松耦合。正如微软说的:

You should try to identify the parts of an application that are likely to change in the future and then decouple them from the rest of the application in order to minimize and localize the impact of those changes.

—-By M$

依赖注入是一个很复杂的东西,并不是像上面那样实现一个接口那么简单。但是由这个例子,不难想象Dependency Injection模式需要提供的功能(相对应地,微软开发的这个企业级类库叫做Unity)。这只是入门,关于依赖注入的具体内容在后面的内容中会慢慢讲到。

刀之魂所在的项目组的产品正使用了这种设计思路,也用到了Unity。刀之魂周五开始请假回去休息十天,那么等我回来后再继续依赖注入的话题吧。

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