介绍
依赖倒置原则的原始定义为:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象(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 数组),我一开始设想了两种做法:
- 在 communicate.h 里赋值直接赋值给 intf_id_map_table。但这种做法导致了 communicate 对 spi 产生了依赖。
- 在 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/