【面向對象設計原則】之依賴倒置原則(DIP)


依賴倒轉原則(Dependency Inversion  Principle, DIP):抽象不應該依賴於細節,細節應當依賴於抽象。換言之,要針對抽象(接口)編程,而不是針對實現細節編程。

開閉原則(OCP)是面向對象設計原則的基礎也是整個設計的一個終極目標,而依賴倒置原則(DIP )則是實現OCP原則的一個基礎,換句話說開閉原則(OCP)是你蓋一棟大樓的設計藍圖,那么依賴倒置原則就是蓋這棟大樓的一個鋼構框架,沒有鋼構架構是很難順利蓋起一棟大樓的,同樣的在面向對象軟件設計的過程中不遵守依賴倒置原則是很難開發出符合開閉原則的軟件的。更不用說開發出易於維護,易於升級的軟件。 因此開閉原則是非常重要的一個原則,它有很強的實操性,並且能夠直接指導我們寫代碼代碼。

通常要符合這個原則的第一步就是針對抽象編程,類之間的依賴關系盡量去使用高層抽象不要使用底層的實現細節,從軟件工程來說高層抽象是較穩定的,也就是說抽象具有一定的穩定性,而實現細節較不穩定,也就是說實現細節具有易變性,而我們期望軟件具有更好的穩定性,顯而易見我們在開發的時候自然而然的要走穩定路線(依賴抽象編程)。這個原則也是對軟件工程中要求“高聚低偶”實踐一個保障和指導。

我們來看一個例子假設我們在開發一個軟件產品需要一個日志系統,要將系統產生的一些重要事情記錄在記事本上。通常我們的實現如下:

    public class Logger
    {
        public void Info(string infoText)
        {
            Console.WriteLine($"[{DateTime.Now}][Info]:{infoText}");
        }

        public void Debug(string debugText)
        {
            Console.WriteLine($"[{DateTime.Now}][Debug]:{debugText}");
        }

        public void Warn(string warmText)
        {
            Console.WriteLine($"[{DateTime.Now}][Warm]:{warmText}");
        }

        public void Error(string errorText,Exception exception)
        {
            Console.WriteLine($"[{DateTime.Now}][Error]:{errorText} - Exception:{exception.Message}");
        }
    }

客戶端調用如下:

    static void Main(string[] args)
    {
        Logger logger = new Logger();
        logger.Info("This is a info text.");
        logger.Debug("This is a debug text.");
        logger.Warn("This is a warn text.");
        logger.Error("This is a error text", new Exception("This is a exception."));

        Console.ReadKey();
    }

輸出:

image

這看起來還不錯,一切都是那么自然。但是隨着時間的推移,產品做的好買了很多客戶,產品變得越來越大,使用Logger 類的地方成千上萬處,可怕的事情終於發生了:

A 客戶提出來我想把日志存在數據庫中便於做統計分析。

B 客戶說我想把日志打印在一個控制台上便於我時時監測系統運行情況。

C 客戶說我要把日志存到Windows Azure  Storage上。

。。。。

