深入理解IOC控制反轉及應用例項
1、程式V1.0
話說,多年以前UT公司提出一個需求,要提供一個系統,其中有個功能可以在新春佳節之際給公司員工傳送一封郵件。郵件中給大家以新春祝福,並告知發放一定數額的過節費。
經分析,決定由張三、李四和王五來負責此係統的開發。
其中:由張三負責業邏輯控制模組 LogicController的開發,此處簡化為UT.LogicController.exe ;由李四負責祝福訊息管理類(GreetMessageService),並整合到元件
UT.MessageService.dll中;由王五負責郵件功能幫助類(EmailHelper),並提供元件 UT.Email.dll。
類依賴關係如下:
王五郵件功能模組核心程式碼如下:
public class EmailHelper
{
public void Send(string message)
{
Console.Write("Frome email: " + message);
}
}
李四訊息管理模組核心程式碼如下:
public class GreetMessageService { EmailHelper greetTool; public GreetMessageService() { greetTool = new EmailHelper(); } public void Greet(string message) { greetTool.Send(message); } }
張三業務整合模組核心程式碼如下:
string message = "新年快樂!過節費5000.";
MessageService.GreetMessageService service = new MessageService.GreetMessageService();
service.Greet(message);
三人經過一個月的艱苦奮戰,終於大功告成,系統也在春節其間成功發出問候信。企業如此關懷,給員工帶來無比的溫暖,因此深受全體員工好評!
春節過後,相應的功能也移植到了與“UT公司”相關的“UT編輯部”和“UT房產”類似的應用當中,並在後繼的“元宵”、“端午”、“中秋”等節日中得以廣泛應用。
2、程式V2.0
又是一個年關將至……
說真的,過節費的多少,有時可能直接影響整個假日的行程安排、從而影響假日的整體質量,因此部門領導高度重視。而郵件通知的方式,在邊遠山區常常因為受網路環境的影
響而無法正常收取,許多在外過年的同事對此頗有微詞。後經多方考證,決得采用當下非常主流的電話語言播報的方式進行通知。
於是乎,張三、李四、王五又忙起來了。但李四,卻有點頭疼了,因為他的模組現在不僅在“UT公司”內部使用,而且還在“UT編輯部”和“UT房產”也都有獨立執行。如何讓此處變
化影響最小,就得費點腦筋。為了達到較好的效果,李四決定按以下方式進行整改。
更改後的類關係圖如下:
首先為了能讓不同“祝福方式”能有效替換,決定以“面向介面”的方式來進行分離。同時,讓EmailHelper的郵件通知類和TelephoneHelper的語音播報類都實現此介面。核心程式碼如下:
public interface ISendable
{
void Send(string message);
}
public class EmailHelper : ISendable
{
public void Send(string message)
{
Console.Write("Frome email: " + message);
}
}
public class TelephoneHelper : ISendable
{
public void Send(string message)
{
Console.Write("Frome telephone: " + message);
}
}
再者,為了方便相容新舊產品,要求Controller決定當前採用什麼方式進行通訊,並以引數方式傳給訊息管理模組,核心程式碼如下:
public enum SendToolType
{
Email,
Telephone,
}
public class GreetMessageService
{
ISendable greetTool;
public GreetMessageService(SendToolType sendToolType)
{
if (sendToolType == SendToolType.Email)
{
greetTool = new UT.EmailV20.EmailHelper();
}
else if (sendToolType == SendToolType.Telephone)
{
greetTool = new UT.TelephoneV20.TelephoneHelper();
}
}
public void Greet(string message)
{
greetTool.Send(message);
}
}
最後,業務整合模組結合具體業務需求進行適當的調整,核心程式碼如下:
string message = "新年快樂!過節費5000.";
GreetMessageService service = new GreetMessageService(SendTool.Telephone);
service.Greet(message);
眼看即將完工,但李四卻越看越不順眼,因為考慮到以後可能再新增新的祝福方式,這種未來的不確定性,一定會讓李四現有的列舉SendToolType和 GreetMessageService中的建構函式不斷的進行更改,這將會是一個沒完沒了工作。
再說了,既然張三要傳SendToolType給我,也就是說在具體產品應用時,張三的模組肯定是知道要採用什麼方式進行祝福,那麼何不讓他直接把祝福方式的例項而不是簡單的方式型別給我呢?這樣,我不就省事了嗎,於是乎把設計進行了優化。
優化後關係圖如下:
又是一個月的苦戰……
王五的程式碼不受影響。
李四刪除 SendToolType列舉,同進把GreetMessageService改成如下:
public class GreetMessageService
{
ISendable greetTool;
public GreetMessageService(ISendable sendtool)
{
greetTool = sendtool;
}
public void Greet(string message)
{
greetTool.Send(message);
}
}
張三,也把業務邏輯控制部分改成如下:
string message = "新年快樂! 過節費5000.";
ISendable greetTool = new TelephoneHelper();
GreetMessageService service = new GreetMessageService(greetTool);
service.Greet(message);
最終:張三更新UT.LogicController.exe中的實現;李四更新了UT.MessageSevice.dll,王五提供新的元件:UT.Telephone.dll,並把介面整合到一個叫UT.Core.dll的庫中。經多方整合測試後系統執行良好!
【點評】:
李四此處成功的利用“介面分離”、並結合“依賴倒置”的方式,使得自己負責的模組初步具備了應對新增祝福方式的擴充套件要求。同時由於其採用的“依賴注入”方式要求李四的業務邏輯控制模組對其所需的 “ISendable”例項進行注入,理論上已經初步具體了“IOC反轉控制”的雛形。
對“IOC反轉控制”此時帶來的優勢就是:確保了“紅色框”內的模組是具有應對變化的能力,在後繼新增新祝福方式時,UT.MessageService.dll元件可以完全不做任何修改
3、V2.1
由於電話語言播報必須接聽、過後不便留底查詢等不足也常被人們詬病,因此簡訊通知的方式被提上議程。
在此要求下,王五提供了新的元件:UT.GSN.dll。核心程式碼如下:
public class SMSHelper : ISendable
{
public void Send(string message)
{
Console.WriteLine("Frome SMS: " + message);
}
}
張三程式碼如下:
string message = "新年快樂! 過節費5000.";
ISendable greetTool = new SMSHelper();
GreetMessageService service = new GreetMessageService(greetTool);
service.Greet(message);
李四坐享其成。
祝福方式日新月異人們的要求也是不斷髮展,沒過多久簡訊方式太呆板、資訊量不足等缺陷也暴露出來,微信深受大夥青睞。
在此要求下,王五提供了新的元件:UT.Wechat.dll。核心程式碼如下:
public class WechatHelper : ISendable
{
public void Send(string message)
{
Console.WriteLine("Frome wechat: " + message);
}
}
string message = "新年快樂! 過節費5000.";
ISendable greetTool = new WechatHelper();
GreetMessageService service = new GreetMessageService(greetTool);
service.Greet(message);
二、IOC擴充套件
由於採用了IOC反轉控制的思想,現在不管系統如何變化,李四負責的模組總的來說還是相當穩定,因此這些年李四過的可謂逍遙自在。然而,相比之下張三卻因為產品在UT公司、UT編輯部、UT房產等都有獨立應用,且各自使用的版本又不盡相同,因此要同時維護三個版本,可謂是焦頭爛額。
我們來看看此時的張三同時維護著三個系統,其中各自核心程式碼基本如下:
UT公司(微信方式)
string message = "新年快樂! 過節費5000.";
ISendable greetTool = new WechatHelper();
GreetMessageService service = new GreetMessageService(greetTool);
service.Greet(message);
UT編輯部(簡訊方式)
string message = "新年快樂! 過節費5000.";
ISendable greetTool = new SMSHelper();
GreetMessageService service = new GreetMessageService(greetTool);
service.Greet(message);
UT房產(郵件方式)
string message = "新年快樂! 過節費5000.";
ISendable greetTool = new EmailHelper();
GreetMessageService service = new GreetMessageService(greetTool);
service.Greet(message);
這些年,本著對工作和客戶的認真負責,張三長時間在這些“版本維護”、“產品相容”等髒活累活中摸爬滾打,現在是心力憔悴……
3、解決方案
為了實現“如何有效建立ISendable例項”的問題,張三引入了“工廠模式”,由於不同的祝福方式而產生的變化,封裝在一個獨立的“SendToolFactory”類中,這樣就算以後再有變化,只要更改此類中部分程式碼即可,而不影響程式中其他所有用到ISendable的地方。
【點評】:
以工廠模式來實現“ISendable”物件例項的建立,是一種典型的“高內聚”與“鬆耦合”的設計方式,它有效的使得應用程式核心部分並不用去關心繫統到底採用了什麼樣的“祝福方式”,而具體的“祝福方式”則在工廠模式內部進行建立。如果以後需求有變動,那也只需在工廠做少許修改即可,程式其他程式碼都將不受影響。
當成功解決完第一個問題後,我們立即拉開針對“如何能實現在新增祝福方式之後,有效的控制對“LogicController”模組的衝擊”這們問題上來。從目前程式的結構來看,在新增祝福方式之後的主要衝擊有兩方面:首先是更改工廠類中的程式碼用以建立新的例項;再者是引入新的動態庫。
最後我們決定採用“工廠模式+反射機制”的方式來解決上述難題,並在工廠模式中依靠配置檔案的節點資訊,然後採用“反射機制”來動態建立相應的例項;如此一來,以後就算再有新的祝福方式採用,也只需把王五新增的動態庫拷貝過來,然後再更改一下配置檔案中的節點資訊就行,不再需要更改任何程式原始碼,也不再需要重新編譯生成程式。
採用工廠模式建立例項
string message = "新年快樂! 過節費5000.";
ISendable greetTool = SendToolFactory.GetInstance();
GreetMessageService service = new GreetMessageService(greetTool);
service.Greet(message);
public abstract class SendToolFactory
{
public static ISendable GetInstance()
{
try
{
Assembly assembly = Assembly.LoadFile(GetAssembly()); // 載入程式集
object obj = assembly.CreateInstance(GetObjectType()); // 建立類的例項
return obj as ISendable;
}
catch
{
return null;
}
}
static string GetAssembly()
{
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ConfigurationManager.AppSettings["AssemblyString"]);
}
static string GetObjectType()
{
return ConfigurationManager.AppSettings["TypeString"];
}
}
配置檔案節點資訊
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!--<add key="AssemblyString" value="UT.EmailV20.dll" />
<add key="TypeString" value="UT.EmailV20.EmailHelper" />-->
<!--<add key="AssemblyString" value="UT.SMSV21.dll" />
<add key="TypeString" value="UT.SMSV21.SMSHelper" />-->
<add key="AssemblyString" value="UT.WechatV22.dll" />
<add key="TypeString" value="UT.WechatV22.WechatHelper" />
</appSettings>
</configuration>
自從V3.0推出後,基於“IOC反轉控制”的思想也算小有收穫,多年來產品執行良好,就算不斷有新的“祝福方式”出現,張三和李四也都不必再為之操心,同時也能適用“UT公司”、“UT編輯部”和“UT房產”等不同的場景要求,可謂皆大歡喜。
【點評】:
①:IOC反轉控制常見的實現手段之一就是DI依賴注入,而依賴注入的方式通常有:介面注入、Setter注入和建構函式注入。本次示例給出的程式碼具備“介面注入”的特徵,並通過建構函式來實現。
②:IOC反轉控制還有一種手段就是依賴查詢,這種方式一般先進行型別註冊,使用時進行查詢;對這種方式有興趣的朋友可以參考微軟企業庫中Microsoft.Practices.Unity.dll中的原始碼(https://entlib.codeplex.com/)和詳細的示例說明整理(如:Enterprise Library 4.1 HOL)。
③:依賴注入一般由呼叫者(LogicController)依賴IOC框架生成好例項物件,然後直接注入到被呼叫者(GreetMessageService)當中,被者用者內部直接使用此例項,程式碼流程清晰明瞭;而依賴查詢一般由呼叫者(LogicController)前期進行型別註冊,被呼叫者(GreetMessageService)內部依賴IOC框架獲取到想要的物件例項,然後再使用此例項。
④:兩者生成例項的目的都是為了能動態建立例項,只不過建立的時機不一樣。我個人認為依賴注入分離了邏輯控制相對來說層次性更清晰明瞭,但在需要注入多個物件時,卻不及查詢注入方式方便簡潔。
三、IOC框架
1、模式的複用
自從張三在上述產品開發過程中成功地總結出“IOC思想”後,在後繼的其他產品中進行了推廣與實踐。在使用的過程中,張三發現這樣的模式是可以很好的在模組間、產品間進行有效的複用,不僅大大提高了開發效率,對產品後繼的擴充套件和維護都帶來不少方便。
2、物件容器
當然,在對“IOC思想”的實踐中,張三還發現有些地方需要完善。比如,有時我們可能要建立單一物件例項,有時卻要要建立多個物件的例項,甚至有時要建立一系列例項;有時要建立一個本地的物件例項,有時卻要建立一個遠端的服務物件例項;等等…..
為了應對複雜的物件應用,張三把原來的“物件工廠”這樣的小作坊升級成了一個功能強大的、具有一定智慧水平的“IOC物件容器”,這個容器可以動態的依據引數設定或配置檔案來進行有策略性的物件建立與管理,使得整個框架對物件集的管理上升到了一個更高的層次。
3、IOC基礎框架
張三通過前期的“介面分離”及“依賴倒置”達到了“反轉控制”的效果,並結合有效的“依賴注入”方式,實現了系統的“鬆耦合”架構;再通過“工廠模式 + 反射機制”有效實現了物件的動態建立,並在後期升級成“物件容器”,大大減少新增需求對程式帶來的衝擊。通過以上方式,張三成功地摸索出一套行這有效且複用性高的“IOC基礎框架”。
4、IOC思想
後來,張三把摸索總結出的“IOC基礎框架”在公司各產品中進行了廣泛實踐,得到一致好評,並且被作為一個公共元件整合在一個叫“UT企業庫”的元件集中。從此,在張三的朋友圈中,IOC思想廣為流傳。
若干年後,我們發現EJB、Spring、Struts、Asp.netMVC等框架中都能看到IOC思想的影子,這些框架都對張三最初IOC的思想作了進一步的發揚、光大。
現在,IOC的思想在軟體設計與系統架構中大放異彩,然而非常遺憾中國人口中的那個神祕的張三至今也不知到底是誰。