C#编程之依赖倒置原则DIP

一、前言

我们先来看看传统的三层架构,如下图所示:

从上图中我们可以看到:在传统的三层架构中,层与层之间是相互依赖的,UI层依赖于BLL层,BLL层依赖于DAL层。分层的目的是为了实现“高内聚、低耦合”。传统的三层架构只有高内聚没有低耦合,层与层之间是一种强依赖的关系,这也是传统三层架构的一种缺点。这种自上而下的依赖关系会导致级联修改,如果低层发生变化,可能上面所有的层都需要去修改,而且这种传统的三层架构也很难实现团队的协同开发,因为上层功能取决于下层功能的实现,下面功能如果没有开发完成,则上层功能也无法进行。

传统的三层架构没有遵循依赖倒置原则(DIP)来设计,所以就会出现上面的问题。

二、依赖倒置

依赖倒置(DIP):Dependence Inversion Principle的缩写,主要有两层含义:

  • 高层次的模块不应该依赖低层次的模块,两者都应该依赖其抽象。
  • 抽象不应该依赖于具体,具体应该依赖于抽象。

我们先来解释第一句话:高层模块不应该直接依赖低层模块的具体实现,而是应该依赖于低层模块的抽象,也就是说,模块之间的依赖是通过抽象发生的,实现类之间不应该发生直接的依赖关系,他们的依赖关系应该通过接口或者抽象类产生。

在来解释第二句话:接口或者抽象类不应该依赖于实现类。举个例子,假如我们要写BLL层的代码,直接就去实现了功能,等到开发完成以后发现没有使用依赖倒置原则,这时候在根据实现类去写接口,这种是不对的,应该首先设计抽象,然后在根据抽象去实现,应该要面向接口编程。

我们在上面说过,在传统的三层架构里面没有使用依赖倒置原则,那么把依赖倒置原则应用到传统的三层架构里面会如何呢?我们知道,在传统的三层架构里面,UI层直接依赖于BLL层,BLL层直接依赖于DAL层,由于每一层都是依赖下一层的实现,所以说当下层发生变化的时候,它的上一层也要发生变化,这时候可以根据依赖倒置原则来重新设计三层架构。

UI、BLL、DAL三层之间应该没有直接的依赖关系,都应该依赖于接口。首先应该先确定出接口,DAL层抽象出IDAL接口,BLL层抽象出IBLL接口,这样UI层依赖于IBLL接口,BLL实现IBLL接口。BLL层依赖于IDAL接口,DAL实现IDAL接口。如下图所示:

我们上面讲了依赖倒置原则,那么依赖倒置原则的目的是什么呢?
有了依赖倒置原则,可以使我们的架构更加的稳定、灵活,也能更好地应对需求的变化。相对于细节的多变性,抽象的东西是稳定的。所以以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定的多。

在传统的三层架构里面,仅仅增加一个接口层,我们就实现了依赖倒置,目的就是降低层与层之间的耦合。有了这样的接口层,三层架构才真正实现了“高内聚、低耦合”的思想。

依赖倒置原则是架构层面上的,那么如何在代码层面上实现呢?下面看控制反转。

三、控制反转

控制反转(IOC):Inversion of Control的缩写,一种反转流、依赖和接口的方式,它把传统上由程序代码直接操控的对象的控制器(创建、维护)交给第三方,通过第三方(IOC容器)来实现对象组件的装配和管理。

IOC容器,也可以叫依赖注入框架,是由一种依赖注入框架提供的,主要用来映射依赖,管理对象的创建和生存周期。IOC容器本质上就是一个对象,通常会把程序里面所有的类都注册进去,使用这个类的时候,直接从容器里面去解析。

四、依赖注入

依赖注入(DI):Dependency Injection的缩写。依赖注入是控制反转的一种实现方式,依赖注入的目的就是为了实现控制反转。

依赖注入是一种工具或手段,目的是帮助我们开发出松耦合、可维护的程序。

依赖注入常用的方式有以下几种:

  • 构造函数注入。
  • 属性注入。
  • 方法注入。

其中构造函数注入是使用最多的,其次是属性注入。

看下面的一个例子:父亲给孩子讲故事,只要给这个父亲一本书,他就可以照着这本书给孩子讲故事。我们下面先用最传统的方式实现一下,这里不使用任何的设计原则和设计模式。

