Я вижу лишь
одну причину использовать ООП в Си - это
желание иметь один интерфейс для
по-разному реализованных сущностей-объектов,
в том числе и не написанных на момент
создания кода, использующего этот единый
интерфейс. Эта ситуация часто возникает
при создании библиотек для организации
обратной связи либо для однообразного
обращения к разнородным источникам
данных. Для решения этой задачи достаточно
подхода Абстрактный Интерфейс —
Реализация, который может быть легко
реализован в чистом Си с помощью
функциональных переменных и возможности
использования указателей на структуры,
без предварительного указания их
содержимого.
Объявим
интерфейс объекта в 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, method,
release);
c->mult =
mult;
return o;
}
Создадим аналогичный файл object-add.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.
Комментариев нет:
Отправить комментарий