C++设计模式之 依赖注入模式探索

news/2024/4/25 9:36:07/文章来源:https://blog.csdn.net/qq_21438461/article/details/130333871

依赖注入模式

  • 前言
  • 依赖注入模式的角色
  • 依赖注入模式的UML图
  • 依赖注入模式的设计和实现(C++)
  • 依赖注入和访问者模式的区别
  • 依赖注入模式的使用场景
  • 依赖注入模式的优缺点
  • 结语

前言

GoF设计模式主要关注的是面向对象编程设计的问题,而依赖注入作为一种编程技术,它的范畴更广泛,不仅适用于面向对象编程,还适用于其他编程范式。

依赖注入的核心思想是依赖反转原则(Dependency Inversion Principle, DIP),它是SOLID设计原则之一。依赖反转原则的核心观点是:高层模块不应该依赖于低层模块,它们都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。

依赖注入通过将依赖关系从对象内部移到对象外部,使对象在运行时可以动态接收所依赖的对象。这样可以降低对象之间的耦合度,提高代码的可维护性和可测试性。依赖注入常见的实现方式有构造函数注入、setter方法注入和接口注入。

总之,虽然依赖注入不属于GoF的23种设计模式,但它仍然是一种重要的编程技术,被广泛应用于软件开发中。

依赖注入模式的角色

虽然依赖注入并不属于传统的设计模式,但我们仍然可以将其划分为以下几个角色:

  1. 依赖抽象(Abstraction of Dependency):这是一个抽象接口或抽象基类,它定义了一个组件所依赖的功能。高层模块和低层模块都依赖于这个抽象,而不是相互依赖。
  2. 依赖实现(Implementation of Dependency):这是实现依赖抽象的具体类。通常,一个依赖抽象可能有多个依赖实现,它们提供了不同的功能。
  3. 依赖消费者(Consumer of Dependency):这是依赖于依赖抽象的类,它通过依赖抽象与依赖实现进行交互。依赖消费者通常在运行时接收所依赖的具体实现,而不是在编译时直接依赖具体实现。这样做有助于降低耦合度,提高代码的可维护性和可测试性。
  4. 依赖注入器(Dependency Injector):这是负责创建和管理依赖实现的对象,以及将依赖实现注入到依赖消费者中的组件。依赖注入器可以是手动编写的代码,也可以是使用依赖注入容器(如 Spring、Google Guice)实现的自动化机制。

依赖注入的过程大致如下:

  1. 定义依赖抽象,以及针对不同场景的依赖实现。
  2. 在依赖消费者中引用依赖抽象,并通过构造函数、setter方法或接口注入的方式接收具体的依赖实现。
  3. 使用依赖注入器将合适的依赖实现注入到依赖消费者中。

通过这样的设计,您可以在不修改依赖消费者的前提下,灵活地调整依赖实现,从而提高代码的可维护性和可测试性。

依赖注入模式的UML图

下面是一个依赖注入的简化版UML类图。这里使用了一个音频播放器的例子,有一个抽象的AudioDecoder接口,以及Mp3DecoderWavDecoder这两个具体实现。AudioPlayer类是依赖消费者,它使用AudioDecoder接口播放音频。DependencyInjector类负责将合适的解码器实例注入到AudioPlayer类中。

+----------------+         +---------------------+
| AudioDecoder   |         | DependencyInjector  |
+----------------+         +---------------------+
| +decode(data)  |<--------| +injectDecoder()    |
+--------+-------+         +----------+----------+^                             ||                             |
+--------+-------+         +----------+----------+
| Mp3Decoder     |         | WavDecoder          |
+----------------+         +---------------------+
| +decode(data)  |         | +decode(data)       |
+----------------+         +---------------------+|                             ||                             |
+--------+-------+         +----------+----------+
| AudioPlayer    |         |                     |
+----------------+         +---------------------+
| -decoder       |-------->|                     |
| +play(data)    |         |                     |
+----------------+         +---------------------+

