1. 依赖倒置

依赖倒置原则(Dependency Inversion Principle, DIP)是 SOLID 原则中的一项,其核心思想是通过抽象解耦高层模块和低层模块,使二者都依赖于抽象而非具体实现

依赖反转/倒置的体现:传统依赖方向是高层模块直接调用低层模块,在源码级别上高层模块依赖低层细节模块。而 DIP 通过抽象反转这种依赖关系,使低层模块的实现在源码级别上依赖高层定义的抽象(视为高层模块的一部分)。

1.1 依赖倒置原则的核心

  1. 高层模块不直接依赖低层模块,二者都应依赖抽象(接口或抽象类,接口由高层模块定义,视为高层模块的一部分)。
  2. 抽象不依赖细节,细节(具体实现)应依赖抽象。

1.2 依赖倒置指导方针

  • 变量不可以持有具体类的引用——改用工厂,避免直接使用 new 持有具体类的引用(new 具体类的操作都封装到工厂中)
  • 不要让类派生自具体类——派生自抽象类或接口,这样就不依赖具体类了
  • 不要覆盖基类中已经实现的方法——如果这样,说明不是一个真正适合被继承的抽象

1.3 示例

场景

  • 高层模块 ReportGenerator 需要生成报告,依赖数据获取功能。
  • 低层模块 MySQLDatabaseSQLiteDatabase 提供具体的数据操作。

传统实现(未遵循 DIP)

// 低层模块:直接依赖具体实现
class MySQLDatabase {
public:
void connect() { /* MySQL 连接逻辑 */ }
std::string fetchData() { return "MySQL 数据"; }
}; // 高层模块直接依赖低层具体类
class ReportGenerator {
private:
MySQLDatabase db; // 直接依赖具体实现
public:
void generateReport() {
db.connect();
auto data = db.fetchData();
std::cout << "报告数据: " << data << std::endl;
}
};

问题ReportGenerator 直接依赖 MySQLDatabase,更换数据库(如改用 SQLite)需修改高层代码。

遵循 DIP 的实现

  1. 定义抽象接口
class Database {
public:
virtual ~Database() = default;
virtual void connect() = 0;
virtual std::string fetchData() = 0;
};
  1. 低层模块实现接口
class MySQLDatabase : public Database {
public:
void connect() override { /* MySQL 连接逻辑 */ }
std::string fetchData() override { return "MySQL 数据"; }
}; class SQLiteDatabase : public Database {
public:
void connect() override { /* SQLite 连接逻辑 */ }
std::string fetchData() override { return "SQLite 数据"; }
};
  1. 高层模块依赖抽象
class ReportGenerator {
private:
Database& db; // 依赖抽象接口
public:
ReportGenerator(Database& database) : db(database) {} // 依赖注入
void generateReport() {
db.connect();
auto data = db.fetchData();
std::cout << "报告数据: " << data << std::endl;
}
};
  1. 使用示例
int main() {
MySQLDatabase mysqlDb;
SQLiteDatabase sqliteDb; ReportGenerator report1(mysqlDb); // 使用 MySQL
report1.generateReport(); ReportGenerator report2(sqliteDb); // 使用 SQLite
report2.generateReport(); return 0;
}

1.4 依赖倒置优势

  • 解耦:高层模块不依赖低层具体实现,可灵活替换数据库(如新增 MongoDB 只需实现 Database 接口)。
  • 可维护性:修改低层代码(如优化 MySQLDatabase)不影响高层模块。
  • 可测试性:可通过 Mock 对象(实现 Database 接口)轻松测试 ReportGenerator

1.5 依赖倒置小结

依赖倒置原则通过抽象解耦模块,使依赖关系从“高层 → 低层”变为“高层 → 抽象 ← 低层”,从而提升系统的灵活性和可维护性。在 C++ 中,可通过抽象类(接口)和依赖注入(如构造函数传入接口指针/引用)实现这一原则。

2. 依赖注入 DI

