Страницы

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

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

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

Итак, что же получилось:

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

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

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

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

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