首頁 > 軟體

C#程式設計之依賴倒置原則DIP

2022-03-20 19:00:37

一、前言

我們先來看看傳統的三層架構,如下圖所示:

從上圖中我們可以看到:在傳統的三層架構中,層與層之間是相互依賴的,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的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com