Dependency Injection或Inverse of Control(控制反转)设计模式来源于架构设计的一个基本原则:
1. Dependency Inverse Policy (依赖倒置/依赖反转)
什么是依赖倒置/依赖反转,看一个简单的例子:一个控制台程序读入用户从键盘输入的字符串输出到屏幕。
static void Main(string[] args)
{
Console.WriteLine("Demo DIP ");
string s = Console.ReadLine();
Console.WriteLine("your input is"+s);
Console.Read();
},呀,代码逻辑耦合太紧,稍微拆分一下,采用一些C里面过程式的设计,改为:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Demo DIP ");
string s = MessageCreater();
MessageProcessor(s);
Console.Read();
}
static string MessageCreater()
{
return Console.ReadLine();
}
static void MessageProcessor(string s)
{
Console.WriteLine("your input is" + s);
}
}
自从有了面向对象,那么我们就可以用对象来封装下面两个函数
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Demo DIP ");
string s = new ConsoleMessageCreator().Create();
new ConsoleMessageProcessor().Process(s);
Console.Read();
}
}
public class ConsoleMessageCreator
{
public string Create()
{
return Console.ReadLine();
}
}
public class ConsoleMessageProcessor
{
public void Process(string s)
{
Console.WriteLine("your input is" + s);
}
}
看起来一切不错,代码逻辑封装在各自的类里面,如果用户需求没有变化,那一切没有问题。但是某一天客户说如果消息是Order就要输出到打印机,否则到屏幕.想了下代码要改为:
public class PrinterMessageProcessor (增加)
{
public void Process(string s)
{
Printer.Print(s);
}
}
…
static void Main(string[] args)
{
Console.WriteLine("Demo DIP ");
string s = new ConsoleMessageCreator().Create();
if (s == "order")
{
new PrinterMessageProcessor().Process(s);
}
else
{
new ConsoleMessageProcessor().Process(s);
}
Console.Read();
}
问题开始来了,Main里面的代码一直会改下去,有新的消息处理器就要加if then else,或者switch….
问题的根源在哪里?可以看一下组 件关系图。
上层对象main依赖于底层对象PrinterMessageProcessor, ConsoleMessageProcessor, ConsoleMessageCreator的具体实现,导致上层对象不断变化以应付底层的变化,这就是我们所说的主体依赖细节,上层逻辑依赖底层实现。从而导致上层main组件一直处于修改中,难以稳定(稳定在OO另一层意思可以理解为复用。)这样的设计很难解决需求变化的问题。(在此是消息处理机制的变化)
那么,应该如何解决以上问题呢? 这就涉及到架构设计的另一些原则了。
1) 针对接口/抽象编程
2) 提取变化进行封装,隔离变化。(变化是会蔓延的,所以要把变化封装起来,架构设计主要目的就是区分稳定(或者相对稳定)的部分和变化的部分 -- Architect for change)
什么是接口:
接口就是组件对外的契约,对组件的理解就是看它的接口了,接口说明了组件能做什么,因此接口应该稳定或者相对稳定(相对于内部逻辑而言),如果一个组件的接口经常变化,那么组件的设计就存在问题。
抽象类似于接口( abstract class和interface区别以后再说,或者大家Google一下),将具体功能抽象化,保持对外界的稳定性。
前面也顺便说到了封装变化点,将变化点隔离出来,保持组件对外的稳定性。那么利用这两个架构设计的原则怎么修改我们的例子呢?
从ConsoleMessageProcessor和PrinterMessageProcessor的代码可以看出来,他们都只有一个共有方法,并且main对他们的调用也只涉及到这个共有的方法。这个就是我们要找的接口了。
public interface IMessageProcessor
{
void Process(string message);
}
我们顺便把消息生成的组件也接口化算了。(设计的极端做法就是组件之间的调用只容许使用接口调用。)
public interface IMessageCreator
{
String Create();
}
接下来的问题就是怎么实施针对接口编程了?其实很简单,就是让main组件与底层的组件的关系接口化/或者抽象化,底层组件对main只暴露接口,main中断对底层组件具体类型的调用,这样main与底层组件的耦合关系被改成main与接口的耦合关系,接口又相对稳定,那我们的目的就达到了。看一下修改后的组件关系图。
2. 基于接口/抽象的编程
依赖简单的来说就是一个组件和对象与另一个组件或对象有直接调用关系。现在来看组件图,Main对接口组件依赖,底层的消息处理和生成组件也对接口组件依赖(main对底层具体组件的依赖被反转了). 这样做的好处简单来说就是main里面的代码稳定下来,新增加消息处理组件无需改动(或很少改动)main的代码。
在后面的系列中我们研究ObjectBuilder的源代码会发现它很好的遵循这个原则。
对于上面的例子我们具体的代码就修改为:
public interface IMessageProcessor
{
void Process(string message);
}
public interface IMessageCreator
{
String Create();
}
public class ConsoleMessageProcessor:IMessageProcessor
{
public void Process(string message)
{
Console.WriteLine(message + " processed by ConsoleMessageProcessor.");
}
}
public class PrinterMessageProcessor : IMessageProcessor
{
public void Process(string message)
{
Printer.Print(message);
}
}
public class ConsoleMessageCreator : IMessageCreator
{
public string Create()
{
return Console.ReadLine();
}
}
再看修改后的main的代码为:
static void Main(string[] args)
{
Console.WriteLine("Demo DIP ");
IMessageCreator messageCreator = null;
IMessageProcessor messageProcessor = null;
//messageCreator=...
//messageProcessor=..
string s = messageCreator.Create();
messageProcessor.Process(s);
Console.Read();
}
哎,问题又来了,main的代码要稳定下来还缺一个重要的步骤,messageCreator ,messageProcessor 没有实例化,谁会给我这个实例啊?希望God能帮我这件事。干脆代码改为:
static void Main(string[] args)
{
Console.WriteLine("Demo DIP ");
IMessageCreator messageCreator = null;
IMessageProcessor messageProcessor = null;
messageCreator = God.MessageCreator;
messageProcessor = God.MessageProcessor;
string s = messageCreator.Create();
messageProcessor.Process(s);
Console.Read();
}
现在main代码现在的变化点已经转移的God对象上面了,很符合我们刚才所说的:
有了变化就先隔离然后封装它。
我们剩下要做的就是实现自己的God对象,于是Factory模式粉墨登场了!