依赖注入(Dependency Injection, DI)是一种将对象依赖关系的外部化技术,其核心思想是:对象不直接创建或管理自己的依赖,而是由外部(调用者或框架)提供依赖的实例。通过这种方式,代码的耦合度降低,灵活性和可测试性显著提高。

2.1 依赖注入的本质

  1. 控制反转(IoC)

    依赖注入是控制反转的一种实现方式。传统代码中,对象自己控制依赖的创建(如 new 一个具体类),而依赖注入将这一控制权交给外部,实现“依赖被注入到对象中”。
  2. 依赖抽象而非实现

    依赖注入通常结合接口或抽象类使用,确保对象依赖的是抽象,而非具体实现(符合依赖倒置原则)。

2.2 依赖注入的三种方式

1. 构造函数注入(最常用)

通过构造函数传递依赖,确保对象在创建时即具备完整依赖。

class NotificationService {
private:
MessageSender& sender; // 依赖抽象接口
public:
NotificationService(MessageSender& sender) : sender(sender) {} // 构造函数注入
void sendMessage(const std::string& msg) {
sender.send(msg);
}
};

2. 属性注入(Setter 注入)

通过公开的成员属性或 Setter 方法动态设置依赖。

class NotificationService {
public:
void setSender(MessageSender& sender) { // Setter 注入
this->sender = &sender;
}
private:
MessageSender* sender;
};

3. 方法注入

通过方法参数传递依赖,适用于临时或局部依赖。

class NotificationService {
public:
void sendMessage(MessageSender& sender, const std::string& msg) { // 方法注入
sender.send(msg);
}
};

2.3 为什么需要依赖注入?

1. 解耦与可维护性

  • 传统代码:对象内部直接创建依赖,导致紧耦合。

class UserService {

private:

MySQLDatabase db; // 直接依赖具体类

};