在这个例子中,AudioPlayer类依赖于AudioDecoder接口来播放音频。通过依赖注入,您可以在运行时将不同的解码器(如Mp3DecoderWavDecoder)注入到AudioPlayer类中,以实现对不同音频格式的支持。DependencyInjector类负责将合适的解码器实例注入到AudioPlayer类中。

这只是一个简化的UML类图,实际应用中可能会更加复杂。例如,您可以使用依赖注入容器来自动管理依赖关系,而无需手动编写DependencyInjector类。

依赖注入模式的设计和实现(C++)

在C++中,我们可以使用构造函数注入、setter方法注入或接口注入的方式实现依赖注入。下面是一个基于构造函数注入的例子,实现一个简单的消息服务:

  1. 定义依赖抽象(MessageService接口):
class MessageService {
public:virtual ~MessageService() = default;virtual void sendMessage(const std::string& message, const std::string& recipient) = 0;
};
  1. 实现依赖抽象(EmailServiceSMSService实现):
class EmailService : public MessageService {
public:void sendMessage(const std::string& message, const std::string& recipient) override {// 实现发送电子邮件的逻辑std::cout << "Sending email: " << message << " to " << recipient << std::endl;}
};class SMSService : public MessageService {
public:void sendMessage(const std::string& message, const std::string& recipient) override {// 实现发送短信的逻辑std::cout << "Sending SMS: " << message << " to " << recipient << std::endl;}
};
  1. 创建依赖消费者(Notification类):
class Notification {
public:Notification(std::shared_ptr<MessageService> messageService) : messageService_(messageService) {}void notify(const std::string& message, const std::string& recipient) {messageService_->sendMessage(message, recipient);}private:std::shared_ptr<MessageService> messageService_;
};
  1. main()函数中使用依赖注入:
int main() {// 创建依赖实现std::shared_ptr<MessageService> emailService = std::make_shared<EmailService>();std::shared_ptr<MessageService> smsService = std::make_shared<SMSService>();// 使用依赖注入创建Notification对象Notification emailNotification(emailService);Notification smsNotification(smsService);// 使用依赖消费者emailNotification.notify("Hello, world!", "user@example.com");smsNotification.notify("Hello, world!", "123-456-7890");return 0;
}

在这个例子中,我们使用构造函数注入的方式将依赖实现(EmailServiceSMSService)注入到依赖消费者(Notification类)中。通过这种方式,我们可以在不修改Notification类的前提下,灵活地调整消息发送的实现,提高代码的可维护性和可测试性。

注意:这个例子中并没有使用专门的依赖注入容器。在实际项目中,您可以使用现有的依赖注入库(如 Boost.DI)来管理依赖关系。

依赖注入和访问者模式的区别

依赖注入和访问者模式都是设计模式,用于解决不同的问题。它们有以下区别:

  1. 目的:
    • 依赖注入:依赖注入(Dependency Injection)主要用于解决对象之间的依赖关系。它可以将对象之间的依赖关系从对象内部移到对象外部,使得对象可以在运行时动态地接收所依赖的对象。这样做的目的是为了降低对象之间的耦合度,提高代码的可维护性和可测试性。
    • 访问者模式:访问者模式(Visitor Pattern)主要用于解决对象结构中元素的操作。它可以将不同类型的对象上的操作解耦,并将它们组织在一个访问者类中。这样做的目的是为了将操作与数据结构分离,使得在添加新的操作时无需修改数据结构,同时在添加新的数据结构时也无需修改操作。
  2. 使用场景:
    • 依赖注入:当对象之间存在依赖关系,但这些依赖关系需要在运行时动态地改变时,可以使用依赖注入。依赖注入可以使代码更易于测试,因为依赖关系可以通过测试框架提供的模拟对象来模拟。
    • 访问者模式:当一个对象结构包含多种类型的对象,且需要在这些对象上执行多种不同的操作时,可以使用访问者模式。访问者模式可以将这些操作从对象结构中分离出来,以实现更高的可扩展性和可维护性。
  3. 实现方式:
    • 依赖注入:依赖注入通常通过构造函数、属性(setter)或接口注入的方式将依赖关系注入到对象中。依赖注入容器(如:Spring, Google Guice)可以帮助管理这些依赖关系。
    • 访问者模式:访问者模式通过定义一个访问者接口,其中包含针对不同类型的对象的操作方法。对象结构的元素实现一个accept方法,用于接受访问者并调用相应的操作方法。

总之,依赖注入和访问者模式解决不同类型的问题,具有不同的目的和使用场景。依赖注入关注于对象之间的依赖关系,访问者模式关注于对象结构上的操作。

依赖注入模式的使用场景

依赖注入(Dependency Injection,DI)模式适用于以下场景:

  1. 解耦组件:依赖注入可以降低组件之间的耦合度,使组件依赖于抽象接口而不是具体实现。这使得组件在不影响其他部分的情况下能够更容易地进行修改和替换。当您有多个组件需要相互协作时,使用依赖注入可以有效地解耦组件,提高系统的可维护性。
  2. 单元测试:依赖注入可以简化单元测试,因为它允许您为依赖提供测试替代品(如mock对象或stub)。这使得您可以独立地测试组件,而无需关心依赖的具体实现。通过依赖注入,您可以更容易地编写可测试的代码,提高代码的可测试性。
  3. 代码重用:依赖注入使您可以将具体实现与消费者解耦,从而更容易地重用代码。例如,如果您有一个通用的数据访问层,您可以将其作为一个依赖注入到其他组件中,而无需关心它是如何实现的。这有助于提高代码重用和模块化。
  4. 配置灵活性:通过依赖注入,您可以在运行时更改组件的依赖关系,而无需重新编译或修改代码。这使得您可以更灵活地调整系统的行为,根据实际需要使用不同的实现。
  5. 控制反转(Inversion of Control, IoC):依赖注入是一种实现控制反转的技术。控制反转意味着将依赖关系的创建和管理从组件内部移动到外部,由依赖注入容器或其他外部实体来负责。这样可以降低代码的耦合度,提高代码的可维护性和可测试性。

综上所述,依赖注入模式在需要解耦组件、提高代码可测试性、重用和配置灵活性的场景中非常有用。

依赖注入模式的优缺点

依赖注入模式具有一定的优点和缺点。

优点:

  1. 解耦:依赖注入模式有助于降低组件之间的耦合度,使组件依赖于抽象接口而非具体实现。这有助于改善代码的可维护性,因为组件可以独立地修改和替换,而不会影响其他部分。
  2. 可测试性:依赖注入允许您为依赖提供测试替代品(如 mock 对象或 stub),使您可以独立地测试组件,而无需关心依赖的具体实现。这有助于编写可测试的代码,提高代码的可测试性。
  3. 配置灵活性:依赖注入允许您在运行时更改组件的依赖关系,而无需重新编译或修改代码。这使得您可以更灵活地调整系统的行为,根据实际需要使用不同的实现。
  4. 代码重用:通过将具体实现与消费者解耦,依赖注入可以帮助您更轻松地重用代码。您可以将通用功能作为依赖注入到其他组件中,而无需关心它是如何实现的。
  5. 控制反转(Inversion of Control, IoC):依赖注入是一种实现控制反转的技术。通过将依赖关系的创建和管理从组件内部移到外部,降低了代码的耦合度,提高了代码的可维护性和可测试性。

缺点:

  1. 学习曲线:对于初学者来说,理解和实现依赖注入可能需要一定的学习成本。特别是在使用依赖注入容器时,可能需要一定时间来学习和熟悉相关概念和技术。
  2. 代码复杂性:使用依赖注入可能会增加代码的复杂性,因为它引入了额外的抽象层。这可能会导致更多的接口和类,以及更复杂的代码结构。
  3. 过度工程:在某些情况下,使用依赖注入可能会导致过度工程。对于一些简单的应用程序,直接使用具体实现可能就足够了,而引入依赖注入可能并不会带来明显的优势。

总之,依赖注入模式在许多情况下都是非常有用的,但需要权衡其优缺点,根据具体场景和需求来决定是否使用。在需要解耦、提高可测试性和配置灵活性的场景中,依赖注入模式的优点可能会超过其缺点。然而,在简单的应用程序中,引入依赖注入可能会导致不必要的复杂性和过度工程。

为了充分利用依赖注入模式的优势,您需要了解如何在适当的场景中使用它。以下是一些建议,可以帮助您在实践中更好地应用依赖注入模式:

  1. 合理划分模块和组件:依赖注入模式依赖于模块化的代码结构,所以在引入依赖注入之前,请确保您的应用程序具有合理的模块和组件划分。这将有助于减轻复杂性,并确保您能够充分利用依赖注入带来的好处。
  2. 选择合适的依赖注入方式:依赖注入有多种实现方法,如构造函数注入、setter方法注入和接口注入。根据您的具体需求和场景选择合适的方式。例如,构造函数注入通常更适用于需要在对象创建时就注入依赖的场景,而setter方法注入可能更适合于依赖可能在运行时发生变化的情况。
  3. 使用依赖注入容器:在复杂的应用程序中,使用依赖注入容器(如Boost.DI)可以简化依赖管理,提高代码的可维护性和可读性。依赖注入容器通常提供更高级的功能,如自动注入和生命周期管理,这可以帮助您更轻松地实现和管理依赖关系。
  4. 适度使用抽象:虽然依赖注入模式依赖于抽象接口,但这并不意味着您需要为每一个组件创建抽象接口。在一些情况下,直接使用具体实现可能更简单、更直接。您需要根据具体场景来权衡抽象程度,以确保代码保持清晰和简洁。
  5. 考虑性能影响:依赖注入模式通常会引入额外的间接性和运行时开销。虽然在大多数情况下,这种开销可能是可以接受的,但在性能敏感的应用程序中,您需要考虑这种影响,并根据需要进行优化。例如,您可以考虑使用更轻量级的依赖注入框架,或者在关键性能路径上避免使用依赖注入。

结语

依赖注入模式具有一定的优点和缺点,但从心理学的角度来看,它符合人类在处理复杂问题时的认知特点。通过解耦组件和使用抽象接口,依赖注入模式可以帮助我们更好地管理和维护代码。当我们面对复杂的软件系统时,人们往往善于处理具有清晰结构和逻辑关系的问题。依赖注入正是利用了这一特点,将复杂的依赖关系简化为更容易理解和处理的形式。

学习和运用依赖注入模式需要一定的努力,但正如心理学研究所揭示的那样,我们具备自我调整和适应新技能的能力。在掌握依赖注入模式的过程中,不妨从实际需求出发,通过案例分析和实践应用来积累经验。在实际项目中运用依赖注入模式,您将更好地领会到它为代码解耦、提高可测试性和可维护性带来的好处。

如果您觉得这篇关于依赖注入模式的介绍对您有所帮助,希望您能收藏和点赞,以便更多的人了解并学习这一有益的设计模式。让我们一起努力,通过学习和运用先进的设计模式,使我们的代码变得更加健壮、灵活和优雅。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_102630.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Dynamic Slicing for Deep Neural Networks

0、摘要 程序切片已广泛应用于各种软件工程任务中。然而&#xff0c;现有的程序切片技术只能处理由指令和变量构建的传统程序&#xff0c;而不能处理由神经元和突触组成的神经网络。在本文中&#xff0c;我们提出了 NNSlicer&#xff0c;这是第一种基于数据流分析的深度神经网络…

【ONE·C++ || 继承】

总言 主要介绍继承相关内容。 文章目录 总言1、继承介绍1.1、继承是什么1.2、继承方式与访问限定符1.3、继承作用域 2、基类和派生类对象赋值转换2.1、子类对象可以赋值给父类对象/指针/引用2.2、基类对象不能赋值给派生类对象2.3、基类的指针可以通过强制类型转换赋值给派生类…

vue2的生命周期

生命周期就是记录数据的状态。对数据进行操作 刚开始 new Vue() 创建了一个实例对象 beforeCreate() 数据还没有创建出来 created() 数据创建出来了&#xff0c;可以访问 判断有没有el 或 template 后 将模板编译成渲染函数 beforeMount() 数据还没有挂在到页面上面 mou…

深度强化学习——蒙特卡洛算法(6)

注&#xff1a;本章的内容作为补充插曲&#xff0c;大家可以选看&#xff0c;不过还是建议把最后一个使用蒙特卡洛近似求期望稍微看一下 蒙特卡洛是一大堆随机算法&#xff0c;通过随机样本来估算真实值 使用随机样本来近似Π 1、在[a,b]做随机均匀抽样&#xff0c;抽出n个样…

数据库物理存储结构

目录 一、数据库文件和文件组 1、数据库文件 &#xff08;1&#xff09; 主数据库文件&#xff08;Primary Database File&#xff09; &#xff08;2&#xff09; 次数据库文件&#xff08;Secondary Database File&#xff09; &#xff08;3&#xff09; 事务日志文件 …

Mysql 45讲和45问笔记(未完待续0203/04/24)

一、mysql 45讲 1&#xff09;索引的本质讲解 定义解释 所以是帮助Mysql高效获取数据的排好序的数据结构 索引数据结构 ①二叉树 ②红黑树 ③Hash表 ④B-Tree 原理讲解 可以看到右边的数据结构里面&#xff0c;是按照k-v来存数据结构的&#xff0c;key是col2的字段&#xf…

Java学习-MySQL-事务

Java学习-MySQL-事务 ACID原则&#xff1a;原子性、一致性、隔离性、持久性 原子性&#xff08;Atomicity&#xff09; 两个步骤要么一起成功&#xff0c;要么一起失败&#xff0c;不可能只成功一个。 举例&#xff1a; A账户400元&#xff0c;B账户600元&#xff0c;A向B转…

Yolo v1 笔记

个人不太懂的点 1.损失函数的4与5项 【论文解读】Yolo三部曲解读——Yolov1 - 知乎 https://www.youtube.com/watch?vNkFENlEb4kM&t672s 训练阶段&#xff1a; C_i 预测值&#xff1a;由网络输出出来7*7*30中第一个bbox和第二个bbox的置信度confidence C_i^hat 标签值…

PTA L2-046 天梯赛的赛场安排 (25 分)

天梯赛使用 OMS 监考系统&#xff0c;需要将参赛队员安排到系统中的虚拟赛场里&#xff0c;并为每个赛场分配一位监考老师。每位监考老师需要联系自己赛场内队员对应的教练们&#xff0c;以便发放比赛账号。为了尽可能减少教练和监考的沟通负担&#xff0c;我们要求赛场的安排满…

C++ 类和对象(中)构造函数 和 析构函数

上篇链接&#xff1a;C 类和对象&#xff08;上&#xff09;_chihiro1122的博客-CSDN博客 类的6个默认成员函数 我们在C当中&#xff0c;在写一些函数的时候&#xff0c;比如在栈的例子&#xff1a; 如上述例子&#xff0c;用C 返回这个栈是否为空&#xff0c;直接返回的话&am…

利用Python操作Mysql数据库

我们在进行Python编程的时候&#xff0c;时常要将一些数据保存起来&#xff0c;其中最方便的莫过于保存在文本文件了。但是如果保存的文件太大&#xff0c;用文本文件就不太现实了&#xff0c;毕竟打开都是个问题&#xff0c;这个时候我们需要用到数据库。提到数据库&#xff0…

json模块和pickle模块

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起探讨和分享Linux C/C/Python/Shell编程、机器人技术、机器学习、机器视觉、嵌入式AI相关领域的知识和技术。 json和pickle模块 json模块序列化与反序列化json模块中的方法 pickle模块 专栏&#xff1a;《python从…

JAVAweb开发学习

六、MybatisPlus快速上手 数据库操作 注意&#xff01;注意&#xff01;注意&#xff01;springboot版本选择2.7.2 1.ORM介绍&#xff08;对象关系映射&#xff09; 既包含存储&#xff0c;又包含映射。将java类映射到数据库 2.MybatisPlus介绍 ORM框架 数据库操作来啦…

【计算机网络】为什么 TCP 每次建立连接时,初始化序列号都要不一样呢?

【计算机网络】为什么 TCP 每次建立连接时&#xff0c;初始化序列号都要不一样呢&#xff1f; 为什么 TCP 每次建立连接时&#xff0c;初始化序列号都要不一样呢&#xff1f; 主要原因是为了防止历史报文被下一个相同四元组的连接接收。 TCP 四次挥手中的 TIME_WAIT 状态不是会…

机械键盘、口袋打印机,万元奖金等你拿!「万象格新」AI绘画X海报设计大赛即将开启...

号外&#xff01;「万象格新」大赛开启 如果阳光暖到你心里&#xff0c;那一定是一格在想你~ 春夏交替&#xff0c;万物焕发生机&#xff0c;明媚色彩娱情惬意 在这样一个美好的时节 如果你&#xff1a; 心中荡漾着色彩斑斓的 AI 绘画创意 想要 show 出独到的审美与非凡设计能力…

吴恩达团队AI诊断心律失常研究:准确率超人类医生

2019年&#xff0c;吴恩达团队在AI医疗领域实现了一项革命性的突破&#xff0c;他们成功地让AI诊断心律失常&#xff0c;其准确率高达83.7%&#xff0c;超过了人类心脏病医生的78.0%。这项研究成果已经发表在了知名期刊Nature Medicine上。 一、如何让AI学会诊断心律失常&…

闲谈【Stable-Diffusion WEBUI】的插件:美不美?交给AI打分

文章目录 &#xff08;零&#xff09;前言&#xff08;一&#xff09;咖啡店艺术评价&#xff08;Cafe Aesthetic&#xff09; &#xff08;零&#xff09;前言 本篇主要提到了WEBUI的Cafe Aesthetic插件&#xff0c;这是一个相对独立的插件&#xff0c;单独标签页&#xff0c;…

Python小姿势 - Python基础知识

Python基础知识 Python是一种解释型、面向对象、动态数据类型的高级程序设计语言。 Python的创始人为吉多范罗苏姆&#xff08;Guido van Rossum&#xff09;&#xff0c;于1989年底发布第一个公开发行版本——0.9.0。 自2004年以来&#xff0c;Python已经成为顶级开源项目&…

希尔排序的实现

希尔排序是插入排序的一种升级&#xff0c;其基本思想是&#xff1a; 先选定一个整数&#xff0c;把待排序文件中所有记录分成个组&#xff0c;所有距离为的记录分在同一组内&#xff0c;并对每 一组内的记录进行排序。然后&#xff0c;取&#xff0c;重复上述分组和排序的工 作…

使用Linux运维常识

一.基础操作 1.终端常用快捷键 快捷键描述ctrl键盘左键向左跳一个单词ctrl键盘右键向右跳一个单词Ctrl c停止当前正在运行的命令。Ctrl z将当前正在运行的命令放入后台并暂停它的进程。Ctrl d关闭当前终端会话。Ctrl l清屏&#xff0c;也可以用clear命令实现Tab自动补全当…