客戶越來越多奇葩需求不斷涌出。我們的產品變得很難修改,很難維護,很難去適合所有的客戶。 怎么辦呢? 回過頭來看看我們的這個日志系統的設計才恍然大悟:沒有遵守面向對象設計原則的依賴倒置原則和開閉原則了。知道就好,找到法門了, 我們將日志這一塊的設計重構一下讓其符合OCP和DIP應該就可以了。 那么我們就要首先抽象寫日志的接口ILog, 讓實際調用的地方調用高層抽象(ILog),具體的實現類TextLogger,ConsoleLogger,DatabaseLogger,AzureStorageLogger都繼承自ILog接口,然后我們在利用反射加配置,不同的用戶配置不同的具體實現類,這樣問題就迎任而解了。 看代碼:

    public interface ILog
    {
        void Info(string infoText);
        void Debug(string debugText);
        void Warn(string warmText);
        void Error(string errorText, Exception exception);
    }
    public class TextLogger:ILog
    {
        public void Info(string infoText)
        {
            Console.WriteLine($"[{DateTime.Now}][Info]:{infoText}");
        }

        public void Debug(string debugText)
        {
            Console.WriteLine($"[{DateTime.Now}][Debug]:{debugText}");
        }

        public void Warn(string warmText)
        {
            Console.WriteLine($"[{DateTime.Now}][Warm]:{warmText}");
        }

        public void Error(string errorText,Exception exception)
        {
            Console.WriteLine($"[{DateTime.Now}][Error]:{errorText} - Exception:{exception.Message}");
        }
    }

    public class DatabaseLogger:ILog
    {
        public void Info(string infoText)
        {
            Console.WriteLine($"[{DateTime.Now}][Info]:{infoText}");
        }

        public void Debug(string debugText)
        {
            Console.WriteLine($"[{DateTime.Now}][Debug]:{debugText}");
        }

        public void Warn(string warmText)
        {
            Console.WriteLine($"[{DateTime.Now}][Warm]:{warmText}");
        }

        public void Error(string errorText,Exception exception)
        {
            Console.WriteLine($"[{DateTime.Now}][Error]:{errorText} - Exception:{exception.Message}");
        }
    }
    public class ConsoleLogger:ILog
    {
        public void Info(string infoText)
        {
            Console.WriteLine($"[{DateTime.Now}][Info]:{infoText}");
        }

        public void Debug(string debugText)
        {
            Console.WriteLine($"[{DateTime.Now}][Debug]:{debugText}");
        }

        public void Warn(string warmText)
        {
            Console.WriteLine($"[{DateTime.Now}][Warm]:{warmText}");
        }

        public void Error(string errorText,Exception exception)
        {
            Console.WriteLine($"[{DateTime.Now}][Error]:{errorText} - Exception:{exception.Message}");
        }
    }

    public class AzureStorageLogger:ILog
    {
        public void Info(string infoText)
        {
            Console.WriteLine($"[{DateTime.Now}][Info]:{infoText}");
        }

        public void Debug(string debugText)
        {
            Console.WriteLine($"[{DateTime.Now}][Debug]:{debugText}");
        }

        public void Warn(string warmText)
        {
            Console.WriteLine($"[{DateTime.Now}][Warm]:{warmText}");
        }

        public void Error(string errorText,Exception exception)
        {
            Console.WriteLine($"[{DateTime.Now}][Error]:{errorText} - Exception:{exception.Message}");
        }
    }

添加一個配置在Config中:

    <appSettings>
        <add key="Logger" value="ConsoleApp1.TextLogger"/>
    </appSettings>

客戶端的調用改成調用ILog:

    static void Main(string[] args)
    {
        string key = ConfigurationManager.AppSettings["Logger"];
        ILog logger = ObjectBuildFactory<ILog>.Instance(key);
        logger.Info("This is a info text.");
        logger.Debug("This is a debug text.");
        logger.Warn("This is a warn text.");
        logger.Error("This is a error text", new Exception("This is a exception."));

        Console.ReadKey();
    }

輸出:

image

A客戶期望將日志寫在數據庫中只需要將配置改成下面這樣就可以了:

    <appSettings>
        <add key="Logger" value="ConsoleApp1.DatabaseLogger"/>
    </appSettings>

根據不同的客戶需求只需要改這個配置的value值就可以了。

要使上面的代碼順利運行我們要加一個輔助類用於反射:

public class ObjectBuildFactory<T>
{
    public static T Instance(string key)
    {
        Type obj = Type.GetType(key);
        if (obj == null) return default(T);

        T factory = (T)obj.Assembly.CreateInstance(obj.FullName);

        return factory;
    }
}

那么有一天E客戶說他們公司有自己的日志系統並開發了一套日志分析工具,他們可以開放API讓我們把日志直接存到他們的日志系統中去。 這次好辦了啊,只需要定義一個具體類繼承自ILog接口並實現所有的方法,在每一個實現的方法中調用客戶的API, 最后將實現的類配置到配置文件中就可以很好的滿足客戶的要求了, 這樣是不是很完美呢?我們完全遵守了DIP和OCP原則,也很好的使用了LSP,使得我們軟件變得穩定,應對需求的變化變得簡單了,也易於升級和易於維護了。

在使用DIP是需要注意一下幾點

1. 繼承自高層接口的類要實現所有接口中的方法。

2.子類中除了接口的方法,在用接口聲明的對象調用的地方是無法被調用到的。除非直接調用子類,但是直接調用子類是違背DIP的。

3. DIP是實現OCP的重要原則保障,一般違背了DIP很難不違背OCP,可以看這一篇【面向對象設計原則】之開閉原則(OCP)

4.LSP 是實現DIP的基礎,多態給實現DIP提供了可能。 可以看這一篇 【面向對象設計原則】之里氏替換原則(LSP)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM