Модульность
С ростом вычислительных мощностей росла и сложность программ. Например, ядро операционной системы Linux написано на языке Си. Версия ядра 4.1 (2015 год) включает в себя 19,5 миллионов строк кода. Как можно догадаться, Линус Торвальдс пишет его не один, а ядро операционной системы заключено не в одном файле.
Разбиение программы на модули позволило существенно увеличить сложность программ и дала возможность работать над одной программой нескольким разработчикам.
Модуль — это законченный, автономный (в идеале) кусок программного кода, реализующий некоторую функциональность и предоставляющий интерфейс к ней.
Откомпилировав часть программы отдельно, можно значительно сократить время на компиляцию и сборку всей программы: если в проекте из 100 модулей было изменено только 2 (условно), то зачем компилировать остальные 98?
Драйвер — хороший пример модуля. Допустим, в некотором гипотетическом устройстве используется датчик температуры DS18B20 и дисплей с контроллером ILI9341, предназначенный для отображения температуры. Программист А берется за реализацию драйвера температурного датчика, предоставляя две функции в качестве интерфейса: инициализации и запроса температуры. Программист Б реализует драйвер управления дисплеем, предоставляя в качестве интерфейса функцию инициализации контроллера и функцию вывода текста на экран. Всё, что остается программисту В — взять эти файлы, подключить их к своему проекту и дописать основную программу, которая будет работать с этими устройствами.
В языке Си модуль обычно состоит из двух частей — файла с исходным кодом и заголовочного файла. Для датчика потребуются файлы: ds18b20.c
, ds18b20.h
. Аналогично и для контроллера дисплея. Если модуль достаточно сложный и большой по объему, имеет смысл добавить файл с окончанием _conf
для его настройки, где могут храниться макросы с указанием используемых портов ввода-вывода и другой используемой периферии. Для удобства портирования всю аппаратно-зависимую часть можно вынести в файлы _port.c
/ _port.h
. Если модули были написаны правильно, то вся необходимая информация для программиста будет содержаться в заголовочных файлах (интерфейс модуля), а детали реализации будут скрыты в файлах исходного кода.
Негласное правило
В целях предотвращения коллизий в именах, все публичные идентификаторы (прототипы функций, типы, переменные и т.д.) должны начинаться с имени модуля. Например, функцию инициализации дисплея следует назвать
ili9341_init()
, а не простоinit()
и вероятно неlcd_init()
, так как в другом проекте вы можете использовать другой драйвер.
Чтобы получше разобраться в модульности, стоит посмотреть, как выглядит процесс компиляции. Некоторые сущности будут даны без пояснений и раскрыты позже по тексту.