Страницы

Объектно-ориентированное программирование в Си(без ++). Пример


Я вижу лишь одну причину использовать ООП в Си - это желание иметь один интерфейс для по-разному реализованных сущностей-объектов, в том числе и не написанных на момент создания кода, использующего этот единый интерфейс. Эта ситуация часто возникает при создании библиотек для организации обратной связи либо для однообразного обращения к разнородным источникам данных. Для решения этой задачи достаточно подхода Абстрактный Интерфейс — Реализация, который может быть легко реализован в чистом Си с помощью функциональных переменных и возможности использования указателей на структуры, без предварительного указания их содержимого.

Объявим интерфейс объекта в 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



Итак, что же получилось:
  1. Простейшее наследование объектов Интерфейс — Реализация.
  2. Настоящее динамическое ООП — более одной реализации одного интерфейса могут сосуществовать в одной области видимости одновременно.
  3. Содержимое объектов недоступно извне в обход методов.
  4. Типизированные указатели объектов, что даёт отслеживание компилятором ошибки присваиваний между разными интерфейсами.
  5. Чистый Си без дополнительных библиотек и магии макросов.
  6. Довольно объёмно по количеству кода и непривычный для ООП вызов методов за счёт необходимости явного выписывания того, что в ООП-языках подразумевается неявно.

Полный исходный код примера доступен на github.

Три года назад я сравнивал производительность такого подхода со встроенными возможностями ООП в C++ и Objective-C.

Комментариев нет:

Отправить комментарий