目录
一,引子
1.1 传统的程序架构
1.2 依赖倒置
1.3 依赖倒置的作用
二,依赖注入
一,引子
1.1 传统的程序架构
在程序执行过程中,传统的程序架构如图:
可以看到,在传统的三层架构中,层与层之间是相互依赖的,UI层依赖于BLL层,BLL层依赖于DAL层。分层的目的是为了实现“高内聚、低耦合”。传统的三层架构只有高内聚没有低耦合,层与层之间是一种强依赖的关系,这也是传统三层架构的一种缺点。这种自上而下的依赖关系会导致级联修改,如果低层发生变化,可能上面所有的层都需要去修改,而且这种传统的三层架构也很难实现团队的协同开发,因为上层功能取决于下层功能的实现,下面功能如果没有开发完成,则上层功能也无法进行。
1.2 依赖倒置
依赖倒置(DIP):Dependence Inversion Principle的缩写,是一种软件架构设计的原则(抽象概念)。依赖于抽象不依赖于细节。主要有两层含义:
- 高层次的模块不应该依赖低层次的模块,两者都应该依赖其抽象。
- 抽象不应该依赖于具体,具体应该依赖于抽象。
如何理解这两句话?
- 第一句话:模块之间的依赖是通过抽象发生的,实现类之间不应该发生直接的依赖关系,他们的依赖关系应该通过接口或者抽象类产生。
- 第二句话:举个例子,假如我们要写BLL层的代码,直接就去实现了功能,等到开发完成以后发现没有使用依赖倒置原则,这时候在根据实现类去写接口,这种是不对的,应该首先设计抽象,然后在根据抽象去实现,应该要面向接口编程。
根据依赖倒置原则重新设计代码执行架构:
UI、BLL、DAL三层之间应该没有直接的依赖关系,都应该依赖于接口。首先应该先确定出接口,DAL层抽象出IDAL接口,BLL层抽象出IBLL接口,这样UI层依赖于IBLL接口,BLL实现IBLL接口。BLL层依赖于IDAL接口,DAL实现IDAL接口。如下图所示:
1.3 依赖倒置的作用
有了依赖倒置原则,可以使我们的架构更加的稳定、灵活,也能更好地应对需求的变化。相对于细节的多变性,抽象的东西是稳定的。所以以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定的多。
在传统的三层架构里面,仅仅增加一个接口层,我们就实现了依赖倒置,目的就是降低层与层之间的耦合。有了这样的接口层,三层架构才真正实现了“高内聚、低耦合”的思想。
二,依赖注入
依赖倒置原则是架构层面上的,那么如何在代码层面上实现呢?这就需要用到控制反转IOC。
传统开发,上端依赖(调用/指定)下端对象,会有依赖,把对下端对象的依赖转移到第三方容器(工厂+配置文件+反射),能够程序拥有更好的扩展性,是DIP的具体实现方式,可以用来减低计算机代码之间的耦合度。
而依赖注入DI 是控制反转的一种实现方式。
- 是实现IOC的手段和方法,就是能做到构造某个对象时,将依赖的对象自动初始化并注入 :
- 有三种注入方式:构造函数注入--属性注入--方法注入(按时间顺序):
- 构造函数注入用的最多,默认找参数最多的构造函数,可以不用特性,可以去掉对容器的依赖
下面通过一个例子,对上述专业名词进行解释:
父亲给孩子讲故事,只要给这个父亲一本书,他就可以照着这本书给孩子讲故事。
- 我们下面先用最传统的方式实现一下,这里不使用任何的设计原则和设计模式。
首先定义一个Book类:
public class Book
{
public string GetContent()
{
return "门前大桥下,游过一群鸭";
}
}
然后定义父亲Father类:
public class Father
{
public void read()
{
Book book = new Book();
Console.WriteLine("爸爸开始给孩子讲故事了:");
Console.WriteLine(book.GetContent());
}
}
主程序调用:
static void Main(string[] args)
{
Father father = new Father();
father.read();
Console.ReadKey();
}
//打印效果:
爸爸开始给孩子讲故事了:
门前大桥下,游过一群鸭
我们来看看关系图:
可以看到:Father是直接依赖于Book类。
这时需求发生了变化,不给爸爸书了,给爸爸报纸,让爸爸照着报纸给孩子读报纸,这时该怎么做呢?按照传统的方式,我们这时候需要再定义一个报纸类,还要修改Father类。
但是需求在不断的变化,不管怎么变化,对于爸爸来说,他一直在读读物,但是具体读什么读物是会发生变化,这就是细节,也就是说细节会发生变化。但是抽象是不会变的。如果这时候还是使用传统的OOP思想来解决问题,那么会导致程序不断的在修改。
下面使用工厂模式来优化:
首先创建一个接口:
public interface IReader
{
string GetContent();
}
然后让Book类和NewsPaper类都继承自IReader接口
public class Book:IReader
{
public string GetContent()
{
return "门前大桥下,游过一群鸭";
}
}
public class NewsPaper : IReader
{
public string GetContent()
{
return "7月3日是地球上有记录以来最热的一天";
}
}
之后创建一个工厂类:
public static class ReaderFactory
{
public static IReader GetReader(string readerType)
{
if (string.IsNullOrEmpty(readerType))
{
return null;
}
switch(readerType)
{
case "NewsPaper":
return new NewsPaper();
case "Book":
return new Book();
default:
return null;
}
}
}
里面方法的返回值是一个接口类型。最后在Father类里面调用工厂类:
public class Father
{
private IReader reader { get; set; }
public Father(string readerName)
{
reader = ReaderFactory.GetReader(readerName);
}
public void read()
{
Console.WriteLine("爸爸开始给孩子讲故事了:");
Console.WriteLine(reader.GetContent());
}
}
最后在主程序调用:
static void Main(string[] args)
{
Father father = new Father("NewsPaper");
father.read();
Console.ReadKey();
}
//打印效果:
爸爸开始给孩子讲故事了:
7月3日是地球上有记录以来最热的一天
此时的依赖关系图:
这时Father已经和Book、Paper没有任何依赖了,Father依赖于IReader接口,还依赖于工厂类,而工厂类又依赖于Book和Paper类。这里实际上已经实现了控制反转。Father(高层)不依赖于低层(Book、Paper)而是依赖于抽象(IReader),而且具体的实现也不是由高层来创建,而是由第三方来创建(这里是工厂类)。但是这里只是使用工厂模式来模拟控制反转,而没有实现依赖的注入,依赖还是需要向工厂去请求。
再进一步优化代码
由于此时没有了工厂,我们还是需要在主程序调用中实例化具体的实现类。