代码结构演变

项目开始阶段

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

开发人员: 张三

版本一

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. Codeforces Round #285 (Div.1 B & Div.2 D) Misha and Permutations Summation --二分+树状数组

    题意:给出两个排列,求出每个排列在全排列的排行,相加,模上n!(全排列个数)得出一个数k,求出排行为k的排列. 解法:首先要得出定位方法,即知道某个排列是第几个排列.比如 (0, 1, 2), (0, ...

  2. UESTC 764 失落的圣诞节 --RMQ/线段树

    题意:n种物品,每种物品对不同的人都有不同的价值,有三个人选,第一个为普通学生,第二个是集,第三个是祈,集和祈可以选一样的,并且还会获得加分,集和祈选的普通学生都不能选,问三个人怎样选才能使总分最高. ...

  3. AC日记——配对碱基链 openjudge 1.7 07

    07:配对碱基链 总时间限制:  1000ms 内存限制:  65536kB 描述 脱氧核糖核酸(DNA)由两条互补的碱基链以双螺旋的方式结合而成.而构成DNA的碱基共有4种,分别为腺瞟呤(A).鸟嘌 ...

  4. javascript高级程序设计 读书笔记1

    第二章  在HTML中使用JS 加载JS有三种:行内,head头部和外部链接JS   最好使用外部链接<script src="example.js" ></sc ...

  5. umeng社交分享最新版5.0的跨进程使用崩溃的问题及解法-Android

    先简单介绍下5.0版的变化.5.0最大的特色是调用简单,采用了链式语法,形如: new ShareAction(context).setPlatform(share_media) .withText( ...

  6. javascript一些小问题

    1.async 类型:Boolean 默认值: true.默认设置下,所有请求均为异步请求.如果需要发送同步请求,请将此选项设置为 false. 注意,同步请求将锁住浏览器,用户其它操作必须等待请求完 ...

  7. 转载 Appstore 上传被拒原因及解释

    原 apps被拒绝的各种理由以及翻译 1. Terms and conditions(法律与条款) 2. Functionality(功能) 3. Metadata (name, descriptio ...

  8. 最大M子段和 V2

    51nod1053 这题还是我们熟悉的M子段和,只不过N,M<=50000. 这题似乎是一个堆+链表的题目啊 开始考虑把所有正数负数锁在一起. 比如: 1 2 3 -1 –2 -3 666 缩成 ...

  9. Sonatype Nexus Maven仓库搭建和管理

    安装 1. 从 http://www.sonatype.org/nexus/ 下载最新的 Nexus 压缩包, 现在已经不提供war包的下载 2. 解压到服务器目录, 例如我是放到/opt/nexus ...

  10. 后台运行程序screen or nohup

    后台运行 方法1 &   方法2:screen screen –S lnmp à起个名字 进去后运行程序 Ctrl+ad à退出lnmp屏幕 Scree –ls à查看 Screen –r x ...