在学习这部分知识之前,我先提几个问题:
1. 控制反转(IOC:Inversion of Control),难道是A类调用B类,改写成B类调用A类么?不合适啊
2. 依赖注入(DI:Dependency Injection),我觉得只要看到一个函数的参数中的类型有接口或抽象类类型的,那么它一定用到了依赖注入
3. 控制反转和依赖注入是什么关系?怎么总把它俩放在一起讨论?
4. 容器和他们是什么关系?
先说说什么是依赖倒置?
依赖倒置是Robert Martin大师提出的面向对象涉及原则,简单说来只有两条:
1. 上层模块不应该依赖于下层模块,他们共同依赖于一个抽象
2. 抽象不能依赖于具象,具象依赖于抽象
理解:上层为使用者,下层为被使用者;这就会导致上层模块依赖于下层木块,当下层模块做改动,避免不了的会影响到上层模块,从而导致系统的不稳定性。相对于具象(这里理解为具体实现),抽象更加稳定(这里理解为不轻易改动)。
再说说控制反转
控制反转并不是将控制器倒置,而是将控制权转移,所以应该叫控制转移更贴切一些
再说说控制反转和依赖注入之间的关系
控制反转只是一个思想,一个概念,而依赖注入是控制反转的一种实现,当然还有依赖查询,这也是一种实现,但我们今天不讨论。
依赖注入是转移了对象的创建权。
容器的概念
如果依赖注入可以算作容器的一部分功能,容器再运行期间通过依赖注入的方式,动态的将某种依赖关系注入到对象中。
我们通过【动物们学唱歌】的程序来解释一下以上概念:
首先是只有一只猫,它要在森林中唱歌,代码怎么写?要实现三个类对吧,Cat、Forest、MainApp主函数类
public class Cat
{
public void Sing(Forest forest)
{
Console.WriteLine("I'm a Cat,I like Singing in the "+ forest.Space);
}
}
public class Forest
{
public string Space { get { return "Forest"; } }
}
public class MainClass
{
public static void RunApp()
{
Cat cat = new Cat();
Forest forest = new Forest();
cat.Sing(forest);
}
}
public class Cat
{
public void Sing(Forest forest)
{
Console.WriteLine("I'm a Cat,I like Singing in the "+ forest.Space);
}
}
public class Forest
{
public string Space { get { return "Forest"; } }
}
public class MainClass
{
public static void RunApp()
{
Cat cat = new Cat();
Forest forest = new Forest();
cat.Sing(forest);
}
}
MainClass.RunApp();
运行结果:
以上三个类的依赖关系图为:
如果小狗看见了,它想在草地上唱歌怎么办?我们是不是应该要增加俩个类 Dog、Lawn
public class Cat
{
public void Sing(Forest forest)
{
Console.WriteLine("I'm a Cat,I like Singing in the "+ forest.Space);
}
}
public class Dog
{
public void Sing(Lawn lawn)
{
Console.WriteLine("I'm a Dog,I like Singing in the " + lawn.Space);
}
}
public class Forest
{
public string Space { get { return "Forest"; } }
}
public class Lawn
{
public string Space { get { return "Lawn"; } }
}
public class MainClass
{
public static void RunApp()
{
Cat cat = new Cat();
Forest forest = new Forest();
cat.Sing(forest);
Dog dog = new Dog();
Lawn lawn = new Lawn();
cat.Sing(forest);
}
}
运行结果
这几个类的依赖关系图:
现在问题来了,那如果我猫又想在草坪上唱歌了,狗也想去森林里唱歌了,有没有发现你的代码要改动很大?根据依赖倒置的说法,上层模块不应该依赖于下层木块,而是
共同依赖于一个抽象。抽象也不能依赖具象,而是具象依赖抽象。猫和狗的作用在这里都是唱歌,那么抽象出一个接口ISing,把森林和草坪抽象为场景(Scene),好,说改就改,改好的代码如下:
public interface ISing
{
void Sing(Scene scene);
}
public class Cat: ISing
{
public void Sing(Scene scene)
{
Console.WriteLine("I'm a Cat,I like Singing in the "+ scene.Space);
}
}
public class Dog: ISing
{
public void Sing(Scene scene)
{
Console.WriteLine("I'm a Dog,I like Singing in the " + scene.Space);
}
}
public abstract class Scene
{
public string Space { get; set;}
}
public class Forest: Scene
{
public Forest()
{
base.Space = "Forest";
}
}
public class Lawn: Scene
{
public Lawn()
{
base.Space = "Lawn";
}
}
public class MainClass
{
public static void RunApp()
{
Scene scene = new Forest();
ISing sing = new Dog();
sing.Sing(scene);
scene = new Lawn();
sing = new Cat();
sing.Sing(scene);
}
}
类依赖关系图如下:
我们目的是减少依赖,但看上去依赖更加复杂了,依赖还依赖接口以及抽象类。接口和抽象类是稳定的,我们可以不考虑,除了MainApp类对其他类的依赖外,其他的依赖都符合依赖倒置原则。那我们现在来解耦 MainApp类,修改代码如下:
public class MainClass
{
Scene scene; ISing sing;
public MainClass(Scene scene, ISing sing)
{
this.scene = scene;
this.sing = sing;
}
public void RunApp()
{
sing.Sing(scene);
}
}
MainClass main = new MainClass(new Lawn(),new Cat());
main.RunApp();
类依赖关系简化为:
这便是通过依赖注入中的构造函数注入来解耦的。我们还可以利用反射继续解耦,更换不同的动物,不同的场景而不用修改代码
Assembly assembly = Assembly.Load(ConfigurationManager.AppSettings["AssemName"]);
ISing sing = (ISing)assembly.CreateInstance(ConfigurationManager.AppSettings["AssemName"] + "." + ConfigurationManager.AppSettings["Animal"]);
Scene scene = (Scene)assembly.CreateInstance(ConfigurationManager.AppSettings["AssemName"] + "." + ConfigurationManager.AppSettings["Scene"]);
MainClass main = new MainClass(new Lawn(),new Cat());
main.RunApp();
<appSettings>
<add key="AssemName" value="ConsoleTest"/>
<add key="Animal" value="Dog"/>
<add key="Scene" value="Forest"/>
</appSettings>
运行结果:
这便是容器的部分功能的实现方式。
真正的大师永远怀着一颗学徒的心。