【设计模式——依赖注入】

为什么使用依赖注入

单例模式的缺点:
使用单例模式失去了利用多态替换实现的可能性。
单例模式的另一个缺点是,如果由于新的需求或需求变化而不得不更改,那么这种更改可能会触发所有依赖类的一系列更改。
在分布式系统中,很难保证一个类拥有唯一实例,这是体现在软件体系结构中一个常见的情况。想象一下微服务模式,一个复杂的软件系统是由许多小的、独立的和分布式的过程组成的。在这样的环境中,单例对象很难保证单实例化,并且由他们导致的紧密耦合也存在问题。
替代单例模式的办法:
只创建一个实例,并且在需要的地方注入它。

依赖注入

从根本上来讲,依赖注入(Dependency Injection,DI)是一钟技术,在这种技术中,客户端对象需要独立的服务对象是由外部提供的。客户端对象不需要关系它所需要的服务对象本身,或者主动请求服务对象,例如,从工厂或者服务定位器中请求。

含义

依赖注入的含义可以表示如下:
将组件与其需要的服务分离,这样组件就不必知道这些服务的名称,也不必知道如何获取它们。
以日志记录器为例,例如一个服务类,它提供了写日志的功能。这样的日志记录器常常被实现为单例。因此,使用日志记录器的每个客户端都依赖于日志的全局对象。

Accounting
CustomerRespository
ShoppingCart
Logger
-Instance:Logger
+getInstance()
+writeInfoEntry(entry:String)
+writeWarnEntry(entry:String)
+writeErrorEntry(entry:String)
-Logger()

web 商店的三个领域类依赖于 Logger 的单例
这就是 Logger 单例类在源代码中的样子:

实现Logger单例模式

#include<string_view>
#include
#include
using string_view = std::string;
class Logger final
{
public:
static Logger& getInstance()
{
static Logger theLogger{};
return theLogger;

}

void writeInfoEntry(string_view entry)
{
	//
}
void writeInfoEntry(string_view entry)
{
	//
}
void writeInfoEntry(string_view entry)
{
	//
}

};

依赖注入模式

