C#软件设计——小话设计模式原则之:依赖倒置原则DIP

前言:好久以前就想动笔总结下关于软件设计的一些原则,或者说是设计模式的一些原则,奈何被各类bootstrap组件所吸引,一直抽不开身。群里面有朋友问博主是否改行作前端了,呵呵,其实博主是想作“全战”,即各方便都有战斗力。关于设计模式,做为程序猿的咱们确定都不陌生。博主的理解,所谓设计模式就是前人总结下来的一些对于某些特定使用场景很是适用的优秀的设计思路,“前人栽树,后人乘凉”,做为后来者的咱们就有福了,当咱们遇到相似的应用场景的时候就能够直接使用了。关于设计模式的原则,博主将会在接下来的几篇里面根据本身的理解一一介绍,此篇就先来看看设计模式的设计原则之——依赖倒置原则。html

软件设计原则系列文章索引前端

1、原理介绍

一、官方定义

依赖倒置原则,英文缩写DIP,全称Dependence Inversion Principle。spring

原始定义:High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions。编程

官方翻译:高层模块不该该依赖低层模块,二者都应该依赖其抽象;抽象不该该依赖细节,细节应该依赖抽象。bootstrap

二、本身理解

2.一、原理解释

上面的定义不难理解,主要包含两次意思:设计模式

1)高层模块不该该直接依赖于底层模块的具体实现,而应该依赖于底层的抽象。换言之,模块间的依赖是经过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是经过接口或抽象类产生的。架构

2)接口和抽象类不该该依赖于实现类,而实现类依赖接口或抽象类。这一点其实不用多说,很好理解,“面向接口编程”思想正是这点的最好体现。app

2.二、被“倒置”的依赖

相比传统的软件设计架构,好比咱们常说的经典的三层架构,UI层依赖于BLL层,BLL层依赖于DAL层。因为每一层都是依赖于下层的实现,这样当某一层的结构发生变化时,它的上层就不得不也要发生改变,好比咱们DAL里面逻辑发生了变化,可能会致使BLL和UI层都随之发生变化,这种架构是很是荒谬的!好,这个时候若是咱们换一种设计思路,高层模块不直接依赖低层的实现,而是依赖于低层模块的抽象,具体表现为咱们增长一个IBLL层,里面定义业务逻辑的接口,UI层依赖于IBLL层,BLL层实现IBLL里面的接口,因此具体的业务逻辑则定义在BLL里面,这个时候若是咱们BLL里面的逻辑发生变化,只要接口的行为不变,上层UI里面就不用发生任何变化。框架

在经典的三层里面,高层模块直接依赖低层模块的实现,当咱们将高层模块依赖于底层模块的抽象时,就好像依赖“倒置”了。这就是依赖倒置的由来。经过依赖倒置,可使得架构更加稳定、更加灵活、更好应对需求变化。ide

2.三、依赖倒置的目的

上面说了,在三层架构里面增长一个接口层能实现依赖倒置,它的目的就是下降层与层之间的耦合,使得设计更加灵活。从这点上来讲,依赖倒置原则也是“松耦合”设计的很好体现。

2、场景示例

 文章最开始的时候说了,依赖倒置是设计模式的设计原则之一,那么在咱们那么多的设计模式中,哪些设计模式遵循了依赖倒置的原则呢?这个就多了,好比咱们常见的工厂方法模式。下面博主就结合一个使用场景来讲说依赖倒置原则如何可以使得设计更加灵活。

场景描述:还记得在C#基础系列——一场风花雪月的邂逅:接口和抽象类这篇里面介绍过设备的采集的例子,这篇继续以这个使用场景来讲明。设备有不少类型,每种设备都有登陆和采集两个方法,经过DeviceService这个服务去启动设备的采集,最开始咱们只有MML和TL2这两种类型的设备,那么来看看咱们的设计代码。

代码示例:

  //MML类型的设备
    public class DeviceMML
    {
        public void Login()
        {
            Console.WriteLine("MML设备登陆");
        }

        public bool Spider()
        {
            Console.WriteLine("MML设备采集");
            return true;
        }
    }

    //TL2类型设备
    public class DeviceTL2
    {
        public void Login()
        {
            Console.WriteLine("TL2设备登陆");
        }

        public bool Spider()
        {
            Console.WriteLine("TL2设备采集");
            return true;
        }
    }

    //设备采集的服务
    public class DeviceService
    {
        private DeviceMML MML = null;
        private DeviceTL2 TL2 = null;
        private string m_type = null;
        //构造函数里面经过类型来判断是哪一种类型的设备
        public DeviceService(string type)
        {
            m_type = type;
            if (type == "0")
            {
                MML = new DeviceMML();
            }
            else if (type == "1")
            {
                TL2 = new DeviceTL2();
            }
        }

        public void LoginDevice()
        {
            if (m_type == "0")
            {
                MML.Login();
            }
            else if (m_type == "1")
            {
                TL2.Login();
            }
        }

        public bool DeviceSpider()
        {
            if (m_type == "0")
            {
                return MML.Spider();
            }
            else if (m_type == "1")
            {
                return TL2.Spider();
            }
            else
            {
                return true;
            }
        }
    }

