设计模式:依赖注入

1.背景

最近比较纠结,申请辞职后,一时走不了,因为还有一堆破事处理。我上一份职业,就是因为做得不开心,任性强行走人(一个月的薪水都不要了)。

这样做,真的好吗?作为过来人的体会,我以后肯定不会这样了,虽说走的潇洒,霸气,但会让一圈人脉断了,因为让你不好意思回头。

也许你不在乎,但确实在显摆个性的时候,形象也会受损的。有一句老话说:好聚好散!真是金玉良言啦。

连续奋战几天后,今天我美美地睡个好觉后,又不知道如何打发时光了。这人吧,很忙却挣钱少的时候,总会觉得累;可闲得没钱挣的时候,心却非常慌。

哎!还是写写博客吧,继续学习,才觉得充实自己。

2.说明

关于ASP.NET 5系列暂时不写了。微软不发布正式版,不能用于项目生产,搞得再明白也是白搭。

在myget.org发布的,已经beta5了,看样子进度很快,可何时到头,微软还是没有明确说明,让人期待却是没心底的等待,相当难受,索性就不关注也罢。

鉴于我还得多多学习,现在也遇到深一步提高水平的瓶颈。该总结什么好呢?不难觉得熟悉设计模式是提升自己编码水平的关键啦。

网上也大把介绍这些知识,市面上也很多这种书籍。可以说这是老生常谈的话题。

偶也买了一本王翔和孙逊写的《模式---工程化实现及扩展》C#版的书,园子里也有作者的博客。

以下总结的知识,大多吸取此书(书中除了代码排版乱点外,知识质量确实不错),自己概述不对之处,欢迎指点啦!

我在上一篇也写过依赖注入,但觉得不够详细。我觉得依赖注入,这项“技术”很重要,我把它再总结一遍,也是很好地再复习。

(注:大家提到设计模式,一般都是先讲设计原则,我在下一章再说吧。)

废话少说,进入正题:

3.场景

我们在讲道理,常常会举一些例子,才让人觉得好理解吧。在讲技术时,也是一样最好模拟一下场景,来介绍这项技术帮助我们解决了那些问题。

先说一下什么是客户程序?这里指的是UI表现层程序:

我们要实现客户程序获取年份,该如何实现呢?先写一个服务业务类:

using System;

namespace GiveCase.DI.Services
{
    /// <summary>
    /// 提供系统时间
    /// </summary>
    public class SystemTimeProvider
    {
        /// <summary>
        /// 获取当前日期
        /// </summary>
        public DateTime CurrentDate
        {
            get { return DateTime.Now; }
        }
    }
}

在客户程序代码:

using System;
using GiveCase.DI.Services;
namespace GiveCase.DI.Consoles
{
    class Program
    {
        static void Main(string[] args)
        {
            //实例化
            SystemTimeProvider stp = new SystemTimeProvider();

            //输出系统当前年份
            Console.WriteLine(stp.CurrentDate.Year);

            Console.ReadKey();
        }
    }
}

以上我们可以得到系统提供的年份,但是假如业务有变,我们获取其它来源提供的日期呢?

比如添加:OtherTimeProvider,客户程序要实例化使用它又得改变。我们该如何使用实例化呢?好吧,添加一个ITimeProvider接口:

using System;

namespace GiveCase.DI.Services
{
    public interface ITimeProvider
    {
        DateTime CurrentDate { get; }
    }
}

再修改一下SystemTimeProvider的实现(继承):

using System;

namespace GiveCase.DI.Services
{
    public class SystemTimeProvider : ITimeProvider
    {
        public DateTime CurrentDate
        {
            get { return DateTime.Now; }
        }
    }
}

你也可以添加OtherTimerProvider的实现:

using System;

namespace GiveCase.DI.Services
{
    /// <summary>
    /// 提供其它时间
    /// </summary>
    public class OtherTimeProvider : ITimeProvider
    {
        //TODO:更为精确的时间来源
        public DateTime CurrentDate
        {
            get { throw new NotImplementedException(); }
        }
    }
}

假如我们在客户程序使用系统时间获取年份,其代码改为:

