Иногда необходимо предоставить множеству клиентов/потоков доступ к одному и тому же состоянию, которое зависит от клиента. То есть каждый клиент может создавать объект состояния. Однако клиенты также могут работать с уже имеющимся состоянием, которое созадали другие клиенты.
В этом случае решением будет хранение состояния внутри модуля в виде списка объектов (поскольку каждый клиент может создавать свой объет состояния). Модуль также предоставляет клиенту некоторый API - набор функций для работы с состоянием. Клиент создает новый объект состояния или получает уже имеющийся и передает его в функции API для работы с ним. Причем при создании объекта состояния клиент должен предоставить некоторый идентификатор, который сохраняется в объекте состояния. Благодаря идентификатору можно разграничить данные объект состояния от других и проверить его наличие. Если объект с идентификатором уже есть, то клиент просто получает уже имеющийся объект. Если объект с идентификатором отсутствует, то он создается.
Формально паттерн можно представить следующим образом:
/* API модуля (заголовочный файл) */ // структура, которая представляет состояние struct INSTANCE { int x; int y; }; // некоторый набор идентификаторов для объектов состояния #define INSTANCE_TYPE_A 1 #define INSTANCE_TYPE_B 2 #define INSTANCE_TYPE_C 3 // Получем объект состояния, для этого в функцию передаем идентификатор объекта struct INSTANCE* openInstance(int id); // некоторая работа с состоянием void operateOnInstance(struct INSTANCE* inst); // освобождаем обхект void closeInstance(struct INSTANCE* inst); /* Реализация в модуле*/ // максимальное число объектов состояния #define MAX_INSTANCES 4 struct INSTANCELIST { struct INSTANCE* inst; int count; }; // список, который хранит все объекты состояния static struct INSTANCELIST list[MAX_INSTANCES]; // создаем объект состояния struct INSTANCE* openInstance(int id) { if(list[id].count == 0) { list[id].inst = malloc(sizeof(struct INSTANCE)); } list[id].count++; return list[id].inst; } void operateOnInstance(struct INSTANCE* inst) { // некоторая работа с состоянием } // получаем состояние по id static int getInstanceId(struct INSTANCE* inst) { int i; for(i=0; i<MAX_INSTANCES; i++) { if(inst == list[i].inst) { break; } } return i; } // удаляем состояние void closeInstance(struct INSTANCE* inst) { int id = getInstanceId(inst); list[id].count--; if(list[id].count == 0) { free(inst); } } /* Возможные клиенты, которые работают с состоянием */ // клиент 1 struct INSTANCE* inst = openInstance(INSTANCE_TYPE_B); // получаем объект operateOnInstance(inst); // работа с объектом closeInstance(inst); // освобождение объекта // клиент 2 struct INSTANCE* inst = openInstance(INSTANCE_TYPE_B); // получаем объект operateOnInstance(inst); // работа с объектом closeInstance(inst); // освобождение объекта
Клиент получает объект в указатель INSTANCE, вызывая фукцию openInstance. Если объект INSTANCE не существует, то он создается. Если существует, то возвращается. Причем функция фукцию openInstance также увеличивает счетчик клиентов, которые в текущий момент обращаются к объекту состояния. Получив объект INSTANCE, для работы с ним клиент передает его в функции API. Завершив работу, клиент удаляетобъект с помощью вызова функции closeInstance. Функция closeInstance учитывает количество обративнихся к этому объекту клиентов. И если клиентов, которые работают с этим объектом уже нет, то объект удаляется.
Таким образом, множество клиентов могут одновременно обращаться к единому разделяемому состоянию. Такой паттерн еще называют Shared Instance
Примером применения паттерна являются функции для работы с файлами в stdio.h. Так, функция fopen открывает файл, а функция fclose() закрывает файл. Между вызовами этих функцию могут вызываться функции для работы с файлом, в частности, запись или чтение файла.
Рассмотрим небольшой пример применения. Допустим, нам надо создать программу для работы с базами данных. Поскольку создание подключения к базу данных и последующее его закрытие может быть затратно в плане производительности, и в то же время с одной базой данных могут одновременно работать несколько клиентов, то в этой ситуации мы как раз можем применить данный паттерн. Для начала создадим заголовочный файл database.h и определим в нем API:
struct Database; // условная база данных struct Database* openDatabase(size_t id); // открытие базы данных void closeDatabase(struct Database* db); // закрытие базы данных void setUser(struct Database* db, size_t index, char* name); // изменение данных void printUsers(struct Database* db); // получение данных
Здесь структура Database представляет условную базу данных - состояние модуля, которое зависит от клиента и которое может использоваться несколькими клиентами. Причем детали реализации этого состояния неизвестны, они скрыты от клиента, и клиент не знает, какие именно поля содержит структура Database.
Для работы с состоянием определены 4 функции. Функция openDatabase получает идентификатор базы данных (индекс в пуле подключений) и возращает объект Database, который затем используется клиентом. Для простоты здесь базы данных идентифицируются по числовым идентификатором, но в реальности можно было бы передавать и имя базы данных.
Функция closeDatabase получает объект Database и закрывает его. И также для непосредственной работы с базой данных определена функция setUser, которая получает объект базы данных, индекс (id) изменяемых данных и новое значение данных. И также определена функция printUsers, которая будет получать все данные и выводит их на консоль.
Далее создадим файл database.c и определим в нем реализацию API модуля:
#include <stdio.h> #include <stdlib.h> #include <assert.h> #include "database.h" #define DATA_MAX_COUNT 5 // максимальное количество данных в условной базе данных #define CON_MAX_COUNT 4 // максимальное количество подключений // база данных - структура представляет разделяемое состояние struct Database{ size_t id; // индекс подключения char* data[DATA_MAX_COUNT];// условные данные }; // подключение - структура для управления состоянием struct Connection { struct Database* db; // подключение size_t count; // количество обратившийся клиентов }; // пул подключений static struct Connection pool[CON_MAX_COUNT]; // открытие условной базы данных struct Database* openDatabase(size_t id) { // проверяем индекс базы данных assert(id<CON_MAX_COUNT && "Invalid database id"); // если еще нет клиентов, которые обращаются к бд // значит база данных отсутствует, и ее надо создать if(pool[id].count==0) { pool[id].db = malloc(sizeof(struct Database)); // создаем базу данных pool[id].db->id = id; // устанавливаем индекс подключения } pool[id].count++; // увеличиваем счетчик клиентов return pool[id].db; // возвращаем указатель на базу данных } // удаляем базу данных void closeDatabase(struct Database* db) { pool[db->id].count--; // уменьшаем счетчик клиентов if(pool[db->id].count == 0) // если клиентов больше нет { free(db); // то освобождаем память } } // изменяем данные void setUser(struct Database* db, size_t index, char* name) { if(index < DATA_MAX_COUNT) db->data[index]=name; else printf("User not found\n"); } // выводим данные на консоль void printUsers(struct Database* db) { for(size_t i =0; i < DATA_MAX_COUNT; i++) { printf("%s\n",db->data[i]); } }
Разделяемое состояние - структура Database состоит из числового идентификатора и массива data, который собственно представляет данные - некоторый набор строк.
Для управления состоянием определена дополнительная структура - Connection или условно подключение к базе данных, которая хранит указатель на объект Database и количество клиентов, которые в текущий момент работают с базой данных. И также для управления состоянием определен условный пул подключений - массив структур Connection.
Реализация функции openDatabase получает идентификатор базы данных, в качестве которого для простоты будет служить индекс соответствующего подключения в пуле подключений. Если база данных с переданным идентификатором еще не создана (то есть нет обращающихся к ней клиентов), то она создается. Иначе функция возвращает уже ранее созданный объект Database. И также увеличивается счетчик работающих в базой данных клиентов.
Функция closeDatabase уменьшает счетчик клиентов, и если клиентов больше нет, освобождает память объекта Database.
Функция setUser изменяет значение одного объекта в базе данных по индексу (Для простоты здесь просто присваиваем строку и не учитываем, что присваиваемая строка может быть выделена в динамической памяти)
Функция printUsers выводит все данные из базы данных на консоль.
Таким образом, грубо говоря, при первом обращении к базе данных создается подключение к базе данных, выделяется память. При последующих обращениях, чтобы из пула подключений используется уже ранее открытое подключение. Если все клиенты завершили работу, то закрываем подключение - освобождаем память.
В данном случае идентификаторы (названия баз данных) для простоты жестко привязаны к индексам структур Connection в массиве pool. Но при желании можно создавать структуры Database с независимыми идентификаторами с дополнительной логикой поиска подключений к определенным базам данных в пуле подключений.
Для тестирования модуля database.c определим следующий файл app.c:
#include <stdio.h> #include "database.h" int main(void) { // условный 1-й клиент создает базу данных с id 2 struct Database* db1 = openDatabase(2); // 1-й клиент открывает базу данных // устанавливаем данные setUser(db1, 0, "Tom"); setUser(db1, 1, "Bob"); setUser(db1, 2, "Sam"); printf("Users List for 1 client\n"); printUsers(db1); // условный 2-й клиент создает базу данных с id 2 struct Database* db2 = openDatabase(2);// 2-й клиент открывает базу данных setUser(db2, 3, "Alice"); printf("\nUsers List for 2 client\n"); printUsers(db2); closeDatabase(db1); // 1-й клиент закрывает базу данных closeDatabase(db2); // 2-й клиент закрывает базу данных }
Здесь условно определены два клиента. Первый клиент работает с переменной db1, а второй клиент - с переменной db2. Каждый клиент сначала вызывает функцию openDatabase. Причем оба клиента обращаются к базе данных с одним и тем же идентификатором - 2. Однако в первом случае эта функция создаст новый объект Database, а во втором случае вернет уже существующий.
struct Database* db1 = openDatabase(2); // 1-й клиент открывает бд - создается новая БД // ....................... // условный 2-й клиент создает базу данных с id 2 struct Database* db2 = openDatabase(2);// 2-й клиент открывает бд - возвращается уже существующая БД
После того, как каждый клиент получил объект Database, идет некоторая работа. Первый клиент устанавливает первые три объекта в базе данных и выводит список объектов на консоль. Второй клиент устанавливает один объект в базе данных и также выводит список объектов на консоль. Но так как они работают с одной базой данных, то и список объектов для них будет одним и тем же.
После завершения работы клиенты должны закрыть базу данных с помощью функции closeDatabase:
closeDatabase(db1); // 1-й клиент закрывает базу данных closeDatabase(db2); // 2-й клиент закрывает базу данных - память освобождается
Но в первом случае функция closeDatabase просто уменьшает счетчик клиентов, а во втором освобождает память. После этого подключение закрыто.
Пример компиляции и работы программы:
c:\C>gcc -Wall -pedantic app.c database.c -o app & app Users List for 1 client Tom Bob Sam Users List for 2 client Tom Bob Sam Alice c:\C>