天天看点

软件设计原则-依赖倒置

介绍

依赖倒置原则的原始定义为:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象(High level modules shouldnot depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details. Details should depend upon abstractions)。其核心思想是:要面向接口编程,不要面向实现编程。

引用自:http://c.biancheng.net/view/1326.html

好处是:

  • 依赖倒置原则可以降低类间的耦合性。
  • 依赖倒置原则可以提高系统的稳定性。
  • 依赖倒置原则可以减少并行开发引起的风险。
  • 依赖倒置原则可以提高代码的可读性和可维护性。

应用场景

对这一原则的理解,可以举个实际项目的例子。

假设需要写一个 bootloader 固件,这个固件支持从 uart, spi, iic 接口进行固件引导。

OK,花了九牛二虎之力,我把 uart, spi, iic 模块都调通了,于是在 bootloader 模块中把这几个模块都引用进来,分别调用其接口并使用判断分支分别进行通讯。代码可能如下:

// bootloader.c
#include "spi.h"
#include "iic.h"
#include "uart.h"

void init(void)
{
    spi_init();
    iic_init();
    uart_init();
}

void write(xxx)
{
    if (current_interface == UART) uart_write(xxx);
    else if (current_interface == SPI) spi_write(xxx);
    else if (current_interface == IIC) iic_write(xxx);
}
           

然后,某一天,

项目需求变更:增加一个 SDIO 接口。你微微一笑,很快在代码里新增一条判断分支,自信回头;

需求继续变更:uart 口不要了。你眉头一皱,按下“CTRL + F”全局搜索,删除了 uart 相关的代码,然后呼出一口气;

需求无穷尽也:IO 口数量有限,只要 UART 口就好了。。。你拿起手机,按下 553:“老婆,今晚加班”。

应用代码

以上例子,充分说明了“高层模块不应该依赖低层模块,应该依赖其抽象”这句话的重要性。

这里,高层模块指的就是 bootloader.c 模块,而低层模块,则是 spi.c 等这些接口驱动模块。

解决这一问题,我们可以在他们之间增加一个抽象模块:communicate.c,来分割 bootloader.c 对各个接口 .c 的直接依赖。

实现代码如下:

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <windows.h>

/* 
依赖倒置原则的示例代码
要面向接口编程,不要面向实现编程。
抽象不应该依赖细节

源码实现目标:
设计一个通讯模块,该模块属于一个 bootloader 固件。
该 bootloader 支持 uart, spi, iic 等通讯接口

抽象是:communicate.c
细节是:iic.c spi.c uart.c 等
 */

#define ERR_CODE_NO_ERROR              0
#define ERR_CODE_TIMEOUT               1
#define ERR_CODE_MEMORY                2



/* commmunicate.c/.h */

/* 对接上、下层 */
#define COMM_INTF_ID_INVALID          0
#define COMM_INTF_ID_IIC              1
#define COMM_INTF_ID_UART             2
#define COMM_INTF_ID_SPI              3

#define INTF_NUM_MAX                  3

/* 对接下层 */
typedef struct interface_s
{
    uint8_t (*init)(void);
    uint8_t (*deinit)(void);
    uint8_t (*recv)(uint8_t *p_data, uint32_t data_len);
    uint8_t (*send)(uint8_t *p_data, uint32_t data_len);
}interface_t;

struct intf_id_map_s
{
    uint8_t intf_id;
    interface_t *p_intf;
};

struct intf_id_map_s intf_id_map_table[3];

/* 对接上层 */
uint8_t comm_check_interface(uint8_t);                                              // COMM_INTF_ID_XX
uint8_t comm_recv(uint8_t *p_data, uint32_t data_len, uint32_t *actual_data_len);
uint8_t comm_send(uint8_t *p_data, uint32_t data_len, uint32_t *actual_data_len);

static uint8_t map_table_index = 0;
static uint8_t current_intf_id = COMM_INTF_ID_INVALID;
static interface_t *p_current_intf = NULL;

static char *get_intf_string(uint8_t intf_id)
{
    if (intf_id == COMM_INTF_ID_IIC)        return "IIC";
    if (intf_id == COMM_INTF_ID_SPI)        return "SPI";
    if (intf_id == COMM_INTF_ID_UART)       return "UART";
    if (intf_id == COMM_INTF_ID_INVALID)    return "INVALID";
}

