Привет, коллеги! Сегодня будем говорить о паттерне «Мост» (Bridge).

Простыми словами, «Мост» позволяет разделить две иерархии: одну — абстракций, другую — реализаций. Паттерн становится полезен, когда есть несколько способов реализации функционала, и хочется сохранить возможность гибкой замены одной реализации на другую.

Архитектура паттерн

Вот как выглядит «Мост» в терминах C:

  1. Абстракция: содержит указатель на реализацию.

  2. Реализация: предоставляет интерфейс для конкретных действий.

  3. Конкретная абстракция: расширяет абстракцию.

  4. Конкретная реализация: реализует интерфейс.

Реализация паттерна в C

Начнем с определения интерфейсов. Условимся, что задача — реализовать систему отрисовки фигур.

Определяем интерфейс для реализации

В C нет встроенных интерфейсов, но есть структуры с функциями. Это способ эмулировать интерфейсы.

#include <stdio.h>

// Интерфейс для реализации
typedef struct Renderer {
    void (*render_circle)(struct Renderer*, float x, float y, float radius);
    void (*render_square)(struct Renderer*, float x, float y, float size);
} Renderer;

Интерфейс с двумя методами для отрисовки круга и квадрата. Заметьте, что первым параметром мы передаем указатель на сам интерфейс — классика ООП на C.

Конкретные реализации

Теперь определим разные способы отрисовки.

typedef struct {
    Renderer base; // Наследуем интерфейс
} ScreenRenderer;

void screen_render_circle(Renderer* self, float x, float y, float radius) {
    printf("Drawing circle on screen at (%.2f, %.2f) with radius %.2f\n", x, y, radius);
}

void screen_render_square(Renderer* self, float x, float y, float size) {
    printf("Drawing square on screen at (%.2f, %.2f) with size %.2f\n", x, y, size);
}

ScreenRenderer create_screen_renderer() {
    ScreenRenderer renderer;
    renderer.base.render_circle = screen_render_circle;
    renderer.base.render_square = screen_render_square;
    return renderer;
}

Другой вариант — рендер в файл:

typedef struct {
    Renderer base;
    const char* filename;
} FileRenderer;

void file_render_circle(Renderer* self, float x, float y, float radius) {
    FileRenderer* file_renderer = (FileRenderer*)self;
    FILE* file = fopen(file_renderer->filename, "a");
    if (!file) return;
    fprintf(file, "Circle: (%.2f, %.2f), radius %.2f\n", x, y, radius);
    fclose(file);
}

void file_render_square(Renderer* self, float x, float y, float size) {
    FileRenderer* file_renderer = (FileRenderer*)self;
    FILE* file = fopen(file_renderer->filename, "a");
    if (!file) return;
    fprintf(file, "Square: (%.2f, %.2f), size %.2f\n", x, y, size);
    fclose(file);
}

FileRenderer create_file_renderer(const char* filename) {
    FileRenderer renderer = {.filename = filename};
    renderer.base.render_circle = file_render_circle;
    renderer.base.render_square = file_render_square;
    return renderer;
}

Абстракция

Теперь перейдем к фигурам. Абстракция будет содержать указатель на реализацию.

typedef struct {
    Renderer* renderer;
} Shape;

void shape_draw_circle(Shape* self, float x, float y, float radius) {
    self->renderer->render_circle(self->renderer, x, y, radius);
}

void shape_draw_square(Shape* self, float x, float y, float size) {
    self->renderer->render_square(self->renderer, x, y, size);
}

Здесь мы чётко отделяем «что рисуем» (круг, прямоугольник) от «как рисуем» (через Renderer).

Конкретные фигуры

Для удобства создадим спец функции для конкретных фигур.

typedef struct {
    Shape base;
    float radius;
} Circle;

void circle_draw(Circle* self, float x, float y) {
    shape_draw_circle(&self->base, x, y, self->radius);
}