首先定义一个Book类:

namespace DipDemo1
{
    public class Book
    {
        public string GetContent()
        {
            return "从前有座山,山上有座庙.....";
        }
    }
}

然后在定义一个Father类:

using System;

namespace DipDemo1
{
    public class Father
    {
        public void Read()
        {
            Book book = new Book();
            Console.WriteLine("爸爸开始给孩子讲故事了");
            Console.WriteLine(book.GetContent());
        }
    }
}

然后在Main方法里面调用:

using System;

namespace DipDemo1
{
    class Program
    {
        static void Main(string[] args)
        {
            Father father = new Father();
            father.Read();
            Console.ReadKey();
        }
    }
}

我们来看看关系图:

我们看到:Father是直接依赖于Book类。

这时需求发生了变化,不给爸爸书了,给爸爸报纸,让爸爸照着报纸给孩子读报纸,这时该怎么做呢?按照传统的方式,我们这时候需要在定义一个报纸类:

namespace DipDemo1
{
    public class NewsPaper
    {
        public string GetContent()
        {
            return "新闻";
        }
    }
}

这时依赖关系变了,因为爸爸要依赖于报纸了,这就导致还要修改Father类:

using System;

namespace DipDemo1
{
    public class Father
    {
        public void Read()
        {
            // 读书
            // Book book = new Book();
            //Console.WriteLine("爸爸开始给孩子讲故事了");
            //Console.WriteLine(book.GetContent());

            // 报纸
            NewsPaper paper = new NewsPaper();
            Console.WriteLine("爸爸开始给孩子讲新闻");
            Console.WriteLine(paper.GetContent());
        }
    }
}

假设后面需求又变了,又不给报纸了,换成杂志、平板电脑等。需求在不断的变化,不管怎么变化,对于爸爸来说,他一直在读读物,但是具体读什么读物是会发生变化,这就是细节,也就是说细节会发生变化。但是抽象是不会变的。如果这时候还是使用传统的OOP思想来解决问题,那么会导致程序不断的在修改。下面使用工厂模式来优化:

首先创建一个接口:

namespace DipDemo2
{
    public interface IReader
    {
        string GetContent();
    }
}

然后让Book类和NewsPaper类都继承自IReader接口,Book类

namespace DipDemo2
{
    public class Book : IReader
    {
        public string GetContent()
        {
            return "从前有座山,山上有座庙.....";
        }
    }
}

NewsPaper类:

namespace DipDemo2
{
    public class NewsPaper : IReader
    {
        public string GetContent()
        {
            return "王聪聪被限制高消费......";
        }
    }
}

然后创建一个工厂类:

namespace DipDemo2
{
    public static class ReaderFactory
    {
        public static IReader GetReader(string readerType)
        {
            if (string.IsNullOrEmpty(readerType))
            {
                return null;
            }

            switch (readerType)
            {
                case "NewsPaper":
                    return new NewsPaper();
                case "Book":
                    return new Book();
                default:
                    return null;
            }
        }
    }
}

里面方法的返回值是一个接口类型。最后在Father类里面调用工厂类:

using System;

namespace DipDemo2
{
    public class Father
    {
        private IReader Reader { get; set; }

        public Father(string readerName)
        {
            // 这里依赖于抽象
            Reader = ReaderFactory.GetReader(readerName);
        }

        public void Read()
        {
            Console.WriteLine("爸爸开始给孩子讲故事了");
            Console.WriteLine(Reader.GetContent());
        }
    }
}

最后在Main方法里面调用:

using System;

namespace DipDemo2
{
    class Program
    {
        static void Main(string[] args)
        {
            Father father = new Father("Book");
            father.Read();

            Console.ReadKey();
        }
    }
}

我们这时候可以在看看依赖关系图:

这时Father已经和Book、Paper没有任何依赖了,Father依赖于IReader接口,还依赖于工厂类,而工厂类又依赖于Book和Paper类。这里实际上已经实现了控制反转。Father(高层)不依赖于低层(Book、Paper)而是依赖于抽象(IReader),而且具体的实现也不是由高层来创建,而是由第三方来创建(这里是工厂类)。但是这里只是使用工厂模式来模拟控制反转,而没有实现依赖的注入,依赖还是需要向工厂去请求。

