Я вижу лишь одну причину использовать ООП в Си - это желание иметь один интерфейс для по-разному реализованных сущностей-объектов, в том числе и не написанных на момент создания кода, использующего этот единый интерфейс. Эта ситуация часто возникает при создании библиотек для организации обратной связи либо для однообразного обращения к разнородным источникам данных. Для решения этой задачи достаточно подхода Абстрактный Интерфейс — Реализация, который может быть легко реализован в чистом Си с помощью функциональных переменных и возможности использования указателей на структуры, без предварительного указания их содержимого.
Объявим интерфейс объекта в object.h
:
typedef struct Object_ClosedRealization Object;
typedef struct Object_ContextRealization ObjectContext;
С указателем на Object
будет взаимодействовать клиент интерфейса. ObjectContext
нужен реализации интерфейса для передачи контекста объекта в его методы; переменную этого типа можно воспринимать как аналог this
во многих ООП — языках.
Добавим объявление функции создания экземпляра объекта и его методы:
extern Object* objectCreate(
ObjectContext **context, int contextSize,
int method(ObjectContext *c, int a),
void release(ObjectContext *c));
extern int objectMethod(Object *o, int a);
extern void objectRelease(Object **o);
Функция objectCreate
нужна только при реализации интерфейса, функции objectMethod
и objectRelease
будут использованы клиентским кодом.
Создадим object.c
, нужный для поддержки интерфейса объекта во время исполнения:
/* В месте этого объявления содержимое структуры увязывается с ранее
объявленным Object
. При этом, содержимое доступно только в object.c
*/
struct Object_ClosedRealization {
int (*method)(ObjectContext *c, int a);
void (*release)(ObjectContext *c);
ObjectContext *context;
};
extern Object* objectCreate(
ObjectContext **c, int contextSize,
int method(ObjectContext *c, int a),
void release(ObjectContext *c))
{
Object *o;
o = (Object *)malloc(sizeof(Object) + contextSize);
o->method = method;
o->release = release;
o->context = (ObjectContext *)(sizeof(Object) + (long unsigned)o);
*c = o->context;
return o;
}
extern int objectMethod(Object *o, int a) {
return o->method(o->context, a);
}
extern void objectRelease(Object **o) {
if (NULL != *o) {
(*o)->release((*o)->context);
free(*o);
*o = NULL;
}
}
Как видно из реализации objectMethod
, можно было бы обойтись и вызовом метода в виде o->method(o->context, a)
без введения специальной функции, но тогда бы пришлось открывать полный доступ ко внутренней структуре для пользователя интерфейса, что нежелательно по многим причинам.
Создадим парочку реализаций интерфейса.
object-mult.c
:
/* Тело структуры опять объявлено внутри Си-файла, поэтому её содержимое скрыто от стороннего кода */
struct Object_ContextRealization {
int mult;
};
/* Методы должны быть объявлены как static
, чтобы не мешаться в
общей области видимости. Связываются они всё равно через указатели */
static int method(ObjectContext *this, int a) {
printf("%d * %d", a, this->mult);
return a * this->mult;
}
static void release(ObjectContext *this) {
printf("object-mult %d released\n", this->mult);
}
/* в ООП терминах функция objectMultCreate
наиболее близка к
фабрике типов, но не конструктору */
extern Object* objectMultCreate(int mult) {
Object *o;
ObjectContext *c;
/* связывание методов с объектом через функциональные переменные*/
o = objectCreate(&c, sizeof(ObjectContext), method, release);
c->mult = mult;
return o;
}
Создадим аналогичный файл object-add.c
, в котором умножение заменим на сложение. Его содержимое мы пропустим, так как его отличия от object-mult.c
тривиальны.
Теперь напишем код main.c
, который всё это использует:
extern int main(void) {
Object *a[4];
int i, j;
/* создадим комбинацию из 2-х реализаций с 2-мя контекстами */
a[0] = objectMultCreate(1);
a[1] = objectAddCreate(1);
a[2] = objectAddCreate(2);
a[3] = objectMultCreate(2);
for (i = 0; i < sizeof(a) / sizeof(a[0]); ++i) {
printf("%d) ", i);
/* вызов одной и той же функции-метода с одинаковым
параметром для разных указателей-объектов */
j = objectMethod(a[i], 3);
printf(" = %d\n", j);
}
for (i = 0; i < sizeof(a) / sizeof(a[0]); ++i) {
objectRelease(a + i);
}
return 0;
}
Результат работы программы — примера:
0) 3 * 1 = 3
1) 3 + 1 = 4
2) 3 + 2 = 5
3) 3 * 2 = 6
object-mult(1) released
object-add(1) released
object-add(2) released
object-mult(2) released
Итак, что же получилось:
- Простейшее наследование объектов Интерфейс — Реализация.
- Настоящее динамическое ООП — более одной реализации одного интерфейса могут сосуществовать в одной области видимости одновременно.
- Содержимое объектов недоступно извне в обход методов.
- Типизированные указатели объектов, что даёт отслеживание компилятором ошибки присваиваний между разными интерфейсами.
- Чистый Си без дополнительных библиотек и магии макросов.
- Довольно объёмно по количеству кода и непривычный для ООП вызов методов за счёт необходимости явного выписывания того, что в ООП-языках подразумевается неявно.
Полный исходный код примера доступен на github.
Три года назад я сравнивал производительность такого подхода со встроенными возможностями ООП в C++ и Objective-C.
Комментариев нет:
Отправить комментарий