Абстракция вариантов кода

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

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

#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

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

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

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