В прошлой теме мы рассмотрели проблему вариантов программы, когда в зависимости от внешних условий/констант программа использует при компиляции тот или иной код. Возьмем код программы из прошлой статьи:
#include <string.h> #include <stdio.h> #include <stdlib.h> #ifdef __unix__ #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #elif defined _WIN32 #include <windows.h> #endif // если выбрана домашняя папка пользователя // устанавливаем пути в зависимости от ОС void getHomeDirectory(char* dirname) { #ifdef __unix__ sprintf(dirname, "%s%s", getenv("HOME"), "/newdir/"); #elif defined _WIN32 sprintf(dirname, "%s%s%s", getenv("HOMEDRIVE"), getenv("HOMEPATH"), "\newdir\"); #endif } // если выбрана текущая папка приложения // устанавливаем пути в зависимости от ОС void getCurrentDirectory(char* dirname) { #ifdef __unix__ strcpy(dirname, "newdir/"); #elif defined _WIN32 strcpy(dirname, "newdir\"); #endif } // определение пути к новой папке void getDirectoryName(char* dirname) { #ifdef HOME_DIR getHomeDirectory(dirname); #elif defined CURRENT_DIR getCurrentDirectory(dirname); #endif } // создание новой папки в зависимости от определенной ОС void createNewDirectory(char* dirname) { #ifdef __unix__ mkdir(dirname,S_IRWXU); #elif defined _WIN32 CreateDirectory (dirname, NULL); #endif } int main(void) { char dirname[60]; char filename[60]; char* text = "Hello METANIT.COM"; getDirectoryName(dirname); // получаем путь к папке вне зависимости от ОС createNewDirectory(dirname); // создание папки sprintf(filename, "%s%s", dirname, "newfile"); FILE* f = fopen(filename, "w+"); fwrite(text, 1, strlen(text), f); // запись текста в новый файл fclose(f); return 0; }
Данная программа записывает в файл некоторый текст вне зависимости от операционной системы. В зависимости от установки констант HOME_DIR/CURRENT_DIR мы устанавливаем в качестве каталога для записи в файл либо домашнюю папку пользователя,
либо текщий каталог. Однако в зависимости от операционной системы пути к каталогам будут отличаться. И чтобы определить текущую систему, проверяем константы __unix__
и
_WIN32
.
Здесь работа с вариантами вынесена в отдельные функции, что упрощает поддержку программы. Тем не менее файл программы по прежнему работает с платформо-специфичным кодом, и платформо-зависимыая и платформо-независимая логика смешиваются в одном файле. Кроме того, платформо-зависимые функции могут потребоваться в других частях кодовой базы, что потребует их повторной реализации.
Решением данной проблемы может быть определение отдельного API для работы с платформо-зависимым кодом. В заголовочных файлах определяются только независимые от платформы функции. Эти функции вызываются клиентом-внешний кодом. А в реализацию функций помещается платформо-зависимый код с проверкой платформы с помощью #ifdef. То есть создается уровень абстракции, через который внешний код обращается к функциям и не знает, что они скрывают платформо-зависимую реализацию. В общем виде это выглядит следующим образом:
/* Код someFeature.h - API с прототипами платформо-независимых функций */ void someFeature(); /* Код someFeature.c - реализация API с использованием платформо-зависимой логики */ void someFeature() { #ifdef PLATFORM_A performFeaturePlatformA(); #elif defined PLATFORM_B performFeaturePlatformB(); #endif } /* Клиент обращается к платформо-независимым функциям API */ #include "someFeature.h" int main(void) { someFeature(); return 0; }
Итак, изменим программу, выделив код, который работает с вариантами в отдельные файлы. Сначала определим файл directoryNames.h с API для получения путь к папкам:
// Получение пути к домашней папке пользователя void getHomeDirectory(char* dirname); // получение пути к текущему каталогу void getCurrentDirectory(char* dirname);
А в файле directoryNames.c определим реализацию этих функций:
#include "directoryNames.h" #include <stdio.h> #include <stdlib.h> #include <string.h> // получаем путь к домашнему каталогу в зависимости от системы void getHomeDirectory(char* dirname) { #ifdef __unix__ sprintf(dirname, "%s%s", getenv("HOME"), "/newdir/"); #elif defined _WIN32 sprintf(dirname, "%s%s%s", getenv("HOMEDRIVE"), getenv("HOMEPATH"),"\newdir\"); #endif } // получаем путь к текущему каталогу в зависимости от системы void getCurrentDirectory(char* dirname) { #ifdef __unix__ strcpy(dirname, "newdir/"); #elif defined _WIN32 strcpy(dirname, "newdir\"); #endif }
Далее определим файл directorySelection.h c API для выбора каталога:
void getDirectoryName(char* dirname);
В файле directorySelection.c определим реализацию этой функции:
#include "directorySelection.h" #include "directoryNames.h" void getDirectoryName(char* dirname) { #ifdef HOME_DIR getHomeDirectory(dirname); #elif defined CURRENT_DIR getCurrentDirectory(dirname); #endif }
Для API создания новой папки определим заголовочный файл directoryHandling.h:
void createNewDirectory(char* dirname);
В файле directoryHandling.c определим реализацию:
#include "directoryHandling.h" #ifdef __unix__ #include <sys/stat.h> #elif defined _WIN32 #include <windows.h> #endif void createNewDirectory(char* dirname) { #ifdef __unix__ mkdir(dirname,S_IRWXU); #elif defined _WIN32 CreateDirectory (dirname, NULL); #endif }
В главном файле программы, допустим, он будет называться app.c используем выше определенные API:
#include <stdio.h> #include <string.h> #include "directorySelection.h" #include "directoryHandling.h" int main(void) { char dirname[60]; char filename[60]; char* text = "Hello METANIT.COM"; getDirectoryName(dirname); createNewDirectory(dirname); sprintf(filename, "%s%s", dirname, "newfile"); FILE* f = fopen(filename, "w+"); fwrite(text, 1, strlen(text), f); fclose(f); return 0; }
То есть в итоге у нас получится следующий набор файлов:
directoryNames.h
directoryNames.c
directorySelection.h
directorySelection.c
directoryHandling.h
directoryHandling.c
app.c
Таким образом, клиент - файл с основной логикой программы полностью независим от операционной системы. В него даже не подключены заголовочные файлы, специфичные для операционных систем. Разделение файлов реализации с помощью уровня абстракции упрощает понимание файлов и позволяет повторно использовать функции в других частях кода. Кроме того, разработку, обслуживание и тестирование можно разделить на платформо-зависимый и платформо-независимый код.
Тем не менее мы вынуждены дублировать код проверки операционной системы в разных функциях и при подлючении заголовочных файлов. И если, к примеру, нам потребуется добавить поддержку еще одной системы, то нам придется изменять все эти места.