Разделение реализации вариантов

Последнее обновление: 19.09.2023

В прошлой теме был определен и реализован API для создания каталога на Windows и Linux, либо в домашней папке пользователя, либо в текущем каталоге программы в зависимости от установки определенных констант. Весь проект содержал следующие файлы:

  • directoryNames.h

  • directoryNames.c

  • directorySelection.h

  • directorySelection.c

  • directoryHandling.h

  • directoryHandling.c

  • app.c

Файлы directoryNames.h и

directoryNames.c определяли соответственно API и реализацию функций для получения пути к домашней и текущей папкам:

/* Файл directoryNames.h */
// Получение пути к домашней папке пользователя
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 и directorySelection.c определяют соответственно API и реализацию функции выбора каталога:

/* Файл directorySelection.h */
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
}

Для создания новой папки определим заголовочный Файлы directoryHandling.h и directoryHandling.c определяют соответственно 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;
}

С помощью проверки констант __unix__/_WIN32 директивой #ifdef в реализации функциий определялась платформа-зависимая часть программы для Windows и Linux. Главный файл содержал только платформо-независимый код и просто использовал определенный API, ничего не зная о его реализации.

Однако в примере выше мы по прежнему вынуждены дублировать проверку констант __unix__/_WIN32 и использовать кучу конструкций #ifdef при определении вариантов кода. Это усложняет нам возможную модификацию кода, например, если мы захотим добавить поддержку еще одной платформы. Кроме того принцип открытости-закрытости (Open/Closed Principle) гласит, что для внедрения новых функций (или переноса на новую платформу) не обязательно трогать существующий код. Код должен быть открыт для таких модификаций. Однако разделение вариантов платформы с помощью операторов #ifdef требует, чтобы при внедрении новой платформы были затронуты существующие реализации, поскольку в существующую функцию необходимо поместить еще одну ветвь #ifdef.

И решением в данном случае может быть помещение каждого варианта в отдельные файлы. Причем необязательно помещать каждую функцию в отдельный файл: связанные между собой функции, предназначенные для одной платформы, можно поместить в один и тот же файл. При наличии отдельных файлов для каждой платформы можно использовать инструкции #ifdef, чтобы определить, какой код скомпилирован на конкретной платформе. Например:

/* файл someFeature.h - платформо-независимый API */
// некоторая функция, которая реализована для всех платформ
someFeature();

/* файл someFeatureWindows.c - реализация функции для Windows */
#ifdef _WIN32
someFeature()
{
  someFeatureWindows();
}
#endif

/* файл someFeatureLinux.c - реализация функции для Linux */
#ifdef __unix__
someFeature()
{
  someFeatureLinux();
}
#endif

Таким образом на уровне файла проверяется установка константа - __unix__/_WIN32. Если она определена вставляем платформо-зависимый код. Стоит отметить, что вместо использования директивы #ifdef на уровне файла можно применять другие механизмы, например, системы сборки типа Make, с помощью которых можно определять, какой код компилировать на конкретной платформе.

Итак, применим выделение вариантов в отдельные файлы. Сначала определим заголовочный файл directoryNames.h со следующим кодом:

// платформо-независимая функция для получения пути к домашней папке
void getHomeDirectory(char* dirname);

// платформо-независимая функция для получения пути к текущей папке
void getCurrentDirectory(char* dirname);

Создадим файл directoryNamesLinux.c, в котором определим реализацию этих функций для Linux:

#ifdef __unix__
  #include "directoryNames.h"
  #include <string.h>
  #include <stdio.h>
  #include <stdlib.h>

  void getHomeDirectory(char* dirname)
  {
    sprintf(dirname, "%s%s", getenv("HOME"), "/newdir/");
  }

  void getCurrentDirectory(char* dirname)
  {
    strcpy(dirname, "newdir/");
  }
#endif

И также создадим файл directoryNamesWindows.c, где определим реализацию тех же функций для Windows:

#ifdef _WIN32
  #include "directoryNames.h"
  #include <string.h>
  #include <stdio.h>
  #include <windows.h>

  void getHomeDirectory(char* dirname)
  {
    sprintf(dirname, "%s%s%s", getenv("HOMEDRIVE"), getenv("HOMEPATH"), "\newdir\");
  }

  void getCurrentDirectory(char* dirname)
  {
    strcpy(dirname, "newdir\");
  }
#endif

Таким образом, у нас есть две функции для получения пути к папкам и есть два файла - для каждой ОС, где эти функции реализованы с помощью платформо-зависимого кода.

Далее определим заголовочный файл directorySelection.h с функцией выбора папки:

void getDirectoryName(char* dirname);

В файле directorySelectionHomeDir.c определим реализацию этой функции для получения пути для домашней папки:

#ifdef HOME_DIR
  #include "directorySelection.h"
  #include "directoryNames.h"

  void getDirectoryName(char* dirname)
  {
    getHomeDirectory(dirname);
  }
#endif

А в файле directorySelectionCurrentDir.c определим реализацию этой функции для получения пути к текущей папке:

#ifdef CURRENT_DIR
  #include "directorySelection.h"
  #include "directoryNames.h"

  void getDirectoryName(char* dirname)
  {
    return getCurrentDirectory(dirname);
  }
#endif

Для хранения прототипа функции создания папки определим заголовочный файл directoryHandling.h:

void createNewDirectory(char* dirname);

В файле directoryHandlingLinux.c реализуем эту функцию для Linux:

#ifdef __unix__
  #include <sys/stat.h>

  void createNewDirectory(char* dirname)
  {
    mkdir(dirname,S_IRWXU);
  }
#endif

В файле directoryHandlingWindows.c реализуем эту функцию для Windows:

#ifdef _WIN32
  #include <windows.h>

  void createNewDirectory(char* dirname)
  {
    CreateDirectory(dirname, NULL);
  }
#endif

Таким образом, получается следующая структура файлов:

  • directoryNames.h: API функций получения путей к домашней и текущей папке

  • directoryNamesLinux.ch: реализация функций получения путей на Linux

  • directoryNamesWindows.c: реализация функций получения путей на Windows

  • directorySelection.h: API функций для выбора каталога

  • directorySelectionHomeDir.c: реализация функции для выбора домашнего каталога пользователя

  • directorySelectionCurrentDir.c: реализация функции для выбора текущего каталога

  • directoryHandling.h: API функции создания каталога

  • directoryHandlingLinux.c: реализация функции создания каталога для Linux

  • directoryHandlingWindows.c: реализация функции создания каталога для Windows

В каждом файле реализации теперь есть только один вариант кода. Такой код гораздо легче читать и понимать. Если же используются такие инструменты, как Make, то можно вообще не использовать в коде директиву #ifdef, а вместо этого можно просто определять варианты на основе файлов.

В файле основной программы, допустим, он называется app.c, используем определенный API:

#include "directorySelection.h" // API для установки пути к папке
#include "directoryHandling.h" // API для создания папки
#include <string.h>
#include <stdio.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;
}

Основной код программы вызывает функции getDirectoryName и createNewDirectory и при этом не знает, какая именно реализация будет использоваться.

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

В то же время помещение каждого варианта в отдельный файл приводит к созданию множества новых файлов. Чем больше файлов, тем сложнее становится процедура сборки и тем дольше время компиляции кода.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850