uint8_t comm_init(void)
{
    map_table_index = 0;

    for (uint8_t i = 0; i < INTF_NUM_MAX; i++)
    {
        if (intf_id_map_table[i].intf_id != COMM_INTF_ID_INVALID)
        {
            map_table_index++;
        }
    }
    printf("[comm] init ok, total %d interface\r\n", map_table_index);
    return ERR_CODE_NO_ERROR;
}

uint8_t comm_check_interface(uint8_t comm_intf)
{
    bool has_check = FALSE;

    printf("[comm] start checking interface: %s\r\n", get_intf_string(comm_intf));

    for (uint8_t i = 0; i < map_table_index; i++)
    {
        if (intf_id_map_table[i].intf_id == comm_intf)
        {
            if (ERR_CODE_NO_ERROR == intf_id_map_table[i].p_intf->init())
            {
                has_check = TRUE;
                current_intf_id = comm_intf;
                p_current_intf = intf_id_map_table[i].p_intf;
                break;
            }
        }
    }

    return ERR_CODE_NO_ERROR;
}

uint8_t comm_recv(uint8_t *p_data, uint32_t data_len, uint32_t *actual_data_len)
{ 
    if (current_intf_id !=  COMM_INTF_ID_INVALID)
    {
        p_current_intf->recv(p_data, data_len);
    }
    return ERR_CODE_NO_ERROR;
}

uint8_t comm_send(uint8_t *p_data, uint32_t data_len, uint32_t *actual_data_len)
{
    if (current_intf_id !=  COMM_INTF_ID_INVALID)
    {
        p_current_intf->send(p_data, data_len);
    }
    return ERR_CODE_NO_ERROR;
}

/* spi.c */

uint8_t spi_init(void)
{
    printf("[spi] init ok!\n");
    return ERR_CODE_NO_ERROR;
}

uint8_t spi_deinit(void)
{
    printf("[spi] deinit\r\n");
    return ERR_CODE_NO_ERROR;
}

uint8_t spi_write(uint8_t *p_data, uint32_t data_len)
{
    printf("[spi] write\r\n");
    return ERR_CODE_NO_ERROR;
}

uint8_t spi_read(uint8_t *p_data, uint32_t data_len)
{
    printf("[spi] read\r\n");
    return ERR_CODE_NO_ERROR;
}

interface_t spi =
    {
        .init  = spi_init,
        .deinit = spi_deinit,
        .recv  = spi_read,
        .send  = spi_write,
    };

/* iic.c */

uint8_t iic_init(void)
{
    printf("[iic] init ok!\n");
    return ERR_CODE_NO_ERROR;
}

uint8_t iic_deinit(void)
{
    printf("[iic] deinit\r\n");
    return ERR_CODE_NO_ERROR;
}

uint8_t iic_write(uint8_t *p_data, uint32_t data_len)
{
    printf("[iic] write\r\n");
    return ERR_CODE_NO_ERROR;
}

uint8_t iic_read(uint8_t *p_data, uint32_t data_len)
{
    printf("[iic] read\r\n");
    return ERR_CODE_NO_ERROR;
}

interface_t iic =
    {
        .init  = iic_init,
        .deinit = iic_deinit,
        .recv  = iic_read,
        .send  = iic_write,
    };

/* uart.c */

uint8_t uart_init(void)
{
    printf("[uart] init ok!\n");
    return ERR_CODE_NO_ERROR;
}

uint8_t uart_deinit(void)
{
    printf("[uart] deinit\r\n");
    return ERR_CODE_NO_ERROR;
}

uint8_t uart_write(uint8_t *p_data, uint32_t data_len)
{
    printf("[uart] write\r\n");
    return ERR_CODE_NO_ERROR;
}

uint8_t uart_read(uint8_t *p_data, uint32_t data_len)
{
    printf("[uart] read\r\n");
    return ERR_CODE_NO_ERROR;
}

interface_t uart =
    {
        .init  = uart_init,
        .deinit = uart_deinit,
        .recv  = uart_read,
        .send  = uart_write,
    };

/* 打算将这个结构体放在 config.h 中,来避免 communicate.c 或者 bootloader.c 产生对各个接口.c 的依赖 */
struct intf_id_map_s intf_id_map_table[3] =
{
    {.intf_id = COMM_INTF_ID_SPI, .p_intf = &spi},
    {.intf_id = COMM_INTF_ID_IIC, .p_intf = &iic},
    {.intf_id = COMM_INTF_ID_UART, .p_intf = &uart},
};

