嵌入式软件设计中的依赖反转原则

文摘   2024-11-09 17:43   湖南  


正文


大家好,我是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)来替代真实的依赖对象进行单元测试。比如在开发一个支付系统时,对于支付网关这个依赖项,在测试时可以通过模拟一个符合抽象接口规范的假支付网关,来测试支付处理模块的功能,而不用依赖真实的支付网关,使得测试更容易进行。

软件的可扩展性变好


便于添加新功能。在遵循依赖反转原则的软件架构中,添加新的功能模块(细节)只需要新模块实现现有的抽象接口就行。例如,在一个图形编辑软件中,要添加新的图形绘制功能,只要新的图形绘制类实现绘图的抽象接口,就可以方便地集成到系统中,而不会对其他模块产生较大影响。


最后

      好了,今天就跟大家分享这么多了,如果你觉得有所收获,一定记得点个~

永久、免费分享嵌入式技术知识平台~

推荐专辑  点击蓝色字体即可跳转
☞  MCU进阶专辑 
☞  嵌入式C语言进阶专辑 
☞  “bug说”专辑 
☞ 专辑|Linux应用程序编程大全
☞ 专辑|学点网络知识
☞ 专辑|手撕C语言
☞ 专辑|手撕C++语言
☞ 专辑|经验分享
☞ 专辑|电能控制技术
☞ 专辑 | 从单片机到Linux

最后一个bug
一个嵌入式技术进阶公众号,定期分享C语言,C++、MCU(如stm32等)、DSP、ARM、嵌入式Linux等“独门”软件设计技巧和知识归纳总结,同时分享应用程序设计、物联网、滤波及控制算法推导和仿真设计等嵌入式硬核知识技巧!欢迎大家关注!
 最新文章