Sping IOC的演變過程

前些天,參與了公司內部小組的一次技術交流,主要是針對《IOC與AOP》,本着學而時習之的態度及積極分享的精神,我就結合一個小故事來初淺地剖析一下我眼中的“IOC前世今生”,以方便初學者能更直觀的來學習與理解IOC!也作拋磚引玉之用。

(雖說故事中的需求有點小,但看客可在腦海中儘量把他放大,想象成一個很大的應用系統)

一、IOC雛形

1、程序V1.0

話說,多年以前UT公司提出一個需求,要提供一個系統,其中有個功能可以在新春佳節之際給公司員工發送一封郵件。郵件中給大家以新春祝福,並告知發放一定數額的過節費。

經分析,決定由張三、李四和王五來負責此係統的開發。

其中:由張三負責業邏輯控制模塊 LogicController的開發,此處簡化爲UT.LogicController.exe ;由李四負責祝福消息管理類(GreetMessageService),並集成到組件 UT.MessageService.dll中;由王五負責郵件功能幫助類(EmailHelper),並提供組件 UT.Email.dll。

類依賴關係如下:

王五郵件功能模塊核心代碼如下:

1
2
3
4
5
6
7
public class EmailHelper
{
    public void Send(string message)
    {
        Console.Write("Frome email: " + message);           
    }
}

李四消息管理模塊核心代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class GreetMessageService
{
    EmailHelper greetTool;
 
    public GreetMessageService()
    {
        greetTool = new EmailHelper();
    }
 
    public void Greet(string message)
    {
        greetTool.Send(message);
    }
}

張三業務集成模塊核心代碼如下:

1
2
3
string message = "新年快樂!過節費5000.";
MessageService.GreetMessageService service = new MessageService.GreetMessageService();
service.Greet(message);

三人經過一個月的艱苦奮戰,終於大功告成,系統也在春節其間成功發出問候信。企業如此關懷,給員工帶來無比的溫暖,因此深受全體員工好評!

春節過後,相應的功能也移植到了與“UT公司”相關的“UT編輯部”和“UT房產”類似的應用當中,並在後繼的“元宵”、“端午”、“中秋”等節日中得以廣泛應用。

2、程序V2.0

又是一個年關將至……

說真的,過節費的多少,有時可能直接影響整個假日的行程安排、從而影響假日的整體質量,因此部門領導高度重視。而郵件通知的方式,在邊遠山區常常因爲受網絡環境的影響而無法正常收取,許多在外過年的同事對此頗有微詞。後經多方考證,決得采用當下非常主流的電話語言播報的方式進行通知。

於是乎,張三、李四、王五又忙起來了。但李四,卻有點頭疼了,因爲他的模塊現在不僅在“UT公司”內部使用,而且還在“UT編輯部”和“UT房產”也都有獨立運行。如何讓此處變化影響最小,就得費點腦筋。爲了達到較好的效果,李四決定按以下方式進行整改。

    ①、初始設計方案如下:

首先爲了能讓不同“祝福方式”能有效替換,決定以“面向接口”的方式來進行分離。同時,讓EmailHelper的郵件通知類和TelephoneHelper的語音播報類都實現此接口。核心代碼如下:

1
2
3
4
public interface ISendable
{
    void Send(string message);
}
1
2
3
4
5
6
7
public class EmailHelper : ISendable
{
    public void Send(string message)
    {
        Console.Write("Frome email: " + message);
    }
}
1
2
3
4
5
6
7
public class TelephoneHelper : ISendable
{
    public void Send(string message)
    {
        Console.Write("Frome telephone: " + message);
    }
}

再者,爲了方便兼容新舊產品,要求Controller決定當前採用什麼方式進行通信,並以參數方式傳給消息管理模塊,核心代碼如下:

1
2
3
4
5
public enum SendToolType
{
    Email,
    Telephone,
}

【備註】:上述代碼,並不是一個優秀的設計,在後繼的優化方案當中將被去除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
    }
}

【備註】:上述代碼,並不是一個優秀的設計,在後繼的優化方案當中將被優化。

最後,業務集成模塊結合具體業務需求進行適當的調整,核心代碼如下:

