C++笔记之设计模式:setter函数、依赖注入
code review!
文章目录
- C++笔记之设计模式:setter函数、依赖注入
- 1.概念
- 2.基本示例
- 3.setter函数
- 4.基本示例+setter函数构成依赖注入
- 5.概念——ChatGpt
- 6.构造函数注入示例
- 7.接口注入示例
- 8. 构造函数注入的使用场景和用途
- 9.接口注入的使用场景和用途
- 10.使用一个简单的业务逻辑来演示构造函数注入和接口注入的使用场景。
- 10.1.构造函数注入示例
- 10.2.接口注入示例
- 11.附加知识:SOLID设计原则
- 12.附加知识:依赖倒置原则
- 13.一个图形绘制应用程序。我们将创建一个图形类(Shape),并通过依赖注入来注入不同的绘制器(Drawer)来绘制不同的图形。
1.概念
依赖注入(Dependency Injection,DI)是一种软件设计模式,用于管理和解决组件之间的依赖关系。在传统的编程中,一个对象通常需要在自身内部创建或获取其所依赖的其他对象,这可能会导致紧密耦合的代码,使得代码难以测试、难以维护和难以扩展。依赖注入的目标是通过从外部传递依赖项来解耦组件,提高代码的可测试性、可维护性和灵活性。
依赖注入的主要思想是,一个组件(被称为"受注入对象")不应该负责创建或获取其依赖的对象。相反,这些依赖应该由外部实例化,并通过构造函数、方法参数、属性等方式注入到受注入对象中。这种方式可以在不修改受注入对象的情况下,灵活地替换依赖项的具体实现,以及在测试时传递模拟对象。
依赖注入可以分为以下几种形式:
-
构造函数注入:通过构造函数将依赖项传递给受注入对象。这是最常见的依赖注入方式。通过构造函数,依赖关系在对象创建时就被传递,并在整个对象生命周期中保持稳定。
-
方法参数注入:通过方法的参数将依赖项传递给对象的方法。这在需要特定依赖项执行特定操作的情况下非常有用。
-
属性注入:通过公开的属性将依赖项传递给对象。这种方式可能导致依赖关系的意外更改,因此在使用时需要小心。
-
接口注入:通过接口或抽象类定义依赖项,然后将具体实现传递给对象。这种方式允许在运行时替换依赖项的具体实现,实现多态性。
依赖注入的优势包括:
-
解耦和灵活性:减少了组件之间的紧耦合,使得代码更加灵活、可维护和易于扩展。
-
可测试性:可以轻松地传递模拟对象或桩对象,以进行单元测试,从而提高代码的测试覆盖率。
-
可读性:依赖关系在代码中被明确地传递,使代码更易于理解。
简单理解来说就是当依赖的某个对象是通过外部来注入,而不是自己创建。
2.基本示例
3.setter函数
4.基本示例+setter函数构成依赖注入
代码
class Tools {
public:
virtual void doWork() = 0;
};
class Hammer : public Tools {
public:
void doWork() override {
std::cout << "use hammer" << std::endl;
}
};
class Axe : public Tools {
public:
void doWork() override {
std::cout << "use Axe" << std::endl;
}
};
class Human {
public:
Human(Tools& t) : tools(t) {}
void doWork() {
tools.doWork();
}
Tools& tools;
};
void MakeHuman() {
Hammer hammer;
Human human1(hammer);
human1.doWork();
Axe axe;
Human human2(axe);
human2.doWork();
}
int main() {
MakeHuman();
return 0;
}
5.概念——ChatGpt
6.构造函数注入示例
代码
#include <iostream>
class Dependency {
public:
void DoSomething() {
std::cout << "Dependency is doing something." << std::endl;
}
};
class Service {
public:
Service(Dependency* dependency) : dependency_(dependency) {}
void UseDependency() {
std::cout << "Service is using dependency." << std::endl;
dependency_->DoSomething();
}
private:
Dependency* dependency_;
};
int main() {
Dependency dependency;
Service service(&dependency);
service.UseDependency();
return 0;
}
7.接口注入示例
代码
#include <iostream>
class IDependency {
public:
virtual void DoSomething() = 0;
};
class Dependency : public IDependency {
public:
void DoSomething() override {
std::cout << "Dependency is doing something." << std::endl;
}
};
class Service {
public:
Service(IDependency* dependency) : dependency_(dependency) {}
void UseDependency() {
std::cout << "Service is using dependency." << std::endl;
dependency_->DoSomething();
}
private:
IDependency* dependency_;
};
int main() {
Dependency dependency;
Service service(&dependency);
service.UseDependency();
return 0;
}
8. 构造函数注入的使用场景和用途
构造函数注入是一种通过类的构造函数将依赖项传递给类的方式。这种方式适用于以下情况:
-
类需要一个稳定的依赖关系:通过构造函数注入,依赖项在对象创建时被设置,然后在整个对象生命周期内保持不变,确保了稳定的依赖关系。
-
松耦合和可测试性:依赖项的传递通过构造函数进行,使得类与具体的依赖实现解耦,从而提高了代码的可测试性和可维护性。
-
代码可读性:通过构造函数明确地传递依赖项,使得类的依赖关系更加明确和清晰。
-
依赖的外部控制:通过构造函数注入,外部代码可以在创建对象时决定传递的依赖项,从而实现对依赖的更好控制。
9.接口注入的使用场景和用途
接口注入是通过使用接口或抽象类定义依赖关系,然后传递具体实现的方式。这种方式适用于以下情况:
-
多态性和扩展性:接口注入允许在运行时决定使用的具体实现,从而实现多态性。这对于在不修改现有代码的情况下扩展应用程序非常有用。
-
模块替换:通过传递不同的实现,可以轻松替换具体的依赖项,从而实现模块的替换和重用。
-
依赖反转:通过依赖接口而不是具体实现,实现了依赖反转的原则,即高层模块不依赖于低层模块的具体实现细节。
-
解耦和灵活性:接口注入减少了类之间的紧耦合,从而提高了代码的灵活性和可维护性。
总之,构造函数注入和接口注入都是实现依赖注入的有效方式,可以根据项目需求选择适当的方式。构造函数注入适用于稳定的依赖关系和明确的依赖项传递,而接口注入适用于多态性、扩展性和模块替换的需求。无论使用哪种方式,依赖注入的目标是减少紧耦合,提高代码的可测试性、可维护性和灵活性。
10.使用一个简单的业务逻辑来演示构造函数注入和接口注入的使用场景。
我们将创建一个模拟的报告生成系统,其中包括一个报告生成器和一个数据源。
业务逻辑:
我们有一个报告生成器(ReportGenerator),它依赖于一个数据源(DataSource)。报告生成器通过数据源获取数据,并生成报告。
10.1.构造函数注入示例
代码
#include <iostream>
#include <string>
// 数据源类,提供获取数据的方法
class DataSource {
public:
std::string GetData() {
return "Data from the data source.";
}
};
// 报告生成器类,依赖于数据源
class ReportGenerator {
public:
// 构造函数,接收一个数据源对象作为依赖
ReportGenerator(DataSource* dataSource) : dataSource_(dataSource) {}
// 生成报告的方法
void GenerateReport() {
// 使用数据源获取数据
std::string data = dataSource_->GetData();
std::cout << "Generating report with data: " << data << std::endl;
}
private:
DataSource* dataSource_; // 数据源对象的指针
};
int main() {
DataSource dataSource; // 创建数据源对象
ReportGenerator reportGenerator(&dataSource); // 创建报告生成器,并传入数据源对象
reportGenerator.GenerateReport(); // 生成报告
return 0;
}
10.2.接口注入示例
代码
#include <iostream>
#include <string>
// 数据源接口,定义获取数据的纯虚方法
class IDataSource {
public:
virtual std::string GetData() = 0;
};
// 数据源类,实现数据源接口,提供获取数据的方法
class DataSource : public IDataSource {
public:
std::string GetData() override {
return "Data from the data source.";
}
};
// 报告生成器类,依赖于数据源接口
class ReportGenerator {
public:
// 构造函数,接收一个数据源接口的指针作为依赖
ReportGenerator(IDataSource* dataSource) : dataSource_(dataSource) {}
// 生成报告的方法
void GenerateReport() {
// 使用数据源接口获取数据
std::string data = dataSource_->GetData();
std::cout << "Generating report with data: " << data << std::endl;
}
private:
IDataSource* dataSource_; // 数据源接口的指针
};
int main() {
DataSource dataSource; // 创建数据源对象
ReportGenerator reportGenerator(&dataSource); // 创建报告生成器,并传入数据源对象
reportGenerator.GenerateReport(); // 生成报告
return 0;
}
11.附加知识:SOLID设计原则
SOLID是一组五个面向对象编程和设计的原则,旨在帮助开发者创建更加可维护、灵活和可扩展的软件。这些原则是:
-
单一职责原则(Single Responsibility Principle,SRP):
每个类应该有且仅有一个引起变化的原因,即一个类应该只负责一项职责。这有助于将类的职责分离,使代码更加模块化,提高可维护性和可测试性。 -
开放封闭原则(Open/Closed Principle,OCP):
软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着在不修改现有代码的情况下,可以通过添加新代码来扩展功能。这有助于减少影响已有功能的风险。 -
里氏替换原则(Liskov Substitution Principle,LSP):
子类必须能够替换其基类,而不影响程序的正确性。这意味着子类应该能够保持基类的行为,并且不应该破坏基类的约定。 -
接口隔离原则(Interface Segregation Principle,ISP):
不应该强迫客户端(使用接口的类)依赖于它们不需要的接口。这个原则鼓励将大型接口拆分成更小、更具体的接口,以便客户端只需实现它们所需的部分。 -
依赖倒置原则(Dependency Inversion Principle,DIP):
高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。这个原则强调通过依赖于抽象来实现解耦和灵活性。
这些原则共同构成了SOLID原则,它们的目标是指导开发者编写更加灵活、可扩展和易于维护的代码。这些原则并不是僵硬的规则,而是一种指导思想,根据实际情况和项目需求进行适当的应用和权衡。
12.附加知识:依赖倒置原则
依赖倒置原则(Dependency Inversion Principle,DIP)是SOLID设计原则中的一部分,提出了一种关于如何设计和组织软件架构的指导思想。DIP的核心思想是:
- 高层模块不应该依赖于低层模块。两者都应该依赖于抽象。
- 抽象不应该依赖于细节。细节应该依赖于抽象。
这个原则强调了以下几个关键概念:
-
高层模块:指的是较高层次的组件、模块或类,它们通常是实现业务逻辑的部分。
-
低层模块:指的是较低层次的组件、模块或类,它们通常是实现底层细节的部分,比如工具类、数据库访问等。
-
抽象:指的是接口、抽象类或其他形式的抽象层,用于定义高层和低层之间的通信接口。
-
细节:指的是具体的实现细节,通常包括具体类、方法等。
依赖倒置原则的目标是减少组件之间的紧耦合,提高系统的灵活性、可维护性和可扩展性。通过将高层模块和低层模块都依赖于抽象,可以实现以下几个好处:
-
可替换性:高层模块可以轻松地切换不同的低层实现,而不需要修改高层代码。
-
可测试性:通过依赖于抽象,可以更容易地进行单元测试,以及在测试中使用模拟对象或桩对象。
-
模块解耦:高层模块和低层模块之间的关系由抽象定义,减少了紧密的依赖关系。
-
灵活性:系统更容易适应变化,因为只需要修改抽象或新的低层实现,而不需要修改高层模块。
在之前提供的示例中,使用接口注入的方式就体现了依赖倒置原则。通过依赖于抽象的接口(或者基类),高层模块和低层模块之间的关系由抽象定义,达到了解耦和灵活性的目标。
13.一个图形绘制应用程序。我们将创建一个图形类(Shape),并通过依赖注入来注入不同的绘制器(Drawer)来绘制不同的图形。
代码
#include <iostream>
#include <string>
// 抽象绘制器接口
class Drawer {
public:
virtual void Draw(const std::string& shapeType) = 0;
};
// 具体的绘制器实现
class ConsoleDrawer : public Drawer {
public:
void Draw(const std::string& shapeType) override {
std::cout << "Drawing " << shapeType << " on console." << std::endl;
}
};
class FileDrawer : public Drawer {
public:
void Draw(const std::string& shapeType) override {
std::cout << "Drawing " << shapeType << " in file." << std::endl;
}
};
// 图形类,依赖于绘制器接口
class Shape {
public:
Shape(Drawer* drawer) : drawer_(drawer) {}
void Draw(const std::string& shapeType) {
drawer_->Draw(shapeType);
}
private:
Drawer* drawer_;
};
int main() {
ConsoleDrawer consoleDrawer;
FileDrawer fileDrawer;
Shape circle(&consoleDrawer);
Shape rectangle(&fileDrawer);
circle.Draw("circle");
rectangle.Draw("rectangle");
return 0;
}