为了摆脱单例对象,并且能够在单元测试期间用一个测试替身替换Logger对象,我们必须使用依赖倒置原则(DIP),这意味着我们必须首先引入一个抽象类(一个接口,并使CustomerRepository和具体等的Logger都依赖于该接口。

Logger
1
CustomerRepository
+findCustomerById(id:identifier)
«interface»
LoggerFacility
+writeInfoEntry(entry:String)
+writeWarnEntry(entry:String)
+writeErrorEntry(entry:String)
StandardOutputLogger
+writeInfoEntry(entry:String)
+writeWarnEntry(entry:String)
+writeErrorEntry(entry:String)

在代码中引入新的接口LoggingFacility:
StandardOutputLogger类是实现了LoggingFacility接口的一个例子,这个类把日志写到标准输出上,正如它的名字一样:

接下来,修改CustomerRepository类。
首先,创建一个新的Logger类型的只能指针的成员变量,**该指针实例通过一个初始化构造函数传递到这个类中。**换句话说,允许在创建期间把实现LoggingFacility接口类的实例注入CustomerRepository对象中。**我们删除了默认构造函数,因为我们不希望在没有Logger的情况下创建CustomerRepository实例。**此外,我们删除了实现中对单例对象的直接依赖,并且用Logger智能指针来写日志。
#pragma once
#include"Identifier.h"
#include"Customer.h"
#include
#include"LoggingFacility.h"
class CustomerRepository
{
public:
//std::map<identifier, Customer> customerMap;
//Customer findCustomerById(const identifier& customerId)
//{
// Logger::getInstance().writeInfoEntry(“Starting to search for a customer by a given unique identifier…”);
// //…
//}

//修改后的CustomerRepository类
CustomerRepository() = delete;
explicit CustomerRepository(const Logger& loggingService) :logger{ loggingService } {}

Customer findCustomerById(const Identifier& customerId)
{
	logger->writeInfoEntry("Starting to search for a customer by a given unique identifier...");
	//...
}

private:
// …
Logger logger; //
};

#pragma once
//引入接口LoggingFacility
#include
#include

using string_view = std::string;

class LoggingFacility
{
public:
virtual ~LoggingFacility() = default;
virtual void writeInfoEntry(string_view entry) = 0;
virtual void writeWarnEntry(string_view entry) = 0;
virtual void writeErrorEntry(string_view entry) = 0;
};
using Logger = std::shared_ptr;

#pragma once
#include
#include"LoggingFacility.h"
//StandardOutputLogger类是LoggingFacility接口的一个实现类。
class StandardOutputLogger :public LoggingFacility
{
public:
virtual void writeInfoEntry(string_view entry) override
{
std::cout << "[Info] " << entry << std::endl;
}
virtual void writeWarnEntry(string_view entry) override
{
std::cout << "[Warning] " << entry << std::endl;
}
virtual void writeErrorEntry(string_view entry) override
{
std::cout << "[Error] " << entry << std::endl;
}
};

作为重构的结果,现在我们**实现了CustomerRepository类不再依赖于特定的日志记录器。**相反,CustomerRepository类只依赖于抽象(接口),这种抽象在类及其接口中是显式可见的,因为它由成员变量和构造函数的参数表示。这意味着现在CustomerRepository类接受从外部传入的用于日志记录的服务对象。
//把Logger对象注入CustomerRespository类的实例
Logger logger = std::make_shared();
CustomerRepository customerRepository{ logger };
**这种变化有着积极的影响,能够促进松耦合。**客户端对象CustomerRespository现在可以配置提供日志功能的各种服务对象,如下面的UML类图所示。
CustomerRepository类可以通过其构造函数传入特定的日志实现类

此外,**CustomerRepository类的可测试性也得到了显著改进,不再对单例有隐藏的依赖。现在可以很容易地用测试对象(mock object)替换真正的日志服务。**例如,可以用spy方法装备模拟对象,以检查单元测试中哪些数据通过LoggingFacility接口离开了CustomerRepository对象。
//一个测试替身(模拟对象),用于对依赖于LoggingFacility的类进行单元测试
namespace test
{
#include"LoggingFacility.h"
#include

class LoggingFacilityMock :public LoggingFacility
{
public:
	virtual void writeInfoEntry(std::string entry) override
	{
		recentlyWrittenLogEntry = entry;
	}
	virtual void writeWarnEntry(std::string entry) override
	{
		recentlyWrittenLogEntry = entry;
	}
	virtual void writeErrorEntry(std::string entry) override
	{
		recentlyWrittenLogEntry = entry;
	}
	std::string getRecentlyWrittenLogEntry()const
	{
		return recentlyWrittenLogEntry;
	}
private:
	std::string recentlyWrittenLogEntry;
};

using MockLogger = std::shared_ptr<LoggingFacilityMock>;

在上面的例子中,用依赖注入模式代替恼人的单例模式,这只是其中一个示例。

基本上,**一个好的面向对象软件设计应该尽可能地保证所涉及的模块或组件是松耦合的,而依赖注入是实现这一目标的关键。通过一致地使用这种模式,软件设计将具有非常灵活的插件体系。**对软件测试的一个积极影响是,这种技术会产生高度的可测试对象。
从对象本身删除对象创建和关联的功能,并将对象创建和关联的功能集中在基础结构组件中,即所谓的汇编器(Assembler)或注入器(Injector)。这个组件通常在程序启动时操作,并处理整个软件系统的构建计划(如配置文件),也就是说,它按照正确的顺序实例化对象和服务,并将服务注入需要它们的对象中。
在这里插入图片描述
注意上图的依赖情况,创建依赖关系的方向(带有构造型<>的虚线箭头)将Assembler类引导到其他模块(类)。换句话说,在设计期间,没有类“知道”Assembler类的存在(这并不是完全正确的,因为至少有一个软件系统中的其他元素知道这个Assembler组件的存在,因为组装过程通常在程序的开始由某组件执行)。
//Assembler程序的部分实现
#include"StandardOutputLogger.h"
#include"CustomerRepository.h"
Logger loggingServiceToInject = std::make_shared();
auto customerRepository = std::make_shared(loggingServiceToInject);

构造函数注入

这种依赖注入被称为构造函数注入,因为被注入的服务对象作为参数,传递给客户端u第项的构造函数。构造函数注入的优点是客户端对象在构造过程中被完全初始化,然后就可以立即使用了。

setter注入

但是,如果在程序运行时将服务对象注入客户端对象,例如,如果在程序执行时偶尔创建一个客户端对象,或者在运行时更换日志记录器(Logger),那么,此时,客户端对象必须为注入的服务对象提供setter。
//为Logger注入提供setter方法的Customer类
#include"LoggingFacility.h"
class Customer
{
public:
Customer() = default;
void setLoggingService(const Logger& loggingService)
{
logger = loggingService;
}
private:
Identifier id;
Logger logger;
};

这种依赖注入技术称为setter注入。当然可以将构造函数注入和setter注入结合起来。

依赖注入是一种设计模式,它能够使软件松耦合,并且具有很好的配置性,可以根据不同客户端或产品的配置文件创建不同的对象。它极大地提高了软件系统的可测试性,因为通过依赖注入技术可以很容易地注入模拟对象。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值