谈谈对IOC及DI的理解与思考
一、前言
在实际的开发过程中,我们经常会遇到这样的情况,在进行调试分析问题的时候,经常需要记录日志信息,这时可以采用输出到控制台。
因此,我们通常会定义一个日志类,来实现输出日志。
定义一个生成验证的逻辑处理方法,
public class Logger
{
public void AddLogger()
{
Console.WriteLine("日志新增成功!");
}
}
然后在控制台中输出结果。
static void Main(string[] args)
{
Logger logger = new Logger();
logger.AddLogger();
Console.Read();
}
看着实现的结果,我们以为完成任务了,当其实这才是刚刚开始。
二、开始
相信大家在开发中,都会遇到这种情况,有时需要控制台输出,但也有可能要你输出到文本,数据库或者远程服务器等等,这些都是有可能。因此最初采用直接输出到控制台已经不能满足条件了,所以我们需要将上述代码进行重写改造,实现不同的输出方式。
2.1 第一种方式
2.1.1 控制台方式
用到控制台方式输出的时候:
/// <summary>
/// 控制台输出
/// </summary>
public class ConsoleLogger
{
public void AddLogger()
{
Console.WriteLine("控制台输出:日志新增成功!");
}
}
定义一个获取输出日志的处理逻辑类,因此我们需要定义ConsoleLogger
类并初始化
/// <summary>
/// 定义一个输出日志的统一类
/// </summary>
public class LoggerServer
{
private readonly ConsoleLogger consoleLogger = new ConsoleLogger();//添加一个私有变量的对象 个私有变量的数字对象
public void AddLogger()
{
consoleLogger.AddLogger();
}
}
控制台输出结果:
static void Main(string[] args)
{
LoggerServer loggerServer = new LoggerServer();
loggerServer.AddLogger();
Console.Read();
}
控制台输出:日志新增成功!
2.1.2 文本输出
当用到文本输出日志的时候,我们再次定义一个生成文本日志的方式
/// <summary>
/// 文本输出
/// </summary>
public class FileLogger
{
public void AddLogger()
{
Console.WriteLine("文本输出:日志新增成功!");
}
}
然后再次定义一个获取验证的处理逻辑类,因此我们需要定义ImageVerification
类并初始化
/// <summary>
/// 定义一个输出日志的统一类
/// </summary>
public class LoggerServer
{
private readonly FileLogger fileLogger = new FileLogger();//添加一个私有变量的对象
public void AddLogger()
{
fileLogger.AddLogger();
}
}
最后输出结果:
文本输出:日志新增成功!
通过以上的方式,我们实现了不同方式输出不同日志方式,但是仔细观察可以发现,这种方式不是一种良好的软件设计方式。
所以可能有人会改成下面第二种方式,以接口来实现。
2.2 第二种方式
定义一个ILogger
接口并声明一个AddLogger
方法
public interface ILogger
{
void AddLogger();
}
在控制台输出方式中ConsoleLogger
类,实现ILogger
接口
/// <summary>
/// 控制台输出
/// </summary>
public class ConsoleLogger : ILogger
{
public void AddLogger()
{
Console.WriteLine("控制台输出:日志新增成功!");
}
}
在文本输出日志方式FileLogger
类中,实现ILogger
接口
/// <summary>
/// 文本输出
/// </summary>
public class FileLogger : ILogger
{
public void AddLogger()
{
Console.WriteLine("文本输出:日志新增成功!");
}
}
定义一个统一的输出日志类LoggerServer
类
/// <summary>
/// 定义一个统一的输出日志类
/// </summary>
public class LoggerServer
{
private readonly ConsoleLogger consoleLogger = new ConsoleLogger();//添加一个私有变量的对象
//private readonly FileLogger fileLogger = new FileLogger();//添加一个私有变量的对象
public void AddLogger()
{
_logger.AddLogger();
}
}
最后,控制台调用输出:
static void Main(string[] args)
{
LoggerServer loggerServer = new LoggerServer();
loggerServer.AddLogger();
Console.Read();
}
控制台输出:日志新增成功!
虽然第二种方式中,采用了接口来实现,降低耦合,但还是没有达到我们想要的效果,因此以上的两种方式,都不是很好的软件设计方式。
代码可拓展性比较差,以及组件之间存在高度耦合,违反了开放关闭原则,在设计的时候,也应当考虑对扩展开放,对修改关闭。
2.3 思考
既然要遵循开放关闭原则,那上面的写法,选择采用控制台日志输出方式所需要的ConsoleLogger
创建和依赖都是在统一的日志类LoggerServer
内部进行的,既然要使内部不直接存在绑定依赖,那有没有什么方式从外部传递的方式给LoggerServer
类内部引用使用呢?
三、引入
3.1 依赖注入
依赖注入 : 它提供一种机制,将需要依赖对象的引用传递给被依赖对象。
下面我们先看看具体的几种注入方式,再做小结说明。
3.1.1 构造函数注入
在LoggerServer
类中,定义一个私有变量_logger
, 然后通过构造函数的方式传递依赖
public class LoggerServer
{
private ILogger _logger; //1. 定义私有变量
//2.构造函数
public LoggerServer(ILogger logger)
{
//3.注入 ,传递依赖
this._logger = logger;
}
public void AddLogger()
{
_logger.AddLogger();
}
}
通过控制台程序调用,先在外部创建依赖对象,而后通过构造的方式注入依赖
static void Main(string[] args)
{
#region 构造函数注入
// 注入控制台输出方式
// 外部创建依赖的对象 -> ConsoleLogger
ConsoleLogger console = new ConsoleLogger();
// 通过构造函数注入 -> LoggerServer
LoggerServer loggerServer1 = new LoggerServer(console);
loggerServer1.AddLogger();
// 注入 文件输出方式
FileLogger file = new FileLogger();
// 通过构造函数注入 -> LoggerServer
LoggerServer loggerServer2 = new LoggerServer(file);
loggerServer2.AddLogger();
#endregion
Console.Read();
}
输出:
控制台输出:日志新增成功!
文本输出:日志新增成功!
显然的发现,通过这种构造函数注入的方式,在外部定义依赖,降低内部的耦合度,同时也增加了扩展性,只需从外部修改依赖,就可以实现不同的验证结果。
3.1.2 属性注入
即通过定义一个属性来传递依赖
/// <summary>
/// 定义一个输出日志的统一类
/// </summary>
public class LoggerServer
{
//1.定义一个属性,可接收外部赋值依赖
public ILogger _logger { get; set; }
public void AddLogger()
{
_logger.AddLogger();
}
}
通过控制台,定义不同的方式,通过不同依赖赋值,实现不同的验证结果:
static void Main(string[] args)
{
#region 属性注入
// 注入 控制台输出方式
//外部创建依赖的对象 -> ConsoleLogger
ConsoleLogger console = new ConsoleLogger();
LoggerServer loggerServer1 = new LoggerServer();
//给内部的属性赋值
loggerServer1._logger = console;
loggerServer1.AddLogger();
// 注入 文件输出方式
//外部创建依赖的对象 -> FileLogger
FileLogger file = new FileLogger();
LoggerServer loggerServer2 = new LoggerServer();
//给内部的属性赋值
loggerServer2._logger = file;
loggerServer2.AddLogger();
#endregion
Console.Read();
}
输出
控制台输出:日志新增成功!
文本输出:日志新增成功!
3.1.3 接口注入
先定义一个接口,包含一个设置依赖的方法。
public interface IDependent
{
void SetDepend(ILogger logger);//设置依赖项
}
这个与之前的注入方式不一样,而是通过在类中继承并实现这个接口。
public class LoggerServer : IDependent
{
private ILogger _logger;
// 继承接口,并实现依赖项方法,注入依赖
public void SetDepend(ILogger logger)
{
_logger = logger;
}
public void AddLogger()
{
_logger.AddLogger();
}
}
通过调用,直接通过依赖项方法,传递依赖
static void Main(string[] args)
{
#region 接口注入
// 注入 控制台输出方式
//外部创建依赖的对象 -> ConsoleLogger
ConsoleLogger console = new ConsoleLogger();
LoggerServer loggerServer1 = new LoggerServer();
//给内部赋值,通过接口的方式传递
loggerServer1.SetDepend(console);
loggerServer1.AddLogger();
//注入 文件输出方式
//外部创建依赖的对象 -> FileLogger
FileLogger file = new FileLogger();
LoggerServer loggerServer2 = new LoggerServer();
//给内部赋值,通过接口的方式传递
loggerServer2.SetDepend(file);
loggerServer2.AddLogger();
#endregion
Console.Read();
}
输出
控制台输出:日志新增成功!
文本输出:日志新增成功!
3.1.4 小结
依赖注入(DI—Dependency Injection)
它提供一种机制,将需要依赖对象的引用传递给被依赖对象通过DI,我们可以在LoggerServer
类在外部ConsoleLogger
对象的引用传递给LoggerServer
类对象。 注入某个对象所需要的外部资源(包括对象、资源、常量数据)
依赖注入把对象的创造交给外部去管理,很好的解决了代码紧耦合的问题,是一种让代码实现松耦合的机制。
松耦合让代码更具灵活性,能更好地应对需求变动,以及方便单元测试。
3.2 IOC
控制反转(Inversion of Control,缩写为IoC),在面向对象编程中,是一种软件设计模式,教我们如何设计出更优良,更具有松耦合的程序。
在上文的例子中,我们发现如果在获取对象的过程中靠类内部主动创建依赖对象,则会导致代码直接高度耦合并且期存在难以维护这种隐患,所以为了避免这种问题,我们采用了由外部提供依赖对象,内部对象类被创建的时候,将其所依赖的对象引用传递给它,实现了依赖被注入到对象中去。
通俗的说明:
在类A中用到了类B的对象时候,一般情况下,需要在A的代码中显式的new一个B的对象。这种方式都是通过我们自己主动创建出来的,创建合作对象的主动权在自己手上,自己需要哪个对象,就主动去创建,创建对象的主动权和创建时机是由自己把控的,而这样就会使得对象间的耦合度高了,A对象需要使用对象B来共同完成一件事,A要使用B,那么A就对B产生了依赖,也就是A和B之间存在一种耦合关系,并且是紧密耦合在一起。
public class A { private B b = new B();//主动的new一个B的对象。主动创建出来 public void Get() { B.Create(); } }
采用依赖注入技术之后,A的代码只需要定义一个私有的B对象,不需要直接new来获得这个对象,而是通过相关的容器控制程序来将B对象在外部new出来并注入到A类里的引用中。现在创建对象而是有第三方控制创建,你要什么对象,它就给你什么对象,依赖关系就变了,原先的依赖关系就没了,A和B之间耦合度也就减少了。
public class A { private B b;//外部new出来, 注入到引用中 public void Get() { B.Create(); } }
3.3 关系
控制反转(IoC) 是一种软件设计的模式,指导我们设计出更优良,更具有松耦合的程序,
而具体的实现方式有依赖注入和依赖查找。
在这一篇主要说的是常用的依赖注入方式。
你在实际开发中,可能还会听到另一名词叫 IoC容器,这其实是一个依赖注入的框架,
用来映射依赖,管理对象创建和生存周期。 (在后续篇章会具体说明)
四、思考
说到依赖,就想到依赖注入和工厂模式这两者的区别?
这是网上有一个对比例子:
工厂设计模式 | 依赖注入 | |
---|---|---|
对象创建 | 它用于创建对象。我们有单独的Factory类,其中包含创建逻辑。 | 它负责创建和注入对象。 |
对象的状态 | 它负责创建有状态对象。 | 负责创建无状态对象 |
运行时/编译时间 | 在编译时创建对象 | 在运行时配置对象 |
代码变更 | 如果业务需求发生变化,则可能会更改对象创建逻辑。 | 无需更改代码 |
机制 | 类依赖于工厂方法,而工厂方法又依赖于具体类 | 父对象和所有从属对象可以在单个位置创建 |
好啦,这篇文章就先讲述到这里吧,在后续篇章中会对常用的IOC容器进行使用说明,希望对大家有所帮助。
如果有不对的或不理解的地方,希望大家可以多多指正,提出问题,一起讨论,不断学习,共同进步。🤣