我正在从Robert C. Martin的书“清洁建筑”中学习建筑学。本书强调的主要规则之一是DIP规则,该规则规定源代码依赖关系必须仅指向更高级别的策略。试图在嵌入式域中翻译它假设有两个组件scheduler
和timer
。调度程序是高级策略,依赖于低级别的计时器驱动程序,需要调用API get_current_time()
和set_timeout()
,我只是将模块拆分为实现文件timer.c
和标题(接口?)timer.h
和scheduler.c
可以简单包括timer.h
使用这些API。阅读本书将前一个场景描述为违反依赖关系规则,并暗示应实现两个组件之间的接口以打破依赖关系。
要在c中模仿,例如timer_abstract
可以包含一个通用结构,指向函数struct timer_drv {
uint32 (*get_current_time)(void);
void (*set_timeout)(uint32 t);
}
这对我来说看起来像过度设计。是不是一个简单的头文件足够?可以将C头文件视为接口吗?
我认为你想要一个定时器接口的原因确实是为了打破依赖关系。由于Scheduler使用Timer,因此每个位置Scheduler.o都链接到,如果您使用依赖于计时器符号的调度程序符号,Timer.o也必须链接到。
如果您使用了Timer的接口,则不需要从Scheduler.o到Timer.o(或Scheduler.so,如果需要,也可以是Timer.so)的链接。您将在运行时创建一个Timer实例,可能会将其传递给Scheduler的构造函数,Timer.o将链接到其他地方。
现在为什么这会有用?单元测试就是一个例子:您可以将Timer存根类传递给Scheduler的ctor并链接到TimerTestStub.o等。您可以看到这种工作方式确实会破坏依赖关系。 Scheduler.o确实需要一个Timer,但是在scheduler.so的构建时间级别不需要哪个,但是更高。您将Timer实例作为Scheduler的ctor的参数传递。
这对于在使用库时降低构建时依赖性的数量也非常有用。真正的麻烦在创建依赖链时开始。调度程序需要Timer,Timer需要X类,X类需要Y类,Y类需要Z类...这看起来仍然可以,但是知道每个类都可以在另一个库中。然后你想使用Scheduler但是被迫拖动大量的includepath设置并且可能会进行大量的链接。您可以通过仅在其界面中公开您真正需要的Scheduler功能来打破依赖关系,当然您可以使用多个接口。
您应该制作自己的演示,编写10个类,将它们放在10个共享库中,确保每个类需要其他3个类。现在在main.cpp中包含1个类标题,看看你需要做什么让它正确构建。现在你需要考虑打破这些依赖关系。
在计算中,“接口”是两个或更多个组件或子系统交换信息的公共边界。
C或C ++中的头文件是一个文本文件,其中包含一组声明和(可能)宏,可以插入到编译单元(源代码的单独单元,如源文件)中,并允许该编译单元使用这些声明和宏。换句话说,在后续编译之前,C或C ++预处理器将源文件中的#include "headerfile"
替换为headerfile
的内容。
根据这些定义,我不会将头文件描述为接口。
头文件可以定义数据类型,声明变量和声明函数。多个源文件可能包含该标头,并且每个文件都可以使用该标头中声明的数据类型,变量和函数。一个编译单元可以包括该头,然后定义头中声明的一些(或所有)函数。
但是,不需要将类型,变量和函数放在头文件中。一个足够坚定的程序员可以手动将声明和宏复制到每个使用它们的源文件中,并且永远不会使用头文件。 C或C ++编译器无法区分 - 因为所有预处理器都是文本替换。
声明和宏的逻辑分组实际上是代表接口的,而不是有关接口的信息可用于编译单元的方法。头文件只是一个(可选)方法,通过它可以使一组声明和宏可用于编译单元。
当然,头文件通常用于避免使用一组声明和宏时出错 - 因此可以帮助更轻松地管理由这些声明和宏表示的接口。 #include
s头文件的每个编译单元都接收相同的内容(除非受其他预处理器宏的影响)。与程序员手动将声明复制到需要它们的每个源文件相比,这更不容易出错。它也更容易维护 - 编辑头文件意味着可以重建所有编译单元并可以看到更改。然而,手动将声明和宏更新到每个源文件中可能会引入错误,因为程序员容易出错 - 例如,通过编辑源文件之间不一致的声明。