Привет, коллеги! Сегодня будем говорить о паттерне «Мост» (Bridge).
Простыми словами, «Мост» позволяет разделить две иерархии: одну — абстракций, другую — реализаций. Паттерн становится полезен, когда есть несколько способов реализации функционала, и хочется сохранить возможность гибкой замены одной реализации на другую.
Архитектура паттерн
Вот как выглядит «Мост» в терминах C:
Абстракция: содержит указатель на реализацию.
Реализация: предоставляет интерфейс для конкретных действий.
Конкретная абстракция: расширяет абстракцию.
Конкретная реализация: реализует интерфейс.
Реализация паттерна в 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)
dreesh
04.12.2024 20:01Если не кто ни хочет говорить, то я скажу:
В завершение рекомендую обратить внимание на открытые уроки, которые совсем скоро пройдут в Otus в рамках курса «Программист С»:
Статья полностью дискредитирует ваши курсы! Вы в вызовах create* выделяете структуры на стеке....
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 имеются структуры с функциями, конечно, дискредитируют.
Emelian
Жаль, что нет графической реализации, для быстрого восприятия, как, например, в моей статье на похожую тему: «Модульное программирование в C++. Статические и динамические плагины» ( https://habr.com/ru/articles/566864/ ).