/* bootloader.c */
int main(void)
{
    printf("hihihihi\r\n");

    comm_init();

    for (uint8_t i = 0; i < INTF_NUM_MAX; i++)
    {
        if (ERR_CODE_NO_ERROR == comm_check_interface(i+1))
        {
            comm_recv(NULL, 0, NULL);
            comm_send(NULL, 0, NULL);
        }
    }
}
           

当你的软件进行了以上设计之后,我们再来回顾之前的几个需求:

  • 需求变更1,增加 SDIO 接口。
你需要做的,只是增加一个 sdio.c 文件,将 sdio 接口调通,然后在 config.h 中对 intf_id_map_table 再赋一个值即可。
  • 需求变更2,删除 uart 接口。
删除 config.h 头文件中 intf_id_map_table 数组的一个值即可。
  • 需求变更3,只保留 uart 接口。
只给 config.h 头文件的 intf_id_map_table 数组赋一个值即可。

可见,当高层模块不再对具体实现(低层模块)产生依赖,则无论实现怎么变化,我们都可以用比较小的代价来适应新的需求。上文提及的依赖倒置的几个好处,在这里也得以体现。

  • 依赖倒置原则可以降低类间的耦合性。
  • 依赖倒置原则可以提高系统的稳定性。
  • 依赖倒置原则可以减少并行开发引起的风险。
  • 依赖倒置原则可以提高代码的可读性和可维护性。

一些细节

需要指出的是,上面的实现代码写的不够优雅。

为了不违背“抽象不依赖于细节”这一原则;我选择增加一个 config.h 头文件,来分割 communicate.c(抽象)对接口 .c (细节)的依赖。

/* 打算将这个结构体放在 config.h 中,来避免 communicate.c 或者 bootloader.c 产生对各个接口.c 的依赖 */
struct intf_id_map_s intf_id_map_table[3] =
{
    {.intf_id = COMM_INTF_ID_SPI, .p_intf = &spi},
    {.intf_id = COMM_INTF_ID_IIC, .p_intf = &iic},
    {.intf_id = COMM_INTF_ID_UART, .p_intf = &uart},
};
           

这种做法很不优雅。

在上面的示例代码中,spi 模块(细节)依赖了 communicate 模块(抽象);它在本模块内,实例化了 interface_t 接口,创建一个 spi 对象。代码如下:

interface_t spi =
    {
        .init  = spi_init,
        .deinit = spi_deinit,
        .recv  = spi_read,
        .send  = spi_write,
    };
           

之后,我必须将这个 spi 对象注册给 communicate 模块(把它赋值给 intf_id_map_table 数组),我一开始设想了两种做法:

  1. 在 communicate.h 里赋值直接赋值给 intf_id_map_table。但这种做法导致了 communicate 对 spi 产生了依赖。
  2. 在 bootloader 模块里,将 spi 送给 communicate 模块,如,comm_register(spi); 这种做法又导致了 bootloader 对 spi 产生了依赖(虽然很弱)。

这两种做法都违背了我们设计的初衷。

在没想到更好的解决方式的情况下,我选择了再开一个头文件,config.h。在里面完成赋值过程:

/* 打算将这个结构体放在 config.h 中,来避免 communicate.c 或者 bootloader.c 产生对各个接口.c 的依赖 */
struct intf_id_map_s intf_id_map_table[3] =
{
    {.intf_id = COMM_INTF_ID_SPI, .p_intf = &spi},
    {.intf_id = COMM_INTF_ID_IIC, .p_intf = &iic},
    {.intf_id = COMM_INTF_ID_UART, .p_intf = &uart},
};
/* 打算将这个结构体放在 config.h 中,来避免 communicate.c 或者 bootloader.c 产生对各个接口.c 的依赖 */
struct intf_id_map_s intf_id_map_table[3] =
{
    {.intf_id = COMM_INTF_ID_SPI, .p_intf = &spi},
    {.intf_id = COMM_INTF_ID_IIC, .p_intf = &iic},
    {.intf_id = COMM_INTF_ID_UART, .p_intf = &uart},
};
           

如此一来,依赖关系就变成了:

软件设计原则-依赖倒置

我知道一种还算优雅的方式,利用链接器的特性来扩展 C 语言的能力,将 spi 等模块注册给 communicate 模块。在此不展开讨论,有兴趣的可参考文章:

http://www.sunyouqun.com/2018/08/ble-event-callback-in-observer-pattern/

继续阅读