using System;
using GiveCase.DI.Services;
namespace GiveCase.DI.Consoles
{
    class Program
    {
        static void Main(string[] args)
        {
            //接口引用具体实例
            ITimeProvider stp = new SystemTimeProvider();

            Console.WriteLine(stp.CurrentDate.Year);
            Console.ReadKey();
        }
    }
}

以上UML类图可以表示为:

注:业务不需要时OtherTimeProvider时,可以不画上。

客户程序依赖SystemTimeProvider的存在,显然没有达到解耦合目的。我们先用工厂模式,来解决这个问题,添加TimeProviderFactory:

using System;
using GiveCase.DI.Services;

namespace GiveCase.DI.Factories
{
    public static class TimeProviderFactory
    {
        public static ITimeProvider CreateTimeProvider(Genre genre)
        {
            switch (genre)
            {
                case Genre.A:
                    return new SystemTimeProvider();
                case Genre.B:
                    return new OtherTimeProvider();
                default:
                    throw new NotSupportedException();
            }
        }

        public enum Genre { A, B }
    }
}

这时客户程序调用工厂:

using System;
using GiveCase.DI.Services;
using GiveCase.DI.Factories;

namespace GiveCase.DI.Consoles
{
    class Program
    {
        static void Main(string[] args)
        {
            //接口引用工厂方法来生产对象实例
            ITimeProvider stp = TimeProviderFactory
                .CreateTimeProvider(GiveCase.DI.Factories.TimeProviderFactory.Genre.A);

            Console.WriteLine(stp.CurrentDate.Year);
            Console.ReadKey();
        }
    }
}

(注:这里只是使用简单工厂来演示,建议使用抽象工厂+反射)

此时类图:

我们利用工厂,已经不需要知道SystemTimProvider和OtherTimeProvider存在了。

这是不是一种完美解决方法呢?也许吧,毕竟工厂模式非常经典啦。

可是现在DI依赖注入框架大行其道,似乎用起来更方便,甚至可以控制对象的生命周期。

在使用DI框架前,我们自己先写一个保存实体类型的装配Assembler类,来体会各种注入方式:

using GiveCase.DI.Services;
using System;
using System.Collections.Generic;

namespace GiveCase.DI.Frameworks
{
    public class Assembler
    {
        //保存抽象类型和实体类型
        static Dictionary<Type, Type> d = new Dictionary<Type, Type>();

        static Assembler()
        {
            //注册抽象类型需要使用的实体类型
            d.Add(typeof(ITimeProvider), typeof(SystemTimeProvider));
        }

        public object Create(Type type)
        {
            if ((type == null) || !d.ContainsKey(type))
            {
                throw new NullReferenceException();
            }
            return Activator.CreateInstance(d[type]);
        }

        public T Create<T>()
        {
            return (T)Create(typeof(T));
        }
    }
}

Assembler来装配类型(温馨提示:反射类型保存到Dictionary是一种方案,也可以保存到缓存或Key-Values型数据库中),就可以不用工厂:

注:演示就不再提OtherTimeProvier。

我们如何使用Assembler来装配类型,并注入客户程序使用呢?这里我们分为下面几种方式一一叙述。

4.构造注入

构造注入是在构造函数执行过程中,通过Assembler把抽象类型作为参数传递给客户类型,这也是一次性注入的。

其实现代码:

using GiveCase.DI.Services;

namespace GiveCase.DI.Tests.Constructors
{
    /// <summary>
    /// 构造注入
    /// </summary>
    public class Client
    {
        ITimeProvider _tp;
        public Client(ITimeProvider tp)
        {
            _tp = tp;
        }
    }
}

单元测试:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using GiveCase.DI.Services;
using GiveCase.DI.Frameworks;

namespace GiveCase.DI.Tests.Constructors
{
    [TestClass]
    public class TestClient
    {
        [TestMethod]
        public void Test()
        {
            ITimeProvider tp = (new Assembler()).Create<ITimeProvider>();

            //判断抽象类型获取实例
            Assert.IsNotNull(tp);

            //在构造函数中注入
            Client c = new Client(tp);
        }
    }
}

5.设值注入

相对构造注入。设值注入给了客户类型后,还有修改的机会,它也适合生命周期较长的场景。

其实现代码:

using GiveCase.DI.Services;