在Main函数里面调用

   class Program
    {

        static void Main(string[] args)
        {
            var oSpider = new DeviceService("1");
            oSpider.LoginDevice();
            var bRes = oSpider.DeviceSpider();
            
            Console.ReadKey();
        }

上述代码通过开发、调试、部署、上线。能够正常运行,貌似一切都OK。

日复一日、年复一年。后来公司又来两种新的设备TELNET和TL5类型设备。因而程序猿们又有得忙了,加班,赶进度!因而代码变成了这样:

   //MML类型的设备
    public class DeviceMML
    {
        public void Login()
        {
            Console.WriteLine("MML设备登陆");
        }

        public bool Spider()
        {
            Console.WriteLine("MML设备采集");
            return true;
        }
    }

    //TL2类型设备
    public class DeviceTL2
    {
        public void Login()
        {
            Console.WriteLine("TL2设备登陆");
        }

        public bool Spider()
        {
            Console.WriteLine("TL2设备采集");
            return true;
        }
    }

    //TELNET类型设备
    public class DeviceTELNET
    {
        public void Login()
        {
            Console.WriteLine("TELNET设备登陆");
        }

        public bool Spider()
        {
            Console.WriteLine("TELNET设备采集");
            return true;
        }
    }

    //TL5类型设备
    public class DeviceTL5
    {
        public void Login()
        {
            Console.WriteLine("TL5设备登陆");
        }

        public bool Spider()
        {
            Console.WriteLine("TL5设备采集");
            return true;
        }
    }


    //设备采集的服务
    public class DeviceService
    {
        private DeviceMML MML = null;
        private DeviceTL2 TL2 = null;
        private DeviceTELNET TELNET = null;
        private DeviceTL5 TL5 = null;
        private string m_type = null;
        //构造函数里面经过类型来判断是哪一种类型的设备
        public DeviceService(string type)
        {
            m_type = type;
            if (type == "0")
            {
                MML = new DeviceMML();
            }
            else if (type == "1")
            {
                TL2 = new DeviceTL2();
            }
            else if (type == "2")
            {
                TELNET = new DeviceTELNET();
            }
            else if (type == "3")
            {
                TL5 = new DeviceTL5();
            }
        }

        public void LoginDevice()
        {
            if (m_type == "0")
            {
                MML.Login();
            }
            else if (m_type == "1")
            {
                TL2.Login();
            }
            else if (m_type == "2")
            {
                TELNET.Login();
            }
            else if (m_type == "3")
            {
                TL5.Login();
            }
        }

        public bool DeviceSpider()
        {
            if (m_type == "0")
            {
                return MML.Spider();
            }
            else if (m_type == "1")
            {
                return TL2.Spider();
            }
            else if (m_type == "2")
            {
                return TELNET.Spider();
            }
            else if (m_type == "3")
            {
                return TL5.Spider();
            }
            else
            {
                return true;
            }
        }
    }

好比咱们想启动TL5类型设备的采集,这样调用能够实现:

        static void Main(string[] args)
        {
            var oSpider = new DeviceService("3");
            oSpider.LoginDevice();
            var bRes = oSpider.DeviceSpider();
         
            Console.ReadKey();
        }

花了九年二虎之力,总算是能够实现了。但是又过了段时间,又有新的设备类型呢?是否是又要加班,又要改。这样下去,感受这就是一个无底洞,再加上时间越久,项目所经历的开发人员越容易发生变化,这个时候再改,那维护的成本堪比开发一个新的项目。而且,随着设备类型的增多,代码里面充斥着大量的if...else,这样的烂代码简直让人没法直视。

基于这种状况,若是咱们当初设计这个系统的时候考虑了依赖倒置,那么效果可能大相径庭。咱们来看看依赖倒置如何解决以上问题的呢?

    //定义一个统一接口用于依赖
    public interface IDevice
    {
        void Login();
        bool Spider();
    }

    //MML类型的设备
    public class DeviceMML : IDevice
    {
        public void Login()
        {
            Console.WriteLine("MML设备登陆");
        }

        public bool Spider()
        {
            Console.WriteLine("MML设备采集");
            return true;
        }
    }

    //TL2类型设备
    public class DeviceTL2 : IDevice
    {
        public void Login()
        {
            Console.WriteLine("TL2设备登陆");
        }

        public bool Spider()
        {
            Console.WriteLine("TL2设备采集");
            return true;
        }
    }

    //TELNET类型设备
    public class DeviceTELNET : IDevice
    {
        public void Login()
        {
            Console.WriteLine("TELNET设备登陆");
        }

        public bool Spider()
        {
            Console.WriteLine("TELNET设备采集");
            return true;
        }
    }

    //TL5类型设备
    public class DeviceTL5 : IDevice
    {
        public void Login()
        {
            Console.WriteLine("TL5设备登陆");
        }

        public bool Spider()
        {
            Console.WriteLine("TL5设备采集");
            return true;
        }
    }


    //设备采集的服务
    public class DeviceService
    {
        private IDevice m_device;
        public DeviceService(IDevice oDevice)
        {
            m_device = oDevice;
        }

        public void LoginDevice()
        {
            m_device.Login();
        }

        public bool DeviceSpider()
        {
            return m_device.Spider();
        }
    }

调用

     static void Main(string[] args)
        {
            var oSpider = new DeviceService(new DeviceTL5());
            oSpider.Login();
            var bRes = oSpider.Spider();

            Console.ReadKey();
        }

代码说明:上述解决方案中,咱们定义了一个IDevice接口,用于上层服务的依赖,也就是说,上层服务(这里指DeviceService)仅仅依赖IDevice接口,对于具体的实现类咱们是无论的,只要接口的行为不发生变化,增长新的设备类型后,上层服务不用作任何的修改。这样设计下降了层与层之间的耦合,能很好地适应需求的变化,大大提升了代码的可维护性。呵呵,看着是否是有点眼熟?是否是有点像某个设计模式?其实设计模式的设计原理正是基于此。

3、使用Unity实现依赖倒置

上面说了那么多,都是在讲依赖倒置的好处,那么在咱们的项目中究竟如何具体实现和使用呢?

在介绍依赖倒置具体如何使用以前,咱们须要引入IOC容器相关的概念,咱们先来看看它们之间的关系。

依赖倒置原则(DIP):一种软件架构设计的原则(抽象概念)。

控制反转(IoC):一种反转流、依赖和接口的方式(DIP的具体实现方式)。这是一个有点不太好理解和解释的概念,通俗地说,就是应用程序自己不负责依赖对象的建立和维护,而是将它交给一个外部容器(好比Unity)来负责,这样控制权就由应用程序转移到了外部IoC 容器,即控制权实现了所谓的反转。例如在类型A中须要使用类型B的实例,而B 实例的建立并不禁A 来负责,而是经过外部容器来建立。

依赖注入(DI):IoC的一种实现方式,用来反转依赖(IoC的具体实现方式)。园子里面不少博文里面说IOC也叫DI,其实根据博主的理解,DI应该是IOC的具体实现方式,好比咱们如何实现控制反转,答案就是经过依赖注入去实现。

IoC容器:依赖注入的框架,用来映射依赖,管理对象建立和生存周期(DI框架),自动建立、维护依赖对象。

这些名词是否是有点熟呢?博主以前介绍过MEF,以前使用MEF作过依赖注入,详见C#进阶系列——MEF实现设计上的“松耦合”(一)。其实严格来说,MEF不能算一种正式的IOC容器,由于它的主要做用仍是用于应用程序扩展,避免生成脆弱的硬依赖项,而不是依赖注入。根据博主的了解以及使用经历,经常使用的IOC容器有:

固然,还有其余的IOC容器这里就不一一列举。Spring.net是从Java的Spring框架移植过来的,功能之强大咱们就很少说了,但是自从它宣布再也不更新,博主在使用它的时候就很是慎重了。下面博主仍是就Unity这种IOC容器来看看依赖倒置的具体实现。

一、Unity引入

Unity如何引入?咱们神奇的Nuget又派上用场了。最新的Unity版本已经到了4.0.1。

安装成功后主要引入了三个dll。

二、Unity经常使用API

UnityContainer.RegisterType<ITFrom,TTO>();

UnityContainer.RegisterType< ITFrom, TTO >();

UnityContainer.RegisterType< ITFrom, TTO >("keyName");

IEnumerable<T> databases = UnityContainer.ResolveAll<T>();

IT instance = UnityContainer.Resolve<IT>();

T instance = UnityContainer.Resolve<T>("keyName");

UnitContainer.RegisterInstance<T>("keyName",new T());

UnityContainer.BuildUp(existingInstance);

IUnityContainer childContainer1 = parentContainer.CreateChildContainer();

三、代码注入方式示例

3.一、默认注册方式

仍然以上面的场景为例说明,咱们注入DeviceMML这个实现类。

    class Program
    {
        private static IUnityContainer container = null;
        static void Main(string[] args)
        {
            RegisterContainer();
            var oSpider = container.Resolve<IDevice>();
            oSpider.Login();
            var bRes = oSpider.Spider();

            Console.ReadKey();
        }

        /// <summary>
        /// 代码注入
        /// </summary>
        public static void RegisterContainer()
        {
            container = new UnityContainer();
            container.RegisterType<IDevice, DeviceMML>();  //默认注册方式,若是后面再次默认注册会覆盖前面的
        }
    }

运行结果

3.二、带命名方式的注册

上面默认注入的方式中,咱们只能注入一种具体的实例,若是咱们须要同时注入多个类型的实例呢?看看咱们的 RegisterType() 方法有多个重载。

   class Program
    {
        private static IUnityContainer container = null;
        static void Main(string[] args)
        {
            RegisterContainer();
            var oSpider = container.Resolve<IDevice>("TL5");
            oSpider.Login();
            var bRes = oSpider.Spider();

            Console.ReadKey();
        }

        /// <summary>
        /// 代码注入
        /// </summary>
        public static void RegisterContainer()
        {
            container = new UnityContainer();
           container.RegisterType<IDevice, DeviceMML>("MML");  //默认注册(无命名),若是后面还有默认注册会覆盖前面的
            container.RegisterType<IDevice, DeviceTELNET>("Telnet");  //命名注册
            container.RegisterType<IDevice, DeviceTL2>("TL2");  //命名注册
            container.RegisterType<IDevice, DeviceTL5>("TL5");  //命名注册
        }
    }

运行结果

四、配置文件注入方式示例

在App.config或者Web.config里面加入以下配置:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <configSections>
    <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,Microsoft.Practices.Unity.Configuration"/>
  </configSections>
  <unity>
    <!--容器-->
    <containers>
      <container name="Spider">
        <!--映射关系-->
        <register type="ESTM.Spider.IDevice,ESTM.Spider"  mapTo="ESTM.Spider.DeviceMML,ESTM.Spider" name="MML"></register>
        <register type="ESTM.Spider.IDevice,ESTM.Spider"  mapTo="ESTM.Spider.DeviceTELNET,ESTM.Spider" name="TELNET"></register>
        <register type="ESTM.Spider.IDevice,ESTM.Spider"  mapTo="ESTM.Spider.DeviceTL2,ESTM.Spider" name="TL2"></register>
        <register type="ESTM.Spider.IDevice,ESTM.Spider"  mapTo="ESTM.Spider.DeviceTL5,ESTM.Spider" name="TL5"></register>
      </container>
    </containers>
  </unity>
</configuration>

在代码里面注册配置文件:

namespace ESTM.Spider
{
    class Program
    {
        private static IUnityContainer container = null;
        static void Main(string[] args)
        {
            ContainerConfiguration();
            var oSpider = container.Resolve<IDevice>("TL5");
            oSpider.Login();
            var bRes = oSpider.Spider();

            Console.ReadKey();
        }

        /// <summary>
        /// 配置文件注入
        /// </summary>
        public static void ContainerConfiguration()
        {
            container = new UnityContainer();
            UnityConfigurationSection configuration = (UnityConfigurationSection)ConfigurationManager.GetSection(UnityConfigurationSection.SectionName);
            configuration.Configure(container, "Spider");
        }

    }
}

运行结果:

代码说明

(1)

<register type="ESTM.Spider.IDevice,ESTM.Spider"  mapTo="ESTM.Spider.DeviceMML,ESTM.Spider" name="MML"></register>

节点里面,type对象抽象,mapTo对象具体实例对象,name对象实例的别名。

(2)在app.config里面能够配置多个 <container name="Spider"> 节点,不一样的name配置不一样的依赖对象。

(3)配置文件注入的灵活之处在于解耦。为何这么说呢?试想,若是咱们的IDevice接口对应着一个接口层,而DeviceMML、DeviceTELNET、DeviceTL二、DeviceTL5等实现类在另一个实现层里面,咱们的UI层(这里对应控制台程序这一层)只须要添加IDevice接口层的引用,没必要添加实现层的引用,经过配置文件注入,在运行的时候动态将实现类注入到UI层里面来。这样UI层就对实现层实现了解耦,实现层里面的具体逻辑变化时,UI层里面没必要作任何更改。

4、总结

 到此,依赖倒置原则的讲解基本结束了。根据博主的理解,设计模式的这些原则是设计模式的理论指导,而设计模式则是这些理论的具体运用。说一千道一万,要想搞懂设计模式,必须先了解设计模式遵循的原则,不管是哪一种设计模式都会遵循一种或者多种原则。固然文章可能有理解不当的地方,欢迎大牛们指出。博主将会在接下来的几章继续总结下其余设计原则,若是园友们以为本文对你有帮助,请帮忙推荐,博主将继续努力~~