Можно ли рассматривать заголовочный файл C как интерфейс?

Я изучаю архитектуру из книги Роберта К. Мартина Чистая Архитектура. Одним из основных правил, подчеркиваемых в книге, является правило DIP, в котором говорится Зависимости исходного кода должны указывать только внутрь, на политики более высокого уровня . Попытка перевести это во встроенный домен предполагает 2 компонента 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 как интерфейс?

2

Решение

Я думаю, что причина, по которой вам нужен интерфейс для таймера, заключается в том, чтобы нарушать зависимости. Поскольку планировщик использует таймер, с каждым местоположением, с которым связан Scheduler.o, должен быть связан и Timer.o, если вы используете символы планировщика, которые зависят от символов таймера.

Если бы вы использовали интерфейс для Timer, никакая связь между Scheduler.o и Timer.o (или Scheduler.so с Timer.so, если хотите) не требуется и не нужна. Вы создадите экземпляр Timer во время выполнения, скорее всего, передадите его конструктору Scheduler, Timer.o будет связан с другим местом.

Теперь, почему это было бы полезно? Модульное тестирование является одним из примеров: вы можете передать класс заглушки Timer в ctor планировщика и связать его с TimerTestStub.o и т. Д. Вы можете видеть, что этот способ работы нарушает зависимости. Scheduler.o действительно требует таймера, но который не является обязательным на уровне времени создания scheduler.so, но выше. Вы передаете экземпляр Timer как аргумент ctor планировщика.

Это также очень полезно для уменьшения количества зависимостей времени сборки при использовании библиотек. Настоящая проблема начинается с создания цепочки зависимостей. Планировщик требует таймера, таймер требует класса X, класс X требует класса Y, класс Y требует класса Z …
Это может выглядеть все еще хорошо для вас, но знайте, что каждый класс может быть в другой библиотеке.
Затем вы хотите использовать планировщик, но вынуждены перетаскивать тонну настроек includepath и, вероятно, делать тонны ссылок.
Вы можете нарушать зависимости, только предоставляя функциональность планировщика, которая вам действительно нужна в его интерфейсе, конечно, вы можете использовать несколько интерфейсов.

Вы должны сделать свою собственную демонстрацию, написать 10 классов, поместить их в 10 общих библиотек, убедиться, что каждому классу требуется 3 других класса из этих 10. Теперь включите 1 из этих заголовков классов в свой main.cpp и посмотрите, что вам нужно сделать чтобы получить его построить правильно.
Теперь вам нужно подумать о нарушении этих зависимостей.

1

Другие решения

В вычислительной технике «интерфейс» — это общая граница, через которую два или более компонента или подсистемы обмениваются информацией.

Заголовочный файл на C или C ++ представляет собой текстовый файл, который содержит набор объявлений и (возможно) макросов, которые могут быть вставлены в модуль компиляции (отдельную единицу исходного кода, такого как исходный файл), и разрешить эту единицу компиляции использовать эти объявления и макросы. Другими словами #include "headerfile" в исходном файле заменяется содержимым headerfile препроцессором C или C ++ перед последующей компиляцией.

Основываясь на этих определениях, я бы не стал описывать заголовочный файл как интерфейс.

Заголовочный файл может определять типы данных, объявлять переменные и объявлять функции. Этот заголовок может содержать несколько исходных файлов, и каждый сможет использовать типы данных, переменные и функции, объявленные в этом заголовке. Один модуль компиляции может включать этот заголовок, а затем определять некоторые (или все) функции, объявленные в заголовке.

Однако типы, переменные и функции не нужно помещать в заголовочный файл. Программист, который достаточно полон решимости, может вручную скопировать объявления и макросы в каждый исходный файл, который их использует, и никогда не использовать файл заголовка. Компилятор C или C ++ не может сказать разницу — потому что все препроцессор делает текстовую подстановку.

Логическая группировка объявлений и макросов фактически представляет собой интерфейс, а не средство, с помощью которого информация об интерфейсе становится доступной для модулей компиляции. Заголовочный файл — это просто одно (необязательное) средство, с помощью которого набор объявлений и макросов может быть сделан доступным для модулей компиляции.

Конечно, заголовочный файл часто практически используется для того, чтобы избежать ошибок при использовании набора объявлений и макросов, поэтому он может облегчить управление интерфейсом, представленным этими объявлениями и макросами. Каждый блок компиляции, который #includes заголовочный файл получает тот же контент (если на него не влияют другие макросы препроцессора). Это гораздо менее подвержено ошибкам, чем программист, вручную копирующий объявления в каждый исходный файл, который нуждается в них. Это также проще в обслуживании — редактирование файла заголовка означает, что все модули компиляции могут быть перестроены и видны изменения. Принимая во внимание, что ручное обновление объявлений и макросов в каждом исходном файле может привести к ошибкам, поскольку программисты подвержены ошибкам, например, путем несогласованного редактирования объявлений между исходными файлами.

0