正文
大家好,我是bug菌~
今天跟大家聊聊软件设计中依赖反转这一基本原则。
在软件设计中,依赖反转原则(英文:Dependency Inversion Principle, DIP)是面向对象设计原则的一部分,旨在提高系统的灵活性和可维护性。
虽然C语言本身不是直接支持面向对象无法特性的语言,但我们仍然可以在C语言中实现类似的概念和选择思想。
一、依赖反转原则基本思想
这个原则的主要思想就两点:
高层模块不应依赖于低层模块,两者都应依赖于抽象(接口)。
抽象不应依赖于细节,细节应依赖于抽象。
这句话怎么理解呢?
以图形绘制系统为例,绘图的抽象类或者接口不依赖于具体的图形(如圆形、矩形这些细节)如何绘制。相反,具体图形的绘制类要实现绘图接口规定的方法,即细节依赖抽象。这使得系统更灵活,方便添加新图形种类。
在C语言中的实现
虽然C语言没有类和接口的概念,但可以通过函数指针来实现依赖反转。
以下是一个简单的例子:
#include <stdio.h>
// 定义一个函数指针类型,用于表示抽象行为
typedef void (*LogFunction)(const char*);
// 低层模块:具体实现
void ConsoleLogger(const char* message) {
printf("Console: %s\n", message);
}
void FileLogger(const char* message) {
// 假设将消息写入文件的代码
printf("File: %s\n", message);
}
// 高层模块
void Application(LogFunction logger) {
logger("Hello, Dependency Inversion!");
}
int main() {
// 使用控制台日志记录
Application(ConsoleLogger);
// 使用文件日志记录
Application(FileLogger);
return 0;
}
代码简单分析如下:
LogFunction:定义了一个函数指针类型,用于实现不同的日志记录策略。
ConsoleLogger 和 FileLogger:这两个函数是低层模块,负责具体的日志记录实现。
Application:这是高层模块,它依赖于LogFunction类型的抽象,而不是具体的日志实现。
二、相对复杂一点的例子
以下是一个简单的C语言示例来体现依赖反转原则:
#include <stdio.h>
// 抽象接口:定义了形状的绘制操作
typedef struct Shape Shape;
struct Shape {
void (*draw)(Shape*);
};
// 具体形状:圆形
typedef struct Circle {
Shape base;
double radius;
} Circle;
// 圆形的绘制实现
void circle_draw(Shape* shape) {
Circle* circle = (Circle*)shape;
printf("Drawing a circle with radius %.2f\n", circle->radius);
}
// 具体形状:矩形
typedef struct Rectangle {
Shape base;
double width;
double height;
} Rectangle;
// 矩形的绘制实现
void rectangle_draw(Shape* shape) {
Rectangle* rectangle = (Rectangle*)shape;
printf("Drawing a rectangle with width %.2f and height %.2f\n", rectangle->width, rectangle->height);
}
// 主函数,用于测试
int main() {
// 创建圆形对象并初始化
Circle circle;
circle.base.draw = circle_draw;
circle.radius = 5.0;
// 创建矩形对象并初始化
Rectangle rectangle;
rectangle.base.draw = rectangle_draw;
rectangle.width = 4.0;
rectangle.height = 6.0;
// 通过抽象接口调用绘制操作
Shape* shapes[2];
shapes[0] = (Shape*)&circle;
shapes[1] = (Shape*)&rectangle;
for (int i = 0; i < 2; i++) {
shapes[i]->draw(shapes[i]);
}
return 0;
}
首先定义了一个抽象的 Shape 结构体,它包含一个函数指针 draw ,代表了绘制形状的抽象操作,这相当于依赖反转原则中的抽象部分。
然后分别定义了具体的形状 Circle 和 Rectangle ,它们都包含了 Shape 结构体作为其一部分,并各自实现了对应的绘制函数( circle_draw 和 rectangle_draw ),这是细节依赖抽象的体现。
在 main 函数中,可以通过指向 Shape 结构体的指针数组来统一处理不同的具体形状对象,而不需要关心它们具体是哪种形状,只需要调用抽象接口中定义的 draw 操作即可。
这样高层模块(这里的 main 函数可以看作相对高层的模块,负责协调不同形状的绘制)不依赖于具体的形状模块(圆形或矩形的具体实现),而是依赖于抽象的 Shape 接口,符合依赖反转原则。
如果后续要添加新的形状,只需按照类似的方式定义新形状结构体并实现对应的绘制函数,使其符合 Shape 抽象接口,就可以很方便地集成到系统中。
三、总 结
通过使用函数指针,我们可以在C语言中实现依赖反转原则。这种方法使得高层模块与低层模块解耦,提高了系统的灵活性和可测试性。
灵活性
当系统的某个具体模块(细节)需要修改时,只要它所实现的抽象接口不变,依赖于这个抽象接口的其他模块几乎不需要修改。
例如,在一个物流系统中,运输方式(如陆运、水运)的具体实现细节改变,只要运输方式的抽象接口(如计算运费、预计到达时间)不变,依赖该接口的订单管理等高层模块不用修改,维护起来更方便。
可测试性增强
可以方便地使用模拟对象(Mock)来替代真实的依赖对象进行单元测试。比如在开发一个支付系统时,对于支付网关这个依赖项,在测试时可以通过模拟一个符合抽象接口规范的假支付网关,来测试支付处理模块的功能,而不用依赖真实的支付网关,使得测试更容易进行。
软件的可扩展性变好
便于添加新功能。在遵循依赖反转原则的软件架构中,添加新的功能模块(细节)只需要新模块实现现有的抽象接口就行。例如,在一个图形编辑软件中,要添加新的图形绘制功能,只要新的图形绘制类实现绘图的抽象接口,就可以方便地集成到系统中,而不会对其他模块产生较大影响。