代码结构演变
项目开始阶段
需求: 提供一个系统,可以在新春佳节之际以邮件的形式给员工发送新春祝福。
开发人员: 张三
版本一
string msg = "新年快乐!过节费5000.";
Console.Write("Frome email: " + msg);
版本二
消息发送
string msg = "新年快乐!过节费5000.";
EmailSender emailSender = new EmailSender();
emailSender.Send(msg);
邮件发送模块
public class EmailSender
{
public void Send(string msg)
{
Console.Write("Frome email: " + msg);
}
}
ps :将邮件发送的具体实现从邮件发送服务中抽离出去,分块实现。
需求变更
需求变更:要求增加发短信的方式,供另一部门使用。
祝福消息的发送方式不再是单一模式,要可以发邮件、也可以发短信。
版本三
消息发送方式变复杂了,将这一模块功能拆分出去,交由李四开发,并集成到组件 MessageService.dll中
一、MessageService.dll组件(李四)
消息发送工具
internal class EmailSender
{
public void Send(string msg)
{
Console.Write("Frome email: " + msg);
}
}
internal class SMSSender
{
public void Send(string msg)
{
Console.Write("Frome SMS: " + msg);
}
}
消息发送服务类
public class GreetMessageService
{
public enum SendType
{
Email,
SMS,
}
private SendType sendType;
public GreetMessageService(SendType sendType)
{
this.sendType = sendType;
}
public void Greet(string msg)
{
switch (sendType)
{
case SendType.Email:
EmailSender emailSender = new EmailSender();
emailSender.Send(msg);
break;
case SendType.SMS:
SMSSender sMSSender = new SMSSender();
sMSSender.Send(msg);
break;
}
}
}
二、消息发送(张三)
string message = "新年快乐!过节费5000.";
GreetMessageService service = new GreetMessageService(GreetMessageService.SendType.Email);
service.Greet(message);
分析:
引入了代理模式,核心业务模块不再直接调用信息发送工具,而是通过GreetMessageService类做了中转。
虽然对已有发送工具可以灵活使用了,但考虑到以后可能再添加新的工具,这种未来的不确定性,一定会让李四对现有代码不断地进行更改,这将会是一个没完没了的工作。
版本四
版本三遗留了一个问题:
从业务逻辑上讲,MessageService消息管理模块作为一个消息专用服务,对“是采用邮件还是短信方式发送消息”这样的功能性把控本身不具主动权。这一选择是在核心业务模块中确定的
GreetMessageService service = new GreetMessageService(GreetMessageService.SendType.Email);
将整个消息发送功能封装为一个对象,这个对象在面对需求变更时是不稳定的,每当增加新的发送方式,它的内部代码都需要做出变更,变化点有三处:
1. 增加新的工具类。
2. 枚举GreetMessageService.SendType 中增加新枚举值。
3. GreetMessageService.Greet 方法中的switch语句增加新的分支。
调整:
将MessageService.dll 组件进一步拆分为 MessageService.dll 和 SendTools.dll两个组件,其中SendTools.dll组件移交王五开发。
一、MessageService.dll 组件(李四)
public interface ISendable
{
void Send(string msg);
}
public class GreetMessageService
{
private ISendable greetTool;
public GreetMessageService(ISendable greetTool)
{
this.greetTool = greetTool;
}
public void Greet(string msg)
{
greetTool.Send(msg);
}
}
二、SendTools.dll组件(王五)
public class EmailSender : ISendable
{
public void Send(string msg)
{
Console.Write("Frome email: " + msg);
}
}
public class SMSSender: ISendable
{
public void Send(string msg)
{
Console.Write("Frome SMS: " + msg);
}
}
三、消息发送(张三)
string message = "新年快乐!过节费5000.";
ISendable sender = new EmailSender();
GreetMessageService service = new GreetMessageService(sender);
service.Greet(message);
结构上主要做了以下变动
1. 增加接口Isendable, 整合离散的各消息发送工具。
2. 将GreetMessageService中对消息发送工具Isendable的实例化操作委托给了上层模块。
分析:
要把消息发送工具的实例化操作委托给logicContollor模块,就导致原MessageService.dll中的封装被破坏,需要对外开放EmailSender和SMSSender的访问权限。
于是干脆把它们拆分到另外一个组件中
新增发送工具时王五只要对SendTools.dll进行扩展,然后提供新的组件就可以。
MessageService.dll中的代码因为引入Isendable,以及将new操作移交给上层,自身也保持了稳定。新增发送工具时李四不需要做任何改动。
总结:
这是一个标准的策略模式的应用,同时也是一个IOC反转控制的雏形。
在MessageService中创建 ISendable实例的操作,转交给了第三方(业务逻辑控制模块),由业务逻辑控制模块自主选择合适的ISendable实现,解除了MessageService与EmailSender、SMSSender间的强耦合关系。
新的架构确保了“红色框”内模块具有应对变化的能力。
版本五
在代码结构调整的过程中,自然而然地实现了分层。
LogicContoller ----> 控制器
MessageService.dll ----> 服务层(业务逻辑)
SendTools.dll ----> 基础组件
关于软件的分层:软件的分层、 软件架构模式之分层架构
软件需求总是在变的,但变化是有规律的,不易变化的需求叫稳定需求,而易变的需求叫不稳定的需求。
而软件设计分层就是为了在不同的层次上应对这些稳定性不同的需求,在上层设计中响应不稳定的需求,而在下层设计中实现稳定的需求。
这样结构自然形成一种上层依赖下层的关系,但这样的结构无法包容变化。在面向对象的设计中,有DIP(依赖倒置)原则,需要解开这种依赖关系。上层不再依赖下层,而是大家都依赖抽象层。而在上一版本中已经部分地形成了这种关系。为了更加清晰明了,对SendTools.dll、MessageService.dll进行分割
string message = "新年快乐!过节费5000.";
ISendable sender = new EmailSender();
IGreetMessageService service = new GreetMessageService(sender);
service.Greet(message);
新的问题
随着时间的推移,该程序被集成到了三款应用中,这些应用分别采用不同的消息发送方式,于是张三同时维护着三个系统。
其中各自核心代码基本如下:
UI公司(微信方式)
string message = "新年快乐! 过节费5000.";
ISendable greetTool = new WechatSender();
IGreetMessageService service = new GreetMessageService(greetTool);
service.Greet(message);
UI编辑部(短信方式)
string message = "新年快乐! 过节费5000.";
ISendable greetTool = new SMSSender();
IGreetMessageService service = new GreetMessageService(greetTool);
service.Greet(message);
UI房产(邮件方式)
string message = "新年快乐! 过节费5000.";
ISendable greetTool = new EmailSender();
IGreetMessageService service = new GreetMessageService(greetTool);
service.Greet(message);
当想变更发送工具的时候,张三还是要重新改写代码,程序有三份,张三就要随时应对这三方的变更需求。
如何减少新增祝福方式对“LogicController”模块的冲击,以减少维护成本。也就是如何把张三也从这频繁的代码变动中解脱出来呢。
版本六
张三对业务逻辑控制模块做了调整,使用配置文件来避开对现有代码的频繁改动。通过反射操作依据配置文件中的信息来创建合适的实例。
增加简单工厂模式
public abstract class SendToolFactory
{
public static ISendable GetInstance()
{
string assemblyName = GetAssembly();
string objectTypeName = GetObjectType();
try
{
Assembly assembly = Assembly.LoadFile(assemblyName);
object obj = assembly.CreateInstance(objectTypeName);
return obj as ISendable;
}
catch(Exception e)
{
Console.WriteLine("1:"+e.Message);
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>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
</startup>
<appSettings>
<add key="AssemblyString" value="SendTools4.dll" />
<add key="TypeString" value="SendTools4.SMSSender" />
</appSettings>
</configuration>
string message = "新年快乐! 过节费5000.";
ISendable greetTool = SendToolFactory.GetInstance();
if (greetTool != null)
{
IGreetMessageService service = new GreetMessageService(greetTool);
service.Greet(message);
}
现在,张三也可以悠闲了,用户自己就可以应对这个需求变更了。
版本七
在大项目中,这种依赖关系随处可见,为了系统的灵活可变,很多变化点都需要用到IoC模式。这时就会专门封装一个组件, 来统一管理对象的生命周期及它们的依赖关系,这个组件就是作为IoC容器。
张三在项目中引入Autofac
代码调整如下
版本1. 直接指定实例类型
string message = "新年快乐! 过节费5000.";
ContainerBuilder builder = new ContainerBuilder();
builder.RegisterType<GreetMessageService>().As<IGreetMessageService>();
builder.RegisterType<SMSSender>().As<ISendable>();
using (var container = builder.Build())
{
var manager = container.Resolve<IGreetMessageService>();
manager.Greet(message);
}
版本2.使用配置文件 http://docs.autofac.org/en/latest/configuration/xml.html
string message = "新年快乐! 过节费5000.";
ConfigurationBuilder config = new ConfigurationBuilder();
config.AddJsonFile("autofac.json");
ConfigurationModule module = new ConfigurationModule(config.Build());
ContainerBuilder builder = new ContainerBuilder();
builder.RegisterModule(module);
using (var container = builder.Build())
{
var manager = container.Resolve<IGreetMessageService>();
manager.Greet(message);
}
JSON文件
{
"defaultAssembly": "IOCDemo5",
"components": [
{
"type": "SendTools5.EmailSender,SendTools5",
"services": [
{
"type": "ISendTools5.ISendable,ISendTools5"
}
],
"injectProperties": true
},
{
"type": "MessageService5.GreetMessageService, MessageService5",
"services": [
{
"type": "IMessageService5.IGreetMessageService,IMessageService5"
}
]
}
]
}
源码下载
概念
DIP : 依赖倒置(Dependence Inversion Principle)
IOC : 控制反转 (Inverse of Control)
DI: 依赖注入(Dependency Injection)
依赖倒置
Bob Martins对DIP的定义:
高层模块不应依赖于低层模块,两者应该依赖于抽象。
抽象不应该依赖于实现,实现应该依赖于抽象。
DIP是软件设计原则。
控制反转(IoC)
它是一种 软件设计模式,用来解除相互依赖模块的耦合。它为相互依赖的组件提供抽象,将依赖(低层模块)对象的获得交给第三方(系统)来控制,即依赖对象不在被依赖模块的类中直接通过new来获取。如上例:将信息发送工具类的实例化提升到业务逻辑模块里。
场景
大多数应用程序都是由两个或是更多的类通过彼此的合作来实现业务逻辑,这使得每个对象都需要与其合作的对象(也就是它所依赖的对象)的引用。如果这个获取过程要靠自身实现,那么这将导致代码高度耦合并且难以维护和调试。
Class A中用到了Class B的对象b,一般情况下,需要在A的代码中显示的new一个B的对象。
(软件系统中耦合的对象)
(对象之间的依赖关系)
IOC理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦。采用依赖注入技术之后,A的代码只需要定义一个私有的B对象,不需要直接new来获得这个对象,而是通过相关的容器控制程序来将B对象在外部new出来注入到A类里的引用中。而具体获取的方法、对象被获取时的状态由配置文件(如XML)来指定。
(IOC解耦过程)
由于引进了中间位置的“第三方”,也就是IOC容器,使得A、B、C、D这4个对象没有了耦合关系,齿轮之间的传动全部依靠“第三方”了,全部对象的控制权全部上缴给“第三方”IOC容器,所以,IOC容器成了整个系统的关键核心,它起到了一种类似“粘合剂”的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个“粘合剂”,对象与对象之间会彼此失去联系,这就是有人把IOC容器比喻成“粘合剂”的由来。
实现方法
实现控制反转主要有两种方式:依赖注入(Dependency Injection,简称DI)和依赖查找(Dependency Lookup)。两者的区别在于,前者是被动的接受对象,在类A的实例创建过程中即创建了依赖的B对象,通过类型或名称来判断将不同的对象注入到不同的不同的属性中,而后者是主动索取响应名称的对象,获得依赖对象的时间也可以在代码中自由控制。
依赖注入
依赖注入有如下实现方式:
基于接口:实现特定接口以供外部容器注入所依赖类型的对象。
基于set方法:实现特定属性的public set方法,来让外部容器调用传入所依赖类型的对象。
基于构造函数:实现特定参数的构造函数,在新建对象时传入所依赖类型的对象。
基于注解:基于注解功能,在私有变量前加“@Autowired”等注解,不需要显示的定义以上三种代码,便可以让外部容器传入对象的容器。该方案相当于定义了public的set方法,但是因为没有真正的set方法,从而不会为了实现依赖注入导致暴露了不该暴露的接口。
依赖查找
依赖查找更加主动,在需要的时候通过调用框架提供的方法来获取对象,获取时需要提供相关的配置文件路径等信息来确定获取对象的状态。
IoC容器
IoC容器实际上是一个DI框架,它能简化我们的工作量。它包含以下几个功能:
- 动态创建、注入依赖对象。
- 管理对象生命周期。
- 映射依赖关系。
IoC容器管理着对象的生命周期及它们的依赖关系
倒转了什么
由于IoC容器的加入,对象A与对象B之间失去了直接联系。当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是“控制反转”这个名称的由来。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被IoC容器控制,所以这叫控制反转。
对于编程来说,最大的改变不是代码结构上的,而是思想上的。原本是应用程序需要什么都主动出击。现在则编程被动地等待IOC容器创建并注入它所需要的资源(对象、资源、常数数据等)。很好地体现了好莱坞原则:别来找我,我们找你。
IoC和DI
其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊,所以2004年Martin Fowler又给出了一个新的名字:“依赖注入”,相对IoC 而言,“依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”。