[小菜一碟] 談談物件導向設計原則中 DIP(依賴反轉原則)中的 Inversion(反轉)

上一篇文章提到依賴,依賴也無所不在,而 DIP 要我們將依賴做「反轉」,是要反轉什麼?反過來依賴嗎? 當然不是,而是解除直接依賴的關係轉而去依賴於抽象介面,使得位於較低層次的類別依賴關係的方向反了過來,大致上看起來就會像這樣。

我們不會沒事去依賴其他類別,一定是該類別有提供我們需要的方法才會去把它建立起來使用,而就在建立使用(Create and Use)的過程產生了依賴,DIP 要我們不要直接依賴,中間應該墊一個抽象介面。

利用上一篇文章提到的範例:

internal class Program
{
    private static readonly string ConnectionString = "...";

    private static void Main(string[] args)
    {
        var conn = new SqlConnection(ConnectionString);

        conn.Open();
    }
}

我們先從「使用」這個部分下手解除直接依賴關係,轉依賴抽象介面,將 conn 的型別改為 IDbConnection 就可以看見 Open() 方法就不再是從 SqlConnection 具體類別參考來的。

再來我們從「建立」這個部分來解除直接依賴關係,在這邊我們要有一個觀念「建立是必要的,沒有建立,物件就不能使用。」,不管我們是使用 DI Framework 或是套工廠模式...等方式建立物件,都只是將建立物件的職責移到其他地方,還是免不了會在建立物件的地方產生直接依賴,這裡我選用簡單工廠方法來處理建立物件的部分。

internal class Program
{
    private static readonly string ConnectionString = "...";

    private static void Main(string[] args)
    {
        IDbConnection conn = DbConnectionFactory.Create("SQL Server", ConnectionString);

        conn.Open();
    }
}

internal static class DbConnectionFactory
{
    public static IDbConnection Create(string kindOfDb, string connectionString)
    {
        switch (kindOfDb.ToLowerInvariant())
        {
            case "sql server": return new SqlConnection(connectionString);
            case "oracle": return new OracleConnection(connectionString);
            default: throw new ArgumentOutOfRangeException(nameof(kindOfDb));
        }
    }
}

這樣子 Program 類別與 SqlConnection 類別就解除了直接依賴關係,轉而去依賴 IDbConnection,而我們為什麼要做這樣的事?-「為了提高高層次類別重複運用的容易程度

SqlConnection 類別是與 SQL Server 建立連線用的,當我的資料庫換成了 Oracle,與之連線用的是 OracleConnection,如果 SqlConnection 與 OracleConnection 沒有相同的抽象介面規範,都自己搞自己的,開啟連線就有可能一個叫 Open()、一個叫 Connect(),如果真是這樣就遑論其他 API 了,程式要從 SQL Server 搬遷到 Oracle 的話就很辛苦了,兩邊 API 不一樣的地方都要一行一行地改。

如果 SqlConnection 與 OracleConnection 都有實作同一個介面(事實上有,就是 IDbConnection。),只要調整一下丟給 DbConnectionFactory.Create() 方法的參數及連線字串,這樣子切換不同資料庫就顯得容易多了,其他的 API 如果也都有實作相同介面(事實上也有),那麼原先已經寫好的商業邏輯,重複運用的容易程度就大大地提升了。

如果有用 Dapper 的朋友可以隨便挑一個方法「移至定義」看一下,它的擴充方法是擴充在 IDbConnection 上,這也就是為什麼 Dapper 可以支援多種的資料庫。

實務上或許很少遇到這種轉換資料庫的情境,但是使用的框架/Library 面臨 phase out 需要升級或是換另外一套也是類似的,在這樣的情況之下,要是商業邏輯的程式碼直接依賴框架/Library 的話,就痛苦了,因此如果我們把商業邏輯的部分看成高層模組,把框架/Library 看成低層模組,中間墊一層自己設計的抽象介面的話,要升級或抽換框架/Library 相對就容易多了。

事實上這也是我目前開發程式的起手式,甚至是 .NET Framework 所提供的 API 我也先視為是低層模組,因為框架/Library 像四季一樣會更迭,商業邏輯則是像文化一樣會延續,好了,反轉就談到這裡,希望這兩篇文章對 DIP 的了解有幫助。

相關資源

C# 指南
ASP.NET 教學
ASP.NET MVC 指引
Azure SQL Database 教學
SQL Server 教學
Xamarin.Forms 教學