1
2
3
string message = "新年快樂!過節費5000.";
GreetMessageService service = new GreetMessageService(SendTool.Telephone);
service.Greet(message);

眼看即將完工,但李四卻越看越不順眼,因爲考慮到以後可能再添加新的祝福方式,這種未來的不確定性,一定會讓李四現有的枚舉SendToolType和 GreetMessageService中的構造函數不斷的進行更改,這將會是一個沒完沒了工作。

再說了,既然張三要傳SendToolType給我,也就是說在具體產品應用時,張三的模塊肯定是知道要採用什麼方式進行祝福,那麼何不讓他直接把祝福方式的實例而不是簡單的方式類型給我呢?這樣,我不就省事了嗎,於是乎把設計進行了優化。

   ②、優化後設計方案:

又是一個月的苦戰……

王五的代碼不受影響。

李四刪除 SendToolType枚舉,同進把GreetMessageService改成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class GreetMessageService
{
    ISendable greetTool;
 
    public GreetMessageService(ISendable sendtool)
    {
        greetTool = sendtool;
    }
 
    public void Greet(string message)
    {
        greetTool.Send(message);
    }
}

張三,也把業務邏輯控制部分改成如下:

1
2
3
4
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。核心代碼如下:

1
2
3
4
5
6
7
public class SMSHelper : ISendable
{
    public void Send(string message)
    {
        Console.WriteLine("Frome SMS: " + message);
    }
}

張三也把代碼改成了如下,

1
2
3
4
string message = "新年快樂! 過節費5000.";
ISendable greetTool = new SMSHelper();
GreetMessageService service = new GreetMessageService(greetTool);
service.Greet(message);

李四坐享其成!

4、V2.2

祝福方式日新月異人們的要求也是不斷髮展,沒過多久短信方式太呆板、信息量不足等缺陷也暴露出來,微信深受大夥青睞。

在此要求下,王五提供了新的組件:UT.Wechat.dll。核心代碼如下:

1
2
3
4
5
6
7
public class WechatHelper : ISendable
{
    public void Send(string message)
    {
        Console.WriteLine("Frome wechat: " + message);
    }
}

張三也把代碼改成了如下:

1
2
3
4
string message = "新年快樂! 過節費5000.";
ISendable greetTool = new WechatHelper();
GreetMessageService service = new GreetMessageService(greetTool);
service.Greet(message);

李四再次坐享其成!!

二、IOC擴展

1、李四的逍遙自在與張三的焦頭爛額

由於採用了IOC反轉控制的思想,現在不管系統如何變化,李四負責的模塊總的來說還是相當穩定,因此這些年李四過的可謂逍遙自在。然而,相比之下張三卻因爲產品在UT公司、UT編輯部、UT房產等都有獨立應用,且各自使用的版本又不盡相同,因此要同時維護三個版本,可謂是焦頭爛額。

當然張三曾經也想統一各個版本,從而實現代碼的統一維護。爲此還專門與各相關主管溝通過、協調過,然而由爲UT編輯部與電信服務商早有合作所有短信免費,因此短信方式最得人心;而UT房產基於對信息接收者身份的特殊性考慮,郵件通知被認爲是不二選擇。因此,張三統一版本的夢想最終還是無果而終。

我們來看看此時的張三同時維護着三個系統,其中各自核心代碼基本如下:

UT公司(微信方式)

1
2
3
4
string message = "新年快樂! 過節費5000.";
ISendable greetTool = new WechatHelper();
GreetMessageService service = new GreetMessageService(greetTool);
service.Greet(message);

UT編輯部(短信方式)

1
2
3
4
string message = "新年快樂! 過節費5000.";
ISendable greetTool = new SMSHelper();
GreetMessageService service = new GreetMessageService(greetTool);
service.Greet(message);

UT房產(郵件方式)

1
2
3
4
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

採用工廠模式創建實例

1
2
3
4
string message = "新年快樂! 過節費5000.";
ISendable greetTool = SendToolFactory.GetInstance();
GreetMessageService service = new GreetMessageService(greetTool);
service.Greet(message);

工廠中的實現

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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"];
      }
}

配置文件節點信息

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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的思想在軟件設計與系統架構中大放異彩,然而非常遺憾中國人口中的那個神祕的張三至今也不知到底是誰。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章