namespace GiveCase.DI.Tests.Setters
{
    /// <summary>
    /// 设值注入
    /// </summary>
    public class Client
    {
        public ITimeProvider _tp { get; set; }
    }
}

单元测试:

using GiveCase.DI.Frameworks;
using GiveCase.DI.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace GiveCase.DI.Tests.Setters
{
    [TestClass]
    public class TestClient
    {
        [TestMethod]
        public void Test1()
        {
            ITimeProvider tp = (new Assembler()).Create<ITimeProvider>();

            //判断抽象类型获取实例
            Assert.IsNotNull(tp);

            Client c = new Client();
            c._tp = tp;
        }
    }
}

此测试方法也可以用Lamada简写成:

        [TestMethod]
        public void Test2()
        {
            var c = new Client()
            {
                _tp=(new Assembler()).Create<ITimeProvider>()
            };
        }

6.接口注入

接口注入是将抽象类型的入口以方法定义在一个接口中,如果客户类型需要获得这个方法,就以实现这个接口的方式完成注入。一般不建议使用这种方式。

定义要注入的ITimeProvider类型 接口:

using GiveCase.DI.Services;

namespace GiveCase.DI.Tests.Interfaces
{
    /// <summary>
    /// 定义要注入的类型
    /// </summary>
    public interface IObjectDI
    {
        ITimeProvider tp { get; set; }
    }
}

通过接口方式注入:

using GiveCase.DI.Services;
using System;

namespace GiveCase.DI.Tests.Interfaces
{
    public class Client : IObjectDI
    {
        public ITimeProvider tp { get; set; }
    }
}

单元测试:

using GiveCase.DI.Frameworks;
using GiveCase.DI.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace GiveCase.DI.Tests.Interfaces
{
    [TestClass]
    public class TestClient
    {
        [TestMethod]
        public void Test()
        {
            ITimeProvider _tp = (new Assembler()).Create<ITimeProvider>();

            //判断抽象类型获取实例
            Assert.IsNotNull(_tp);

            IObjectDI od = new Client();
            od.tp = _tp;
        }
    }
}

7.泛型版本的注入

这里举例泛型版的接口注入。

定义接口:

namespace GiveCase.DI.Tests.GenericInterfaces
{
    public interface IObjectDI<IType>
    {
        IType Provider { get; set; }
    }
}

客户程序实现:

using GiveCase.DI.Services;

namespace GiveCase.DI.Tests.GenericInterfaces
{
    public class Client : IObjectDI<ITimeProvider>
    {
        public ITimeProvider Provider { get; set; }
    }
}

单元测试:

using GiveCase.DI.Frameworks;
using GiveCase.DI.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace GiveCase.DI.Tests.GenericInterfaces
{
    [TestClass]
    public class TestClient
    {
        [TestMethod]
        public void Test()
        {
            var c = new Client()
            {
             Provider=(new Assembler()).Create<ITimeProvider>()
            };

            Assert.IsInstanceOfType(c.Provider, typeof(SystemTimeProvider));
        }
    }
}

注:关于接口注入,是比较暴力又啰嗦,就不要多用啦。上述别的方式也可以改成泛型版的(读者自己研究)。

8.特性注入

这项技术还是比较牛X的,我们改造Assembler,添加一个DecoratorAttribute:

using System;

namespace GiveCase.DI.Frameworks
{
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
    public class DecoratorAttribute : Attribute
    {
        /// <summary>
        /// 实现客户类型实际需要的抽象类型的实体类型实例,即待注入到客户类型的内容
        /// </summary>
        public readonly object Injector;
        readonly Type _type;

        public DecoratorAttribute(Type type)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            _type = type;
            Injector = (new Assembler()).Create(_type);
        }

        /// <summary>
        /// 客户类型需要的抽象对象类型
        /// </summary>
        public Type Type { get { return _type; } }
    }
}

再添加一个特性助手类:

using System;
using System.Linq;

namespace GiveCase.DI.Frameworks
{
    /// <summary>
    /// 用户帮助客户类型和客户程序获取其Attribute定义中需要的抽象类型实例
    /// </summary>
    public static class AttributeHelper
    {
        public static T Injector<T>(object target) where T : class
        {
            if (target == null) throw new ArgumentNullException("target");
            return (T)(((DecoratorAttribute[])target
                .GetType().GetCustomAttributes(typeof(DecoratorAttribute), false))
                .Where(x => x.Type == typeof(T))
                .FirstOrDefault()
                .Injector);
        }
    }
}

