【调侃】IOC前世今生
前些天,参与了公司内部小组的一次技术交流,主要是针对《IOC与AOP》,本着学而时习之的态度及积极分享的精神,我就结合一个小故事来初浅地剖析一下我眼中的“IOC前世今生”,以方便初学者能更直观的来学习与理解IOC!也作抛砖引玉之用。
(虽说故事中的需求有点小,但看客可在脑海中尽量把他放大,想象成一个很大的应用系统)
一、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);
李四坐享其成!
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);
李四再次坐享其成!!
二、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框架获取到想要的对象实例,然后再使用此实例。
④:两者生成实例的目的都是为了能动态创建实例,只不过创建的时机不一样。我个人认为依赖注入分离了逻辑控制相对来说层次性更清晰明了,但在需要注入多个对象时,却不及查找注入方式方便简洁。
三、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),代码很简单都没写注释。
(以上故事纯属虚构,若有雷同纯属巧合。你若喜欢,请点“推荐”!)