Привет, коллеги! Сегодня будем говорить о паттерне «Мост» (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+ и шаблоном приложения с базовой структурой для работы с БД. Записаться
ссылка на оригинал статьи https://habr.com/ru/articles/863076/
Добавить комментарий