前些天,參與了公司內部小組的一次技術交流,主要是針對《IOC與AOP》,本着學而時習之的態度及積極分享的精神,我就結合一個小故事來初淺地剖析一下我眼中的「IOC前世此生」,以方便初學者能更直觀的來學習與理解IOC!也做拋磚引玉之用。微信
(雖然說故事中的需求有點小,但看客可在腦海中儘可能把他放大,想象成一個很大的應用系統)網絡
1、IOC雛形架構
1、程序V1.0app
話說,多年之前UT公司提出一個需求,要提供一個系統,其中有個功能能夠在新春佳節之際給公司員工發送一封郵件。郵件中給你們以新春祝福,並告知發放必定數額的過節費。框架
經分析,決定由張3、李四和王五來負責此係統的開發。函數
其中:由張三負責業邏輯控制模塊 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); } }
李四消息管理模塊核心代碼以下:spa
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
又是一個年關將至……
說真的,過節費的多少,有時可能直接影響整個假日的行程安排、從而影響假日的總體質量,所以部門領導高度重視。而郵件通知的方式,在邊遠山區經常由於受網絡環境的影響而沒法正常收取,許多在外過年的同事對此很有微詞。後經多方考證,決得采用當下很是主流的電話語言播報的方式進行通知。
因而乎,張3、李4、王五又忙起來了。但李四,卻有點頭疼了,由於他的模塊如今不只在「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);
李四不勞而獲!
4、V2.2
祝福方式突飛猛進人們的要求也是不斷髮展,沒過多久短信方式太呆板、信息量不足等缺陷也暴露出來,微信深受大夥青睞。
在此要求下,王五提供了新的組件: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);
李四再次不勞而獲!!
2、IOC擴展
1、李四的逍遙自在與張三的焦頭爛額
因爲採用了IOC反轉控制的思想,如今無論系統如何變化,李四負責的模塊總的來講仍是至關穩定,所以這些年李四過的可謂逍遙自在。然而,相比之下張三卻由於產品在UT公司、UT編輯部、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);
這些年,本着對工做和客戶的認真負責,張三長時間在這些「版本維護」、「產品兼容」等髒活累活中摸爬滾打,如今是心力憔悴……
2、張三的出路
某日張三與李四觥籌交錯、把酒言歡……
酒過三巡,張三對李四說:當年你的模塊因「IOC反轉控制」而脫身,卻把「變化點」反轉到我模塊,由我來生成特定的對象,而後再向你注入。這樣你是輕鬆了,但我卻深陷泥潭……
面對張三的吐槽,李四隻能給張三進行細心分析:
首先、MessageService消息管理模塊做爲一個消息專用服務,其實對「是採用郵件仍是微信方式進行祝福」這樣的功能性把控自己是不具主動權,由這個模塊來負責實在是有點鞭長莫及,即使強扭到一塊兒,這瓜也鐵定甜不了。
還有,本着單一職責的原則本消息服務實際上是不方便過多地去處理本應該是業務邏輯處理的相似「選擇祝福方式」這種事情。理論上,做爲業務集成方的「LogicController」負責處理這類業務應該是責無傍代。
再者,做爲新增需求,王五爲此而新增組件(dll)那是必不可少;張三做爲業務的總集成方也是難以脫身;因爲新增需求而引發的變化,對張三和王五產生影響也是情理之中。即使退一萬步來講,就算沒有「反轉控制」張三也是要面對變化的(就像V2.0初始方案中的傳入SendToolType參數),所以有無「反轉控制」對張三而言該變的始終仍是要變化。那麼如今採用「IOC反轉控制」而成全了李四的穩定,對張三來講這是個「利人不損已」的買賣。
最後,無論從架構設計仍是開發效率上來講,「IOC反轉控制」雖然說把變化點從李四的「MessageService」模塊反轉到了張三的「LogicController」模塊當中,但這符合「SOLID面向對象設計」的原則,能夠說是一個好的設計,本無可厚非!
聽完李四的論述,張三以爲甚是有理,酒難免醒了三分!因爲兩人都是這個行業打拼多年的老鳥,爭論也是點到即止。立刻把交流的重點轉移到「如何解決張三同時維護三個產品」的尷尬處境上來。
通過深刻分析,兩人以爲要脫困必須解決好以下兩個問題:
①:如何有效建立「ISendable」實例,減小因爲新增祝福方式對實例建立的影響?
②:如何減小新增祝福方式而對「LogicController」模塊的衝擊,以減小維護成本?
【備註】
SOLID面向對象的五個設計原則對於開發人員很是重要,其身影在任何大中型軟件項目中隨處可見,建議必須掌握並靈活應用。此五原則分別爲:
單一職責原則(Single Resposibility Principle)
開放封閉原則(Open Closed principle)
里氏替換原則(Liskov Substitution Principle)
接口分離原則(Interface Segregation Principle)
依賴倒置原則 (Dependency Inversion Principle)
3、解決方案
爲了實現「如何有效建立ISendable實例」的問題,張三引入了「工廠模式」,因爲不一樣的祝福方式而產生的變化,封裝在一個獨立的「SendToolFactory」類中,這樣就算之後再有變化,只要更改此類中部分代碼便可,而不影響程序中其餘全部用到ISendable的地方。
【點評】:
以工廠模式來實現「ISendable」對象實例的建立,是一種典型的「高內聚」與「鬆耦合」的設計方式,它有效的使得應用程序核心部分並不用去關心繫統到底採用了什麼樣的「祝福方式」,而具體的「祝福方式」則在工廠模式內部進行建立。若是之後需求有變更,那也只需在工廠作少量修改便可,程序其餘代碼都將不受影響。
當成功解決完第一個問題後,咱們當即拉開針對「如何能實如今新增祝福方式以後,有效的控制對「LogicController」模塊的衝擊」這們問題上來。從目前程序的結構來看,在新增祝福方式以後的主要衝擊有兩方面:首先是更改工廠類中的代碼用以建立新的實例;再者是引入新的動態庫。
最後咱們決定採用「工廠模式+反射機制」的方式來解決上述難題,並在工廠模式中依靠配置文件的節點信息,而後採用「反射機制」來動態建立相應的實例;如此一來,之後就算再有新的祝福方式採用,也只需把王五新增的動態庫拷貝過來,而後再更改一下配置文件中的節點信息就行,再也不須要更改任何程序源代碼,也再也不須要從新編譯生成程序。
4、程序V3.0
採用工廠模式建立實例
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框架獲取到想要的對象實例,而後再使用此實例。
④:二者生成實例的目的都是爲了能動態建立實例,只不過建立的時機不同。我我的認爲依賴注入分離了邏輯控制相對來講層次性更清晰明瞭,但在須要注入多個對象時,卻不及查找注入方式方便簡潔。
3、IOC框架
1、模式的複用
自從張三在上述產品開發過程當中成功地總結出「IOC思想」後,在後繼的其餘產品中進行了推廣與實踐。在使用的過程當中,張三發現這樣的模式是能夠很好的在模塊間、產品間進行有效的複用,不只大大提升了開發效率,對產品後繼的擴展和維護都帶來很多方便。
2、對象容器
固然,在對「IOC思想」的實踐中,張三還發現有些地方須要完善。好比,有時咱們可能要建立單一對象實例,有時卻要要建立多個對象的實例,甚至有時要建立一系列實例;有時要建立一個本地的對象實例,有時卻要建立一個遠端的服務對象實例;等等…..
爲了應對複雜的對象應用,張三把原來的「對象工廠」這樣的小做坊升級成了一個功能強大的、具備必定智能水平的「IOC對象容器」,這個容器能夠動態的依據參數設定或配置文件來進行有策略性的對象建立與管理,使得整個框架對對象集的管理上升到了一個更高的層次。
3、IOC基礎框架
張三經過前期的「接口分離」及「依賴倒置」達到了「反轉控制」的效果,並結合有效的「依賴注入」方式,實現了系統的「鬆耦合」架構;再經過「工廠模式 + 反射機制」有效實現了對象的動態建立,並在後期升級成「對象容器」,大大減小新增需求對程序帶來的衝擊。經過以上方式,張三成功地摸索出一套行這有效且複用性高的「IOC基礎框架」。
4、IOC思想
後來,張三把摸索總結出的「IOC基礎框架」在公司各產品中進行了普遍實踐,獲得一致好評,而且被做爲一個公共組件集成在一個叫「UT企業庫」的組件集中。今後,在張三的朋友圈中,IOC思想廣爲流傳。
若干年後,咱們發現EJB、Spring、Struts、Asp.netMVC等框架中都能看到IOC思想的影子,這些框架都對張三最初IOC的思想做了進一步的發揚、光大。
如今,IOC的思想在軟件設計與系統架構中大放異彩,然而很是遺憾中國人口中的那個神祕的張三至今也不知究竟是誰。
四:源代碼
1、開發環境爲:VS2010 + NET4.0 + Windos7
2、下載示例源代碼(IOCDemo),代碼很簡單都沒寫註釋。
(以上故事純屬虛構,如有雷同純屬巧合。你若喜歡,請點「推薦」!)