Модули, API и управление состоянием

Создание API модуля

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

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

Частая встречающаяся проблема представляет дублирование глобальных идентификторов в разных файлах. Например, пусть у нас есть файл (модуль) user.c:

#include <stdio.h>

// условные данные
char* username = "tom@smail.com";

void printUserName()
{
    printf("UserName: %s\n", username);
}
void resetUserName()
{
    username = "Undefined";
}

Здесь определена глобальная переменная username и две функции для ее вывода на консоль и сброса в некоторое стандартное значение. И допустим, у нас есть основной файл программы app.c, который использует модуль "user.c":

#include <stdio.h>

char* username = "Tom";

extern void resetUserName();
void printUserName()
{
    printf("Name: %s\n", username);
}

int main(void)
{
  printUserName();
  resetUserName();
}

Здесь определены глобальная переменная и функция с теми же именами, что и в модуле "user.c", но немного иным содержимым. Попробуем скомпилировать оба файла, например, с помощью gcc

gcc -Wall -pedantic app.c user.c -o app

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

Вторая проблема касается несанкционированного доступа к функционалу. Так, в коде "app.c" мы вызываем функцию resetUserName для сброса имени. Однако здесь мы можем задаться вопросом, а должен ли внешний код иметь возможность выполнять эту функцию? В зависимости от задачи и ситуации ответ может быть разным. Но, допустим, мы НЕ хотим, чтобы внешний код видел эти данные. Особенно если модуль "user.c" создаем мы, а использует его какое-то внешнее приложение, где мы вообще можем не знать, в каком случае эта функция будет вызываться.

Обе проблемы ставят нас перед задачей инкапсуляции функционала в модуле и предоставления внешнему коду доступа только к определенным ограниченным возможностям модуля.

Определение API

В общем случае при определении модуля прежде всего определяется API (Application Program Interface) - тот функционал, который будет виден извне и который может использоваться во внешнем коде. Объявления функций и типов, которые входят в API, выносятся в заголовочный файл, для его подключения во внешнем коде. Остальной функционал, который не попадает в API, определяется с модификатором static, что позволяет сделать этот функционал доступным только в рамках файла определения.

Например, нам надо определить модуль для вывода текстового файла на консоль. Вначале определим API и для этого создадим следующий файл file_printer.h:

// функция вывода файла на консоль
void printFile(char*);

API довольно простое - определяем функцию printFile, которая принимает имя файла.

Далее определим модуль поиска в файле - файл file_printer.c, где будет располагаться реализация вывода файла на консоль:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include "file_printer.h"

// получаем длину файла
// функция не входит в API, поэтому определена со словом static
static int getFileLength(FILE* fp)
{
    fseek(fp, 0, SEEK_END);
    int file_length = ftell(fp);
    return file_length;
}
// считываем файл в buffer
// функция не входит в API, поэтому определена со словом static
static void readFile(FILE* fp, char* buffer, int file_length)
{
    fseek(fp, 0, SEEK_SET);
    int read_elements = fread(buffer, 1, file_length, fp);
    buffer[read_elements] = '\0';   // устанавливем концевой нулевой байт для строки
}
// вывод файла на консоль
// Функция printFile входит в API, поэтому определена без слова static
void printFile(char* filename)
{
    FILE* fp = 0; // инициализируем нулем
    char* text = 0;         // инициализируем нулем 
 
    assert(filename && "Invalid file name"); // если некорректное имя файла, прерываем программу
 
    fp = fopen(filename, "r");  // открываем файл
    assert(fp && "Unable to open file!\n"); // если не удалось открыть файл, прерываем программу
 
    int file_length = getFileLength(fp);  // получаем длину файла
    if(file_length > 0)
    {
        text = malloc(file_length);
        if(text)    // если удалось выделять память
        {
            readFile(fp, text, file_length);  // считываем файл
            printf("%s\n",text);
 
            free(text); // освобождаем память
            text = 0;   // сбрасываем значение указателя в ноль
        }
    }
    fclose(fp);     // закрываем файл
    fp = 0;         // сбрасываем значение указателя в ноль
}

Здесь определена реализация функции printFile для вывода файла на консоль. Однако в своей сущности она использует две другие функции. Так, чтобы вывести файл на консоль, нам надо сначала получить его длину с помощью функции getFileLength и затем считать в буфер полученной длины файл с помощью функции readFile. Использование этих функций в отрыве от функции printFile не имеет смысла. Внешний код не должен к ним обращаться, поэтому они не входят в API и определены с помощью ключевого слова static

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

#include <stdio.h>
#include "file_printer.h"

int main(void)
{
    char* filename ="test.txt";
    printFile(filename);
}

Для использования API подключаем его определение в файле "file_printer.h". Таким образом, для внешнего кода - файла "app.c" существует только функция printFile, а ее реализация для него не важны.

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

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