下面继续优化代码,这里只需要修改Father类:

using System;

namespace DipDemo3
{
    public class Father
    {
        public IReader Reader { get; set; }

        /// <summary>
        /// 构造函数的参数是IReader接口类型
        /// </summary>
        /// <param name="reader"></param>
        public Father(IReader reader)
        {
            Reader = reader;
        }

        public void Read()
        {
            Console.WriteLine("爸爸开始给孩子讲故事了");
            Console.WriteLine(Reader.GetContent());
        }
    }
}

在Main方法里面调用:

using System;
namespace DipDemo3
{
    class Program
    {
        static void Main(string[] args)
        {
            var f = new Father(new Book());
            f.Read();

            Console.ReadKey();
        }
    }
}

如果以后换成了Paper,需要修改代码:

using System;
namespace DipDemo3
{
    class Program
    {
        static void Main(string[] args)
        {
            // Book
            //var f = new Father(new Book());
            //f.Read();

            // Paprer
            var f = new Father(new Paper());
            f.Read();

            Console.ReadKey();
        }
    }
}

由于这里没有了工厂,我们还是需要在代码里面实例化具体的实现类。如果有一个IOC容器,我们就不需要自己new一个实例了,而是由容器帮我们创建实例,创建完成以后在把依赖对象注入进去。

我们在来看一下依赖关系图:

下面我们使用Unity容器来继续优化上面的代码,首先需要在项目里面安装Unity,直接在NuGet里面搜索即可:

这里只需要修改Main方法调用即可:

using System;
using Unity;

namespace UnityDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            // 创建容器
            var container = new UnityContainer();

            // 扫描程序集、配置文件
            // 在容器里面注册接口和实现类,创建依赖关系
            container.RegisterType<IReader, Book>();

            // 在容器里面注册Father
            container.RegisterType<Father>();

            // 从容器里拿出要使用的类,容器会自行创建father对
            // 还会从容器里去拿到他所依赖的对象,并且注入进来
            //
            var father = container.Resolve<Father>();

            // 调用方法
            father.Read();
            Console.ReadKey();
        }
    }
}

