代码结构演变

项目开始阶段

需求: 提供一个系统,可以在新春佳节之际以邮件的形式给员工发送新春祝福。

开发人员: 张三

版本一

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容器配置依赖对象”。

To IOC,代码结构演变的随想的更多相关文章

  1. PHP扩展代码结构详解

    PHP扩展代码结构详解: 这个是继:使用ext_skel和phpize构建php5扩展  内容 (拆分出来) Zend_API:深入_PHP_内核:http://cn2.php.net/manual/ ...

  2. CNN结构演变总结(二)轻量化模型

    CNN结构演变总结(一)经典模型 导言: 上一篇介绍了经典模型中的结构演变,介绍了设计原理,作用,效果等.在本文,将对轻量化模型进行总结分析. 轻量化模型主要围绕减少计算量,减少参数,降低实际运行时间 ...

  3. 【Learning Python】【第四章】Python代码结构(一)

    这一章的主旨在于介绍python的代码结构 缩进 在很多的编程语言中,一般{}用于控制代码块,比如以下的一段C代码 if(var <= 10) { printf("....." ...

  4. 《OOC》笔记(4)——自动化地将C#代码转化为C代码(结构版)

    <OOC>笔记(4)——自动化地将C#代码转化为C代码(结构版) 我在<C表达面向对象语言的机制——C#版>中已经说明了从C#到C的转换方法.这次看<OOC>也是想 ...

  5. CEF3开发者系列之工程和代码结构

    CEF支持一系列的编程语言和操作系统,并且能很容易地整合到新的或已有的工程中去.它的设计思想就是易用且兼顾性能. CEF3支持一系列的编程语言和操作系统,并且能很容易地整合到新的或已有的工程中去.它的 ...

  6. storm源码之storm代码结构【译】【转】

    [原]storm源码之storm代码结构[译]  说明:本文翻译自Storm在GitHub上的官方Wiki中提供的Storm代码结构描述一节Structure of the codebase,希望对正 ...

  7. atitit js 开发工具 ide的代码结构显示(func list) outline总结

    atitit js 开发工具 ide的代码结构显示(func list) outline总结 eclips环境::4.3.1 #-------需要一个js开发工具,可以显示outline或者代码结构显 ...

  8. PHPWind 8.7中代码结构与程序执行顺序

    pw9在此不谈,他是完全重构的作品,是完全MVC下的体系.当然,其中很多东西在PW8.7下已经可见端倪. 主要代码结构 1. 以现代的观点,PW是多入口应用模式,程序根目录下的文件几乎都是入口: 2. ...

  9. Javascript的一种代码结构方式——插件式

    上几周一直在做公司的webos的前端代码的重构,之中对javascript的代码进行了重构(之前的代码耦合严重.拓展.修改起来比较困难),这里总结一下当中使用的一种代码结构——插件式(听起来怎么像独孤 ...

随机推荐

  1. USACO1.5Superprime Rid[附带关于素数算法时间测试]

    题目描述 农民约翰的母牛总是产生最好的肋骨.你能通过农民约翰和美国农业部标记在每根肋骨上的数字认出它们.农民约翰确定他卖给买方的是真正的质数肋骨,是因为从右边开始切下肋骨,每次还剩下的肋骨上的数字都组 ...

  2. using关键字的使用

    using语句的两个作用: 1)using可以导入命名空间 2)using可以释放对象占用的内存资源. 代码如下: using (SqlConnection con=new SqlConnection ...

  3. 文件上传&文件下载

    一.单个文件上传 文件上传需要两个jar包: 首先制作一个简单的页面,用于实现文件上传 <h1>单个文件上传</h1> <s:form action="uplo ...

  4. 以Access为支撑,书写一个C#写入记录的案例

    /// <summary> /// 读取Excel文档 /// </summary> /// <param name="Path">文件名称&l ...

  5. eclipse的包的加减号展开方式

    这是win7系统下面 导航树的风格 可能你不太习惯 一个最简单的方法: 桌面新建个 eclipse 快捷方式--->右键属性--->兼容性  勾上以兼容模式运行这个程序  

  6. Android 下载网络图片保存到本地

    通过网络地址获取网络图片,点击下载将图片显示出来,然后点击图片将图片保存到本地. 首先需要在manifest上添加一些权限: <!-- 访问网络的权限 --> <uses-permi ...

  7. Json转换类库

    20160605 简单的DaTable转Json private string DtConvertJson(DataTable dt , string modelName="") ...

  8. PAT 1014. 福尔摩斯的约会 (20)

    大侦探福尔摩斯接到一张奇怪的字条:"我们约会吧! 3485djDkxh4hhGE 2984akDfkkkkggEdsb s&hgsfdk d&Hyscvnm".大侦 ...

  9. log4j2 与 spring mvc整合

    log4j2不仅仅是log4j的简单升级,而是整个项目的重构,官网地址:http://logging.apache.org/log4j/2.x/,大家可以从官网的介绍看出它相比log4j第1代的种种优 ...

  10. Spring Security笔记:Remember Me(下次自动登录)

    前一节学习了如何限制登录尝试次数,今天在这个基础上再增加一点新功能:Remember Me. 很多网站,比如博客园,在登录页面就有这个选项,勾选“下次自动登录”后,在一定时间段内,只要不清空浏览器Co ...