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

от автора

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


ссылка на оригинал статьи https://habr.com/ru/articles/863076/