在阅读内容之前,我们需要掌握基础的C#编程知识和面向对象编程特点,听说过设计原理和设计模式。
内容整理部分来源于链接网站
https://www.tutorialsteacher.com/www.tutorialsteacher.com我的个人名言:坚持学习养成习惯往往比获得了什么样的学习成果更重要。
直接开始吧。
我们先来看几个概念性质的名词
控制反转:Inversion of Control (IoC)
依赖反转原理:Dependency Inversion Principle (DIP)
依赖注入:Dependency Injection (DI)
控制反转容器:IoC containers
上面这几个名词有点绕来绕去的对不对,我们给它归纳一下类别。如图:
简单来讲,就是两个原理、一个模式和一个框架。反转(Inversion)是原理(Principle)也可以当作目的,方式/模式(Pattern)是注入(Injection),用第三方框架(Framework)可以更加节省时间。
详细介绍,我们一个一个来看理论描述。
控制反转(Inversion of Control):IoC是一种设计原理,建议在面向对象的设计中反转各种控制,以实现应用程序类之间的松散耦合。 在这种情况下,控制是指类具有的除其主要职责以外的任何其他职责,例如对应用程序流程的控制或对从属对象的创建和绑定的控制(请记住SRP-单一职责原则)。
依赖反转原理(Dependency Inversion Principle):DIP原理建议高级模块不应依赖于低级模块,两者都应依赖抽象,这样有助于实现类之间的松散耦合。强烈建议同时使用DIP和IoC,以实现更佳的松散耦合。
依赖注入(Dependency Injection):依赖注入(DI)是一种设计模式,它实现了IoC原理来反转依赖对象的创建。
控制反转容器(IoC Container):IoC容器是一个用于管理整个应用程序中的自动依赖项注入的框架,因此,作为程序员,我们无需在其中投入更多时间和精力。 .NET有各种IoC容器,例如Unity,Ninject,StructureMap,Autofac等。
值得注意的一点是我们不能单独使用IoC来实现松散耦合的类。 除了IoC,我们还需要使用DIP,DI和IoC容器。 基本上如下图步骤:
那么我们详细来解读一下
首先我们稍微思考一下什么叫依赖(Dependent),Talk is cheap,let's go ahead and see code:
public class A
{
B b;
public A()
{
b = new B();
}
public void Task1() {
// do something here..
b.SomeMethod();
// do something here..
}
}
public class B {
public void SomeMethod() {
//doing something..
}
}
简单来说,上述class A 依赖着 class B,就好像小时候你爸妈不给零花钱,你就啥也买不了一样....狗头...有没有办法解决一下?有啊,你有个损友给你出个招,这么着:
public class A
{
B b;
public A()
{
b = Factory.GetObjectOfB ();
}
public void Task1() {
// do something here..
b.SomeMethod();
// do something here..
}
}
public class Factory
{
public static B GetObjectOfB()
{
return new B();
}
}
给你们翻译下:去警察局→Factory找警察叔叔告状,说不给饭吃虐待幼童,警察叔叔很生气,打电话叫来了你爸妈,你爸妈左手攥着拳头右手把钱包掏了出来,不管结果怎么样,你反正先有零花钱了对不....再次狗头....
当然还有很多其他解决办法,大概是下图这些,就不一一展开了:
玩笑。
那么我们进入主题,先建立一个常见的业务层和数据层紧耦合代码片段:
public class CustomerBusinessLogic
{
DataAccess _dataAccess;
public CustomerBusinessLogic()
{
_dataAccess = new DataAccess();
}
public string GetCustomerName(int id)
{
return _dataAccess.GetCustomerName(id);
}
}
public class DataAccess
{
public DataAccess()
{
}
public string GetCustomerName(int id) {
return "Dummy Customer Name"; // get it from DB in real app
}
}
第一步,我们遵循IoC原理给上述代码做出一些控制反转方向的改动:
public class CustomerBusinessLogic
{
public CustomerBusinessLogic()
{
}
public string GetCustomerName(int id)
{
DataAccess _dataAccess = DataAccessFactory.GetDataAccessObj();
return _dataAccess.GetCustomerName(id);
}
}
public class DataAccessFactory
{
public static DataAccess GetDataAccessObj()
{
return new DataAccess();
}
}
public class DataAccess
{
public DataAccess()
{
}
public string GetCustomerName(int id) {
return "Dummy Customer Name"; // get it from DB in real app
}
}
可以看到,CustomerBusinessLogic类使用DataAccessFactory.GetCustomerDataAccessObj()方法来获取DataAccess类的对象,而不是使用new关键字创建它。 因此,我们已经将创建依赖类对象的控制从CustomerBusinessLogic类转换为DataAccessFactory类。
第二步,我们遵循DIP原理给上述代码做出一些依赖反转方向的改动:
动手前先得稍微回顾一下抽象(Abstraction),它和封装(Encapsulation)同样是面向对象编程的一大特点。如果对于构造函数、抽象类(abstrac class/interface)、重载、继承这些知识点有所欠缺,你可能需要稍微去补补课再接着往下看。
顺带一提,抽象在abstract class和interface的使用上有一些区别,简单来讲就是抽象类说到底是一个完整的类,有完整的内部语法,所以可以在其中定义属性或者方法,用来做一些程序处理;接口只能作为类的头部来使用,有点类似于html页面的头和身体的关系,接口中可以定义属性名称和方法名称,在实际使用中,接口的身体部分会由继承类来提供,有时候会有类继承多个接口就有点哪吒三头六臂的味道了,长得丑但是能干啊...狗头...(由于类能继承类的数量只有一个,具体为什么我还没有去深究,各位可以扩展)。
继续看代码
public interface ICustomerDataAccess
{
string GetCustomerName(int id);
}
public class CustomerDataAccess: ICustomerDataAccess
{
public CustomerDataAccess() {
}
public string GetCustomerName(int id) {
return "Dummy Customer Name";
}
}
public class DataAccessFactory
{
public static ICustomerDataAccess GetCustomerDataAccessObj()
{
return new CustomerDataAccess();
}
}
public class CustomerBusinessLogic
{
ICustomerDataAccess _custDataAccess;
public CustomerBusinessLogic()
{
_custDataAccess = DataAccessFactory.GetCustomerDataAccessObj();
}
public string GetCustomerName(int id)
{
return _custDataAccess.GetCustomerName(id);
}
}
更改点:
- 将DataAccess类换成了CustomerDataAccess类并且添加了同名接口构造了同名方法,以这种方式实现了对同名接口中方法的重载.
- 将factory的输出内容从
DataAccess
类替换成了ICustomerDataAccess
,由于同名接口内的方法已经被重载,返回的数据结构当然也是同名类. - 将
CustomerBusinessLogic
中引用 DataAccess类的部分替换成 ICustomerDataAccess.
上述更改的优点基于interface本身的特点(一个类可以实现多个接口),在维护或者扩展方法时有着极大的好处(以增减接口的方式可以做到既不变更固有类也能扩展新的内容)。
第三步,在完成了IoC和DIP的处理后,但是还有个问题,我们在CustomerBusinessLogic类中使用了DataAccessFactory,假设还有ICustomerDataAccess的另一种实现,并且我们想在CustomerBusinessLogic中使用该新类,我们还需要更改CustomerBusinessLogic类的源代码。我们接下来使用DI的方式来变更一下上一步代码。
进入更新代码的步骤之前,先了解一下什么时依赖注入(Dependency Injection),它是一种用来实现IoC的设计模式,它允许在类外部创建依赖对象,并通过不同方式将这些对象提供给类。 使用DI,我们将依赖对象的创建和绑定移到依赖它们的类之外。
依赖注入模式涉及3种类型的类。
- client class:客户端类(dependent class),依赖于其他通用类做功能处理.
- service class:服务类(dependency),专门处理某些事务的类.
- injector class:注入者类,专门将服务类注入到客户端类的中介所.
关系图:
简单描述一下,注入器类创建服务类的对象,并将该对象注入客户端对象。 这样,DI模式将创建服务类的对象的职责与客户端类分开,就好比正在写作业的你想在干净的家里吃顿可口的饭,家里乱糟糟的你碗还没洗,你(客户)用手机(注入器)叫了家政和外卖(服务),这样也不耽搁你继续写作业的时间。
了解了依赖注入的内容后还需要了解实现依赖注入的三种方式。
- constructor injection:结构注入,顾名思义将整个数据结构注入到客户端.
- Property Injection:属性注入,常被称作Setter Injection,通过满足客户端的公开属性注入服务到客户端.
- Method Injection:方法注入,客户端实现自己的接口并且在其中定义准备使用的服务类中同名方法,注射器会通过重载满足这些方法的需要,从而注入服务到客户端.
我们用上述三种方式来分别实现依赖注入:
constructor injection
public class CustomerBusinessLogic
{
ICustomerDataAccess _dataAccess;
public CustomerBusinessLogic(ICustomerDataAccess custDataAccess)
{
_dataAccess = custDataAccess;
}
public CustomerBusinessLogic()
{
_dataAccess = new CustomerDataAccess();
}
public string ProcessCustomerData(int id)
{
return _dataAccess.GetCustomerName(id);
}
}
public interface ICustomerDataAccess
{
string GetCustomerData(int id);
}
public class CustomerDataAccess: ICustomerDataAccess
{
public CustomerDataAccess()
{
}
public string GetCustomerName(int id)
{
//get the customer name from the db in real application
return "Dummy Customer Name";
}
}
变化点:
a、移除了factory class.
b、更新CustomerBusinessLogic类,使其包含具有一个参数类型为ICustomerDataAccess的构造函数。 现在,调用类必须注入ICustomerDataAccess对象。
注入方式:
public class CustomerService
{
CustomerBusinessLogic _customerBL;
public CustomerService()
{
_customerBL = new CustomerBusinessLogic(new CustomerDataAccess());
}
public string GetCustomerName(int id) {
return _customerBL.GetCustomerName(id);
}
}
CustomerService类创建并将CustomerDataAccess对象注入到CustomerBusinessLogic类中,之后重载了CustomerDataAccess下面的方法。 因此,CustomerBusinessLogic类不需要使用new关键字或使用factory类创建CustomerDataAccess的对象。 调用类(CustomerService)创建适当的DataAccess类并将其设置为CustomerBusinessLogic类。 这样,CustomerBusinessLogic类和CustomerDataAccess类彻底实现了松散耦合的目的。
property injection
public class CustomerBusinessLogic
{
public CustomerBusinessLogic()
{
}
public string GetCustomerName(int id)
{
return DataAccess.GetCustomerName(id);
}
public ICustomerDataAccess DataAccess { get; set; }
}
public class CustomerService
{
CustomerBusinessLogic _customerBL;
public CustomerService()
{
_customerBL = new CustomerBusinessLogic();
_customerBL.DataAccess = new CustomerDataAccess();
}
public string GetCustomerName(int id) {
return _customerBL.GetCustomerName(id);
}
}
CustomerBusinessLogic类包含名为DataAccess的公共属性,我们可以在其中设置实现ICustomerDataAccess的类的实例。 因此,CustomerService类使用此公共属性创建并设置CustomerDataAccess类。
method injection
在方法注入中,依赖关系是通过方法提供的。 此方法可以是类方法或接口方法。
我们这里使用基于接口的方法进行方法注入。
interface IDataAccessDependency
{
void SetDependency(ICustomerDataAccess customerDataAccess);
}
public class CustomerBusinessLogic : IDataAccessDependency
{
ICustomerDataAccess _dataAccess;
public CustomerBusinessLogic()
{
}
public string GetCustomerName(int id)
{
return _dataAccess.GetCustomerName(id);
}
public void SetDependency(ICustomerDataAccess customerDataAccess)
{
_dataAccess = customerDataAccess;
}
}
public class CustomerService
{
CustomerBusinessLogic _customerBL;
public CustomerService()
{
_customerBL = new CustomerBusinessLogic();
((IDataAccessDependency)_customerBL).SetDependency(new CustomerDataAccess());
}
public string GetCustomerName(int id) {
return _customerBL.GetCustomerName(id);
}
}
在上面的代码中,CustomerBusinessLogic类实现了IDataAccessDependency接口,其中包括SetDependency()方法。 因此,注入器类CustomerService现在将使用此方法将依赖类(CustomerDataAccess)注入到客户端类。
至此,控制反转的用法基本上就说得差不多了。在理解了内容之后,我们就可以畅快的使用第三方插件了,上文提到过控制反转容器,有兴趣可以看看,使用挺方便的。
今天就到这里了,写技术文章实在费劲,致敬各行各业奋斗在一线的战士们!!!