依賴倒轉原則(Dependency Inversion Principle, DIP):抽象不應該依賴於細節,細節應當依賴於抽象。換言之,要針對抽象(介面)編程,而不是針對實現細節編程。 開閉原則(OCP)是面向對象設計原則的基礎也是整個設計的一個終極目標,而依賴倒置原則(DIP )則是實現OCP原 ...
依賴倒轉原則(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(); }
輸出:
這看起來還不錯,一切都是那麼自然。但是隨著時間的推移,產品做的好買了很多客戶,產品變得越來越大,使用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(); }
輸出:
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)。