到此这篇关于C#编程之依赖倒置原则DIP的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • C#面向对象设计原则之里氏替换原则

    里氏替换原则(LSP) 定义:在任何父类出现的地方都可以用它的子类类替换,且不影响功能.解释说明:其实LSP是对开闭原则的一个扩展,在OO思想中,我们知道对象是由一系列的状态和行为组成的,里氏替换原则说的就是在一个继承体系中,对象应该具有共同的外在特性,使用LSP时,如果想让我们的程序达到一个父类出现的地方都可以用它的子类来替换且不影响功能,那么这个父类也应该尽量声明出子类所需要的一些公共的方法,父类被子类替换之后,会比较顺利,那么为什么说它是对开闭原则的一个扩展呢?因为我们在开闭原则中说尽量使

  • C#面向对象设计原则之接口隔离原则

    接口隔离原则(ISP) 定义:使用多个专门的接口比使用单一的总接口要好.即不要把鸡蛋都放到一个篮子里.好处:比较灵活.方便,不想实现的或不用实现的可以不实现.解释说明:大部分人都喜欢用一个接口把需要用到的方法全部声明出来,但是ISP建议我们使用多个专门的接口比使用单一的总接口要好,也就是一个接口里的方法多的话,实现起来不是很方便. 示例1: using System; using System.Collections.Generic; using System.Linq; using Syste

  • 浅谈C#六大设计原则

    笔者作为一个菜鸟,会尝试以简单的代码和容易理解的语句去解释这几种原则的特性和应用场景. 这六种原则分别为单一职责原则.接口隔离原则.里氏替换原则.迪米特法则.依赖倒置原则.开闭原则. 单一职责原则 单一职责原则(SRP:Single responsibility principle),规定一个类中应该只有一个原因引起类的变化. 单一职责原则的核心就是解耦和增强内聚性. 问题: // 假设此类是数据库上下文 public class DatabaseContext { } public class

  • C#实现六大设计原则之迪米特法则

    定义: 一个对象应该对其他对象保持最少的了解. 问题由来: 类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大. 解决方案: 尽量降低类与类之间的耦合. PS: 自从我们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚. 无论是面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率. 低耦合的优点不言而喻,但是怎么样编程才能做到低耦合呢?那正是迪米特法则要去完成的. 迪米特法则又叫最少知道原则,最早是在1987年由美国Northe

  • C#面向对象设计原则之开闭原则

    开闭原则(OCP) 定义:对扩展开发,对修改关闭.好处: 适应性和灵活性. 稳定性和延续性. 可复用性与可维护性. 解释说明:开闭原则指的是两方面:对功能扩展开发,对修改进行关闭:有时当用户要求或需求发生变化时,我们不得不打开原来的代码进行修改,进行功能的扩展或增加,这种设计如果应用到我们以后的项目开发中会导致严重的问题,这样容易导致意外的错误.好的程序,应该保证在我们进行程序扩展时,不会更改以前的代码.如何才能保证这样的效果呢?我们在定义一个类的功能时:最好先定义他的抽象类或接口,这样在功能扩

  • C#实现六大设计原则之依赖倒置原则

    依赖倒置原则(DIP)定义: 高层模块不应该依赖低层模块,二者都应该依赖其抽象:抽象不应该依赖细节:细节应该依赖抽象. 问题由来: 类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成.这种场景下,类A一般是高层模块, 负责复杂的业务逻辑:类B和类C是低层模块,负责基本的原子操作:假如修改类A,会给程序带来不必要的风险. 解决方案: 将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率. ps: 依赖倒置原则

  • C#实现六大设计原则之接口隔离原则

    接口隔离原则(ISP)定义: 客户端不应该依赖它不需要的接口:一个类对另一个类的依赖应该建立在最小的接口上. 问题由来: 类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法. 解决方案: 将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系.也就是采用接口隔离原则. 举例来说明接口隔离原则: 类A依赖接口I中的方法1.方法2.方法3,类B是对类A依赖的实现. 类C依赖接口I中的方法1.方法4.

  • C#实现六大设计原则之单一职责原则

    单一职责(SRP)定义: 不要存在多于一个导致类变更的原因,通俗的说,即一个类只负责一项职责. 问题由来: 类T负责两个不同的职责:职责P1,职责P2.当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障. 解决方案: 遵循单一职责原则.分别建立两个类T1.T2,使T1完成职责P1功能,T2完成职责P2功能.这样,当修改类T1时,不会使职责P2发生故障风险:同理,当修改T2时,也不会使职责P1发生故障风险. ps: 说到单一职责原则,很多人都会不屑一顾.因为

  • C#面向对象设计原则之组合/聚合复用原则

    组合/聚合复用原则(LSP) 定义:优先使用组合,使系统更灵活,其次才考虑继承,达到复用的目的.重用的方式:继承.组合.聚合解释说明: 继承:在我们想复用代码时,我们一般会优先想到继承,但是具有继承关系的两个类是耦合度最高的两个类.(父类改了子类可能会受影响,子类改了父类也可能会受影响)如果父类的功能比较稳定,建议使用继承来实现代码复用,因为继承是静态定义的,在运行时无法动态调用. 组合:是整体与部分的关系,整体离不开部分,部分离开了整体没有意义,如飞机翅膀与飞机的关系. 聚合:也是整体与部分的

  • C#面向对象设计原则之单一职责原则

    单一职责原则(SRP) 定义:系统中的每一个类都应该只有一个职责. 好处:高内聚.低耦合. 解释说明: 单一职责也就是说我们应该让一个类或一个对象只做一件事情,每个类所要关注的就是自己要完成的职责是什么,能够引起这个类变化的原因也应该只有一个,这也是后面提到的所有的设计模式都会遵守的一个原则. 高内聚:先按照面向对象的封装特性来理解,封装也就是我们说的,应该把一个类或对象它所有相关的属性.方法.行为放到一起,放到一个类中,这样就实现了一个封装的特性.那么内聚,就是一个类里面应该包含它所有的属性和

  • C#实现六大设计原则之里氏替换原则

    定义: 1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子型. 2:所有引用基类的地方必须能透明地使用其子类的对象. 问题由来: 有一功能P1,由类A完成.现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成. 新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障. 解决

随机推荐