Circle create_circle(Renderer* renderer, float radius) {
    Circle circle;
    circle.base.renderer = renderer;
    circle.radius = radius;
    return circle;
}

Использование

Все готово. Настало время посмотреть, как это работает.

int main() {
    // Создаём рендерер
    ConsoleRenderer* console_renderer = create_console_renderer();

    // Создаём круг
    Circle circle = {
        .base = { .renderer = (Renderer*)console_renderer, .draw = circle_draw },
        .x = 10, .y = 20, .radius = 5
    };

    // Создаём прямоугольник
    Rectangle rectangle = {
        .base = { .renderer = (Renderer*)console_renderer, .draw = rectangle_draw },
        .x = 5, .y = 10, .width = 15, .height = 25
    };

    // Рисуем
    circle.base.draw((Shape*)&circle);
    rectangle.base.draw((Shape*)&rectangle);

    // Освобождаем память
    free(console_renderer);

    return 0;
}

Но будем честны: писать подобный код в Си — не всегда очевидно. Но если нужно построить систему, где разные реализации могут легко заменяться, «Мост» работает идеально.

  • Хотите добавить еще один рендерер? Просто создайте новую реализацию Renderer.

  • Надо поддержать новые фигуры? Расширяйте Shape.

Особенно хорошо паттерн заходит в условиях ограниченных ресурсов, например, на встраиваемых системах. Там, где инструментарий далеко не такой богатый, как в высокоуровневых языках, этот подход помогает построить легковесную и легко расширяемую архитектуру.

В завершение рекомендую обратить внимание на открытые уроки, которые совсем скоро пройдут в Otus в рамках курса «Программист С»:

  • 5 декабря: «Функциональное программирование на языке С».
    На нем освоите концепции функционального программирования в С, а также узнаете, как писать чистый, поддерживаемый код с использованием функциональных подходов. Записаться

  • 19 декабря: «Создаем приложение на С с графическим интерфейсом пользователя».
    На занятии познакомитесь с подходами к созданию GUI на языке С, с описанием библиотеки GTK+ и шаблоном приложения с базовой структурой для работы с БД. Записаться

Комментарии (3)


  1. Emelian
    04.12.2024 20:01

    Реализация паттерна Bridge в чистом C

    Жаль, что нет графической реализации, для быстрого восприятия, как, например, в моей статье на похожую тему: «Модульное программирование в C++. Статические и динамические плагины» ( https://habr.com/ru/articles/566864/ ).


  1. dreesh
    04.12.2024 20:01

    Если не кто ни хочет говорить, то я скажу:

    В завершение рекомендую обратить внимание на открытые уроки, которые совсем скоро пройдут в Otus в рамках курса «Программист С»:

    Статья полностью дискредитирует ваши курсы! Вы в вызовах create* выделяете структуры на стеке....


    1. eptr
      04.12.2024 20:01

      Вы в вызовах create* выделяете структуры на стеке....

      И ― что?

      Circle create_circle(Renderer* renderer, float radius) {
          Circle circle;
          circle.base.renderer = renderer;
          circle.radius = radius;
          return circle;
      }

      Возврат-то все равно идёт по значению.

      Другое дело, что в таком коде логично использовать составной литерал:

      Circle create_circle(Renderer* renderer, float radius) {
          return (Circle){.base.renderer = renderer, .radius = radius};
      }

      Статья полностью дискредитирует ваши курсы!

      Статья дискредитирует немного по другой причине.

      В самих структурах должны быть не указатели на функции интерфейса, а указатели на служебные структуры, в свою очередь, содержащие уже указатели на сами функции интерфейса.

      Тогда, сколько бы не было функций у интерфейса, в каждом экземпляре память расходовалась бы только на указатель на служебную структуру.

      А сейчас, если имеется, к примеру, 50 функций в интерфейсе, то каждый экземпляр вынужден хранить все 50 указателей на эти функции.

      Ну, и фразы, вроде того, что в C имеются структуры с функциями, конечно, дискредитируют.