客户程序实现:

using GiveCase.DI.Frameworks;
using GiveCase.DI.Services;

namespace GiveCase.DI.Tests.Attributes
{
    [Decorator(typeof(ITimeProvider))]
    public class Client
    {
        public int GetYear()
        {
            // 与其他方式注入不同的是,这里使用的ITimeProvider来自自己的Attribute
            var provider = AttributeHelper.Injector<ITimeProvider>(this);
            return provider.CurrentDate.Year;
        }
    }
}

单元测试:

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace GiveCase.DI.Tests.Attributes
{
    [TestClass]
    public class TestClient
    {
        [TestMethod]
        public void Test()
        {
            Assert.IsTrue(new Client().GetYear() > 0);
        }
    }
}

嘎嘎!这种方式是不是爽呆了,有点象MEF框架了,下面我们介绍.NetFramework自带的MEF框架,它比我们自己写的特性注入强大得多。

9.MEF注入

导出部件:

客户程序代码:

using GiveCase.DI.Services;
using System;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;

namespace GiveCase.DI.Tests.MEF
{
    public class Client
    {
        //导入部件
        [Import]
        public ITimeProvider _tp { get; set; }

        #region 此段代码一般写在程序入口
        private static CompositionContainer container;
        public void Compose()
        {
            //var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
            //container = new CompositionContainer(catalog);
            ////将部件和宿主程序添加到组合容器
            //container.ComposeParts(this, new SystemTimeProvider());

            var catalog = new DirectoryCatalog(AppDomain.CurrentDomain.BaseDirectory, "GiveCase.DI.Services.dll");
            container = new CompositionContainer(catalog);
            //将部件和宿主程序添加到组合容器
            container.ComposeParts(this);
        }
        #endregion
    }
}

单元测试:

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace GiveCase.DI.Tests.MEF
{
    [TestClass]
    public class TestClient
    {
        [TestMethod]
        public void Test()
        {
            var c = new Client();
            c.Compose();

            Assert.IsTrue(c._tp.CurrentDate.Year > 0);
        }
    }
}

关于MEF更多知识,建议读者另行了解。另外别的DI框架,本人不是很懂,就不在这里举例了。

对于ASP.NET 5 新技术来说,模块化的DI框架已经更为简单。上一章也简单说过了。

10.小结

本章内容是我对上一章重新整理,代码基本是参考书得来的,如有侵权行为,我广告补偿了,也够意思啦。

本章项目目录,如下图:

需要源码的(其实也没必要了,我贴码时,命名空间都带了……),请加QQ群:290576772 到群空间下载!

时间: 2024-10-29 15:14:28

设计模式:依赖注入的相关文章

依赖注入[2]: 基于IoC的设计模式

正如我们在<控制反转>提到过的,很多人将IoC理解为一种"面向对象的设计模式",实际上IoC自身不仅与面向对象没有必然的联系,它也算不上是一种设计模式.一般来讲,设计模式提供了一种解决某种具体问题的方案,但是IoC既没有一个针对性的问题领域,其自身没有提供一种可实施的解决方案,所以我更加倾向于将IoC视为一种设计原则.实际上很多我们熟悉的设计模式背后采用了IoC原则,接下来我们就来介绍几种典型的"设计模式". 一.模板方法 提到IoC,很多人首先想到的是

依赖注入[4]: 创建一个简易版的DI框架[上篇]

本系列文章旨在剖析.NET Core的依赖注入框架的实现原理,到目前为止我们通过三篇文章(<控制反转>.<基于IoC的设计模式>和< 依赖注入模式>)从纯理论的角度对依赖注入进行了深入论述,为了让读者朋友能够更好地理解.NET Core的依赖注入框架的设计思想和实现原理,我们创建了一个简易版本的DI框架,也就是我们在前面文章中多次提及的Cat.我们会上下两篇来介绍这个被称为为Cat的DI框架,上篇介绍编程模型,下篇关注设计实现.[源代码从这里下载] 目录一.DI容器的层

依赖注入[5]: 创建一个简易版的DI框架[下篇]