若需改用 `SQLiteDatabase`,必须修改 `UserService` 的代码。 - **依赖注入**:通过接口解耦,仅需注入不同实现。 ```cpp
class UserService {
private:
Database& db; // 依赖抽象
public:
UserService(Database& db) : db(db) {}
};

2. 可测试性

  • 依赖注入允许在测试时替换为 Mock 对象。

    class MockDatabase : public Database { /* 模拟实现 */ };
    
    TEST(UserServiceTest) {
    MockDatabase mockDb;
    UserService service(mockDb); // 注入 Mock 对象
    // 执行测试...
    }

3. 扩展性

  • 新增功能时,只需实现新依赖并注入,无需修改现有代码。

    class MongoDB : public Database { /* 新数据库实现 */ };
    
    MongoDB mongoDb;
    UserService service(mongoDb); // 直接注入新依赖

2.4 C++ 依赖注入的实践技巧

1. 使用智能指针管理生命周期

避免裸指针导致的内存泄漏,使用 std::shared_ptrstd::unique_ptr

cpp

class NotificationService {
private:
std::shared_ptr<MessageSender> sender; // 智能指针管理依赖
public:
NotificationService(std::shared_ptr<MessageSender> sender) : sender(sender) {}
};

2. 结合工厂模式

通过工厂类集中管理依赖的创建逻辑。

class SenderFactory {
public:
static std::shared_ptr<MessageSender> createSender(const std::string& type) {
if (type == "email") return std::make_shared<EmailSender>();
else return std::make_shared<SmsSender>();
}
}; // 使用工厂创建依赖
auto sender = SenderFactory::createSender("email");
NotificationService service(sender);

3. 依赖注入容器(IoC Container)

在复杂项目中,使用容器自动管理依赖关系(如 Boost.DI)。

#include <boost/di.hpp>
namespace di = boost::di; // 定义接口和实现
class Database { /* ... */ };
class MySQLDatabase : public Database { /* ... */ }; // 配置容器
auto injector = di::make_injector(
di::bind<Database>().to<MySQLDatabase>()
); // 自动注入依赖
class UserService {
public:
UserService(Database& db) { /* ... */ }
};
UserService service = injector.create<UserService>();

2.5 依赖注入的常见误区

  1. 依赖注入 ≠ 工厂模式

    工厂模式负责创建对象,而依赖注入负责传递对象。二者常结合使用,但目的不同。
  2. 依赖注入 ≠ 必须用框架

    即使不用框架(如 Boost.DI),通过构造函数或参数传递依赖,也能实现依赖注入。
  3. 过度注入问题

    若一个类需要注入过多依赖(如超过 4 个),可能设计存在问题,需考虑拆分职责。

2.6 依赖注入小结

  • 依赖注入的核心:将依赖的创建和绑定从对象内部转移到外部。

  • 核心价值:解耦、可测试、可扩展。

  • C++ 实现关键:

    • 通过接口抽象依赖。
    • 使用构造函数/智能指针传递依赖。
    • 结合工厂模式或 IoC 容器管理复杂依赖关系。

3. 控制反转 IoC

IoC(Inversion of Control,控制反转) 是一种软件设计原则,其核心思想是将程序流程的控制权从开发者转移给框架或容器,以降低代码的耦合度,提高模块化和可维护性。它是实现依赖倒置原则(DIP)的关键机制,也是现代框架(如 Spring、.NET Core)和依赖注入(DI)容器的基础。

3.1 控制反转 IoC vs. 依赖注入 DI

  • IoC(控制反转):广义的设计原则,表示控制权转移的范式。其本质是将程序流程的控制权从开发者转移到框架或容器
  • DI(依赖注入):IoC 的一种具体实现技术,通过外部传递依赖。

关系

  • 依赖注入是控制反转的实现方式之一。
  • 控制反转还可以通过模板方法、回调(关联:好莱坞原则)等方式实现。
  • 使用 IoC 容器(如 Boost.DI)自动管理复杂依赖关系。

4. 工厂模式

尽管依赖倒置和依赖注入都强调面向抽象编程,但在实际编码中仍需创建(new)具体底层组件(ConcreteClass)

工厂模式主要分为三种,严格来说包括 简单工厂模式工厂方法模式抽象工厂模式。以下是它们的核心区别、适用场景及 C++ 示例:

4.1 简单工厂模式(Simple Factory)

有时候简单工厂不被视为正式的设计模式,而是一个编程习惯。

核心思想

  • 通过一个工厂类,根据传入的参数决定创建哪种具体产品对象。
  • 不符合开闭原则(新增产品需修改工厂类逻辑)。

适用场景

  • 产品类型较少且创建逻辑简单。
  • 不需要频繁扩展新类型。

C++ 示例

// 抽象产品
class Shape {
public:
virtual void draw() = 0;
virtual ~Shape() = default;
}; // 具体产品
class Circle : public Shape {
public:
void draw() override { std::cout << "画一个圆形" << std::endl; }
}; class Square : public Shape {
public:
void draw() override { std::cout << "画一个正方形" << std::endl; }
}; // 简单工厂类
class ShapeFactory {
public:
static Shape* createShape(const std::string& type) {
if (type == "circle") return new Circle();
else if (type == "square") return new Square();
else return nullptr;
}
}; // 使用示例
int main() {
Shape* circle = ShapeFactory::createShape("circle");
circle->draw(); // 输出: 画一个圆形
delete circle;
return 0;
}

4.2 工厂方法模式(Factory Method)

核心思想

  • 定义一个创建对象的抽象方法,由子类决定实例化哪个类。
  • 符合开闭原则(新增产品只需新增子类工厂)。

适用场景

  • 产品类型可能频繁扩展。
  • 需要将对象创建延迟到子类。

C++ 示例

// 抽象产品
class Database {
public:
virtual void connect() = 0;
virtual ~Database() = default;
}; // 具体产品
class MySQL : public Database {
public:
void connect() override { std::cout << "连接到 MySQL" << std::endl; }
}; class PostgreSQL : public Database {
public:
void connect() override { std::cout << "连接到 PostgreSQL" << std::endl; }
}; // 抽象工厂
class DatabaseFactory {
public:
virtual Database* createDatabase() = 0;
virtual ~DatabaseFactory() = default;
}; // 具体工厂
class MySQLFactory : public DatabaseFactory {
public:
Database* createDatabase() override { return new MySQL(); }
}; class PostgreSQLFactory : public DatabaseFactory {
public:
Database* createDatabase() override { return new PostgreSQL(); }
}; // 使用示例
int main() {
DatabaseFactory* factory = new PostgreSQLFactory();
Database* db = factory->createDatabase();
db->connect(); // 输出: 连接到 PostgreSQL
delete db;
delete factory;
return 0;
}

4.3 抽象工厂模式(Abstract Factory)

核心思想

  • 提供一个接口,用于创建相关或依赖对象族,而无需指定具体类。
  • 抽象工厂包含多个工厂方法,每个方法负责创建一个产品族中的对象。

适用场景

  • 需要创建一组相关或依赖的对象(例如 GUI 组件:按钮、文本框、下拉菜单等)。
  • 系统需要独立于产品的创建、组合和表示。

C++ 示例

// 抽象产品:按钮
class Button {
public:
virtual void render() = 0;
virtual ~Button() = default;
}; // 具体产品:Windows 按钮
class WindowsButton : public Button {
public:
void render() override { std::cout << "Windows 风格按钮" << std::endl; }
}; // 具体产品:MacOS 按钮
class MacOSButton : public Button {
public:
void render() override { std::cout << "MacOS 风格按钮" << std::endl; }
}; // 抽象产品:文本框
class TextBox {
public:
virtual void display() = 0;
virtual ~TextBox() = default;
}; // 具体产品:Windows 文本框
class WindowsTextBox : public TextBox {
public:
void display() override { std::cout << "Windows 风格文本框" << std::endl; }
}; // 具体产品:MacOS 文本框
class MacOSTextBox : public TextBox {
public:
void display() override { std::cout << "MacOS 风格文本框" << std::endl; }
}; // 抽象工厂
class GUIFactory {
public:
virtual Button* createButton() = 0;
virtual TextBox* createTextBox() = 0;
virtual ~GUIFactory() = default;
}; // 具体工厂:Windows 风格组件
class WindowsFactory : public GUIFactory {
public:
Button* createButton() override { return new WindowsButton(); }
TextBox* createTextBox() override { return new WindowsTextBox(); }
}; // 具体工厂:MacOS 风格组件
class MacOSFactory : public GUIFactory {
public:
Button* createButton() override { return new MacOSButton(); }
TextBox* createTextBox() override { return new MacOSTextBox(); }
}; // 使用示例
int main() {
GUIFactory* factory = new MacOSFactory(); Button* button = factory->createButton();
button->render(); // 输出: MacOS 风格按钮 TextBox* textBox = factory->createTextBox();
textBox->display(); // 输出: MacOS 风格文本框 delete button;
delete textBox;
delete factory;
return 0;
}

4.4 三种工厂模式对比

模式 核心目标 扩展性 适用场景
简单工厂 集中创建单一类型的不同对象 差(需修改工厂类) 少量固定类型,无需频繁扩展
工厂方法 将对象创建延迟到子类 好(新增工厂子类) 单一产品,类型可能频繁扩展
抽象工厂 创建多个相关或依赖的对象族 好(新增工厂子类) 多个关联产品,需保持风格一致性

4.5 工厂模式小结

  • 简单工厂:适合简单场景,但违背开闭原则。
  • 工厂方法:解决单一产品的扩展问题。
  • 抽象工厂:解决多产品族的创建问题,强调产品之间的关联性。

根据需求选择合适模式:若产品单一且可能扩展,用工厂方法;若需创建一组关联对象,用抽象工厂;若产品类型固定且简单,用简单工厂。

5. 总结

依赖倒置(DIP)、依赖注入(DI)、控制反转(IoC)和工厂模式是软件设计中紧密相关的概念,它们共同服务于代码的解耦和可维护性。

5.1 关联

  • 依赖倒置原则(Dependency Inversion Principle, DIP):高层模块不依赖低层模块,两者都依赖抽象(接口或抽象类)。该思想指导工厂模式、DI 和 IoC 的设计方向。

  • 控制反转(Inversion of Control, IoC):将对象的创建和生命周期管理权从程序内部转移给外部容器(如框架)。例如:依赖由外部容器(如工厂或框架)创建并注入,而不是直接创建依赖。工厂模式和依赖注入 DI 是实现 IoC 的具体方式。

  • 依赖注入(Dependency Injection, DI):通过构造函数、Setter 或接口,将依赖对象被动传递给使用方。是实现 IoC 的具体技术手段。工厂模式常用于生成这些依赖对象。

  • 工厂模式(Factory Pattern):封装具体对象创建逻辑,通过工厂类统一创建对象。是实现 IoC 的手段之一,隐藏实例化细节,支持 DIP 和 DI。是依赖注入 DI 和控制反转 IoC 的底层支撑。

四者共同目标是解耦代码,提升扩展性和可维护性。

5.2 示例全链路

// 1. 遵循 DIP:定义抽象接口
class IStorage { /* ... */ }; // 2. 具体实现
class DatabaseStorage : public IStorage { /* ... */ }; // 3. 工厂模式:封装对象创建
class StorageFactory {
public:
static IStorage* createStorage() { return new DatabaseStorage(); }
}; // 4. 依赖注入:通过构造函数传递对象
class UserService {
private:
IStorage* storage;
public:
UserService(IStorage* storage) : storage(storage) {}
}; // 5. 控制反转:由工厂创建依赖,而非 UserService 内部创建
int main() {
IStorage* storage = StorageFactory::createStorage();
UserService userService(storage); // DI 注入
userService.saveUser();
delete storage;
return 0;
}

依赖倒置 DIP、依赖注入 DI、控制反转 IoC 和工厂模式的更多相关文章

  1. 个人对【依赖倒置(DIP)】、【控制反转(IOC)】、【依赖注入(DI)】浅显理解

    一.依赖倒置(Dependency Inversion Principle) 依赖倒置是面向对象设计领域的一种软件设计原则.(其他的设计原则还有:单一职责原则.开放封闭原则.里式替换原则.接口分离原则 ...

  2. 依赖注入 DI 控制反转 IOC MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  3. ADO.NET .net core2.0添加json文件并转化成类注入控制器使用 简单了解 iTextSharp实现HTML to PDF ASP.NET MVC 中 Autofac依赖注入DI 控制反转IOC 了解一下 C# AutoMapper 了解一下

    ADO.NET   一.ADO.NET概要 ADO.NET是.NET框架中的重要组件,主要用于完成C#应用程序访问数据库 二.ADO.NET的组成 ①System.Data  → DataTable, ...

  4. 浅析“依赖注入(DI)/控制反转(IOC)”的实现思路

    开始学习Spring的时候,对依赖注入(DI)——也叫控制反转(IOC)—— 的理解不是很深刻.随着学习的深入,也逐渐有了自己的认识,在此记录,也希望能帮助其他入门同学更深入地理解Spring.本文不 ...

  5. ASP.NET MVC 中 Autofac依赖注入DI 控制反转IOC 了解一下

    先简单了解一这个几个 名词的意思. 控制反转(IOC) 依赖注入(DI) 并不是某种技术. 而是一种思想.一种面向对象编程法则 什么是控制反转(IOC)?  什么是依赖注入(DI) 可以点击下面链接 ...

  6. 依赖注入 DI 控制反转 IOC 概念 案例 MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  7. ASP.NET中IOC容器Autofac(依赖注入DI 控制反转IOC)

    IOC的一个重点是在程序运行中,动态的向某个对象提供它所需要的其他对象.这一点是通过DI来实现的.Autofac则是比较流行的一款IOC容器. IoC和DI有什么关系呢?其实它们是同一个概念的不同角度 ...

  8. 依赖、耦合、解耦、控制反转(IOC)、依赖注入(DI)

    随着net的深入学习,出现了很多概念性的东西需要理解,现在统一记录一下. 1.依赖:现阶段在任何一个有请求作用的系统,都会出现A类调用B类的情况,这时候A类就依赖于B类,A类和B类存在依赖关系. 2. ...

  9. 轻松学,浅析依赖倒置(DIP)、控制反转(IOC)和依赖注入(DI) 依赖注入和控制反转的理解,写的太好了。

    轻松学,浅析依赖倒置(DIP).控制反转(IOC)和依赖注入(DI) 2017年07月13日 22:04:39 frank909 阅读数:14269更多 所属专栏: Java 反射基础知识与实战   ...

  10. 依赖倒置原则DIP&控制反转IOC&依赖注入DI

    依赖倒置原则DIP是软件设计里一个重要的设计思想,它规定上层不依赖下层而是共同依赖抽象接口,通常可以是上层提供接口,然后下层实现接口,上下层之间通过接口完全透明交互.这样的好处,上层不会因依赖的下层修 ...

随机推荐

  1. Mac下SSH Key配置

    1 .检查.ssh文件夹是否存在 $ ls -al ~/.ssh 2.如果不存在新建.ssh文件平 $ mkdir ~/.ssh 3.生成KEY在命令行中输入,your_email@example.c ...

  2. jacoco代码覆盖率报告分析

    一.目的 对Jacoco代码覆盖率统计维度.报告字段说明.报告详细分析描述.并为精准测试.健壮性测试提供指导. 二.Jacoco代码覆盖率统计维度 Jacoco是从代码指令(Instructions, ...

  3. JVM实战—1.Java代码的运行原理

    大纲 1.Java代码到底是如何运行起来的 2.JVM类加载机制的一系列概念 3.JVM中有哪些内存区域及各自的作用 4.JVM的垃圾回收机制的作用 5.问题汇总 1.Java代码到底是如何运行起来的 ...

  4. Qt 中实现系统主题感知

    [写在前面] 在现代桌面应用程序开发中,系统主题感知是一项重要的功能,它使得应用程序能够根据用户的系统主题设置(如深色模式或浅色模式)自动调整其外观. Qt 作为一个跨平台的C++图形用户界面应用程序 ...

  5. LRU的map+双链表实现(Go描述)

    面云账户时候问了LRU,具体实现的方式是map+双链表.Set和Get的时间复杂度都是O(1).完整写一遍复习一下, 仅作记录 /** * @Author: lzw5399 * @Date: 2021 ...

  6. 【Java 温故而知新系列】基础知识-03 基本类型对应之包装类

    1.包装类都有哪些? 基本类型都有对应的包装类型,这些包装类提供了一种面向对象的方式来处理基本数据类型,允许它们被用于需要对象的场景,如集合框架.泛型等. 对应关系: 基本类型 包装类型 boolea ...

  7. RabbitMQ-限流

    1.简介 为什么要对消费端进行限流? 其实很好理解,比如我们常能接触到的消费场景:春运期间12306火车票的抢购,双11期间的下单等.这些场景都有一个共同点就是都会导致短暂时间内请求数激增,如果我们的 ...

  8. JavaWeb的一些理解

    WEB概述 WEB是什么 WEB,在英语中web即表示网页的意思,它用于表示Internet主机上供外界访问的资源. Internet上的资源分类 Internet上供外界访问的Web资源分为: 静态 ...

  9. ElasticSearch入门 第二篇

    集群配置----------------------------- ElasticSearch共有两个配置文件,都位于config目录下,分别是elasticsearch.yml和logging.ym ...

  10. C# Winform 通过 NAudio 获取控制电脑操作系统音量

    https://github.com/naudio/NAudio NAudio 是一个开源的 .NET 音频库,由 Mark Heath 开发,开源地址:https://github.com/naud ...