为了让读者朋友们能够对.NET Core DI框架的实现原理具有一个深刻而认识,我们采用与之类似的设计构架了一个名为Cat的DI框架.在<依赖注入[4]: 创建一个简易版的DI框架[上篇]>中我们介绍了Cat的基本编程模式,接下来我们就来聊聊Cat的设计和实现. 目录一.服务注册:ServiceRegistry 二.DI容器:Cat 三.扩展方法 一.服务注册:ServiceRegistry 由于作为DI容器的Cat对象总是利用预先添加到服务注册来提供对应的服务实例,所以服务注册至关重要.如下

编码最佳实践——依赖注入原则

我们在这个系列的前四篇文章中分别介绍了SOLID原则中的前四个原则,今天来介绍最后一个原则--依赖注入原则.依赖注入(DI)是一个很简单的概念,实现起来也很简单.但是简单却掩盖不了它的重要性,如果没有依赖注入,前面的介绍的SOLID技术原则都不可能实际应用. 控制反转(IoC) 人们在谈论依赖注入的时候,经常也会谈到另一个概念--控制反转(IoC).按照大内老A的解释:"IoC主要体现了这样一种设计思想:通过将一组通用流程的控制权从应用转移到框架中以实现对流程的复用,并按照"好莱坞法则

依赖注入 ---- 系列文章

依赖注入[1]: 控制反转依赖注入[2]: 基于IoC的设计模式依赖注入[3]: 依赖注入模式依赖注入[4]: 创建一个简易版的DI框架[上篇]依赖注入[5]: 创建一个简易版的DI框架[下篇]依赖注入[6]: .NET Core DI框架[编程体验]依赖注入[7]: .NET Core DI框架[服务注册]依赖注入[8]: .NET Core DI框架[服务消费] 出处:https://www.cnblogs.com/artech/category/219608.html 原文地址:https

依赖注入 - 设计模式

随着 Asp.Net Mvc Core 发布,给我的感觉,Microsoft(微软) 一下子成了依赖注入的使用的疯狂者,因为在我的印象中,依赖注入(Ioc DI)是业务程序员写业务代码时常用的,但像 Microsoft(微软)把这个设计模式应用到了 Platform or Framework 上,这个实在罕见.这就导致在今后,你使用Microsoft(微软)Platform 或者 Framework 可能连 new 这个 keyword 都接触不到 , 当然了,这个是我的极端想法.但是大部分还是

《设计模式》总结系列02: 依赖注入

1.前言 在讲<设计模式>前,还有一个实现技巧说一下.它就是依赖注入. 为什么要介绍它? 面向抽象(接口)编程是抓住“依赖倒置原则”(后续文章会介绍)  的核心. 依赖倒置是站在客户程序角度来看的,客户程序依赖的是“相对稳定”的接口,而不是“相对多的”子类.也就是客户程序不要依赖子类. 设计原则还有一个“里氏替换原则”,它是站在模式对象角度来看的,模式对象将“相对多变”的子类视同它的接口(或父类).也就是父类出现的地方,子类可以代替. 说到“倒置”,可能晕乎.那么先说“正置”吧.依赖正置就是类

基于反射的通过set方法的依赖注入,可以看成一种设计模式,自己来用

非常好用,在properties文件中配置字符串和类名之间的对应,在程序里读取文件,找到类名,通过反射,达到调用set方法的目的,然后直接将自己的指向其他类的对象的引用赋值,指向实体对象. 比如userservice类,(当然spring用这个方法依赖注入好了,但是会这个原理,可以在某些时候方便自己用) 有一个userDao要注入,可以让userservice继承一个baseService类,在baseService类的构造方法中,定义一个反射方法,这样每次实例化userService的时候,它

大话设计模式--DI(依赖注入)

1.背景 想象一个场景:有个功能通过某个参数决定了路由到不同的方法上或者几个方法模块可以自由搭配,咋办?一般人会对每个方法写一个helper(比如SendMessageForEmail.SendMessageForSMS). 2.分析 这样的方法可以吗?当然可以实现,而且跑起来压根没问题,如果你想让自己代码更好一点,更有结构更容易维护,你需要做一些事,用到DI依赖注入模式 3.代码 一般方法: 1 private bool callSendNote(int tenant_id, string a