HardТеория4 min

Bridge

Паттерн Bridge в Go: разделение абстракции и реализации через композицию интерфейсов

Bridge (Мост)

Проблема

У вас есть два измерения изменчивости: например, тип фигуры (круг, квадрат) и способ отрисовки (vector, raster). Без Bridge количество комбинаций растёт мультипликативно: VectorCircle, RasterCircle, VectorSquare, RasterSquare...

Bridge разделяет абстракцию (что делать) от реализации (как делать), позволяя менять их независимо.

Диаграмма

    Без Bridge (комбинаторный взрыв):
    VectorCircle, VectorSquare, VectorTriangle
    RasterCircle, RasterSquare, RasterTriangle
    = 6 типов (3 x 2)

    С Bridge:
    +----------+       +-----------+
    |  Shape   |------>| Renderer  |  <-- interface
    +----------+       +-----------+
    | renderer |       | RenderCircle()  |
    | Draw()   |       | RenderRect()    |
    +----------+       +-----------+
        ^                   ^
     +--+--+            +---+---+
     |     |            |       |
   Circle Square    Vector   Raster
    = 5 типов (3 + 2)

Реализация

Интерфейс реализации (Renderer)

package drawing

import "fmt"

// Renderer defines how shapes are drawn.
// This is the "implementation" side of the bridge.
type Renderer interface {
    RenderCircle(x, y, radius float64) string
    RenderRect(x, y, width, height float64) string
    RenderLine(x1, y1, x2, y2 float64) string
}

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

// VectorRenderer draws shapes using vector graphics.
type VectorRenderer struct{}

func (v *VectorRenderer) RenderCircle(x, y, radius float64) string {
    return fmt.Sprintf("<circle cx=%.1f cy=%.1f r=%.1f />", x, y, radius)
}

func (v *VectorRenderer) RenderRect(x, y, width, height float64) string {
    return fmt.Sprintf("<rect x=%.1f y=%.1f w=%.1f h=%.1f />", x, y, width, height)
}

func (v *VectorRenderer) RenderLine(x1, y1, x2, y2 float64) string {
    return fmt.Sprintf("<line x1=%.1f y1=%.1f x2=%.1f y2=%.1f />", x1, y1, x2, y2)
}

// RasterRenderer draws shapes using pixels.
type RasterRenderer struct {
    DPI int
}

func (r *RasterRenderer) RenderCircle(x, y, radius float64) string {
    return fmt.Sprintf("Drawing %d pixels for circle at (%.1f,%.1f) r=%.1f @%dDPI",
        int(radius*float64(r.DPI)), x, y, radius, r.DPI)
}

func (r *RasterRenderer) RenderRect(x, y, width, height float64) string {
    return fmt.Sprintf("Drawing %d pixels for rect at (%.1f,%.1f) %1.fx%.1f @%dDPI",
        int(width*height*float64(r.DPI)), x, y, width, height, r.DPI)
}

func (r *RasterRenderer) RenderLine(x1, y1, x2, y2 float64) string {
    return fmt.Sprintf("Drawing pixel line (%.1f,%.1f)-(%.1f,%.1f) @%dDPI",
        x1, y1, x2, y2, r.DPI)
}

Абстракция (Shape)

// Shape is the abstraction side of the bridge.
type Shape interface {
    Draw() string
    Resize(factor float64)
}

// --- Circle ---

type Circle struct {
    renderer Renderer // bridge to implementation
    x, y    float64
    radius  float64
}

func NewCircle(renderer Renderer, x, y, radius float64) *Circle {
    return &Circle{
        renderer: renderer,
        x:        x,
        y:        y,
        radius:   radius,
    }
}

func (c *Circle) Draw() string {
    return c.renderer.RenderCircle(c.x, c.y, c.radius)
}

func (c *Circle) Resize(factor float64) {
    c.radius *= factor
}

// --- Rectangle ---

type Rectangle struct {
    renderer      Renderer
    x, y          float64
    width, height float64
}

func NewRectangle(renderer Renderer, x, y, w, h float64) *Rectangle {
    return &Rectangle{
        renderer: renderer,
        x:        x,
        y:        y,
        width:    w,
        height:   h,
    }
}

func (r *Rectangle) Draw() string {
    return r.renderer.RenderRect(r.x, r.y, r.width, r.height)
}

func (r *Rectangle) Resize(factor float64) {
    r.width *= factor
    r.height *= factor
}

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

func main() {
    vector := &drawing.VectorRenderer{}
    raster := &drawing.RasterRenderer{DPI: 96}

    // Same shapes, different renderers
    shapes := []drawing.Shape{
        drawing.NewCircle(vector, 10, 20, 5),
        drawing.NewCircle(raster, 10, 20, 5),
        drawing.NewRectangle(vector, 0, 0, 100, 50),
        drawing.NewRectangle(raster, 0, 0, 100, 50),
    }

    for _, s := range shapes {
        fmt.Println(s.Draw())
    }

    // Output:
    // <circle cx=10.0 cy=20.0 r=5.0 />
    // Drawing 480 pixels for circle at (10.0,20.0) r=5.0 @96DPI
    // <rect x=0.0 y=0.0 w=100.0 h=50.0 />
    // Drawing 480000 pixels for rect at (0.0,0.0) 100.0x50.0 @96DPI
}

Реальный пример: система логирования

package logger

import (
    "context"
    "fmt"
    "io"
    "time"
)

// Formatter defines how log entries are formatted (implementation).
type Formatter interface {
    Format(level, msg string, fields map[string]any) string
}

// Writer defines where log entries are sent (another implementation dimension).
type Writer interface {
    Write(ctx context.Context, formatted string) error
}

// Logger is the abstraction that bridges Formatter and Writer.
type Logger struct {
    formatter Formatter
    writer    Writer
    level     string
}

// NewLogger creates a Logger with the given formatter and writer.
func NewLogger(f Formatter, w Writer) *Logger {
    return &Logger{formatter: f, writer: w, level: "info"}
}

func (l *Logger) Info(ctx context.Context, msg string, fields map[string]any) error {
    formatted := l.formatter.Format("INFO", msg, fields)
    return l.writer.Write(ctx, formatted)
}

// --- Formatters ---

type JSONFormatter struct{}

func (f *JSONFormatter) Format(level, msg string, fields map[string]any) string {
    return fmt.Sprintf(`{"level":"%s","msg":"%s","time":"%s"}`,
        level, msg, time.Now().Format(time.RFC3339))
}

type TextFormatter struct{}

func (f *TextFormatter) Format(level, msg string, fields map[string]any) string {
    return fmt.Sprintf("[%s] %s %s", level, time.Now().Format("15:04:05"), msg)
}

// --- Writers ---

type FileWriter struct {
    w io.Writer
}

func NewFileWriter(w io.Writer) *FileWriter {
    return &FileWriter{w: w}
}

func (fw *FileWriter) Write(_ context.Context, formatted string) error {
    _, err := fmt.Fprintln(fw.w, formatted)
    return err
}

type RemoteWriter struct {
    endpoint string
}

func (rw *RemoteWriter) Write(ctx context.Context, formatted string) error {
    // Send to remote logging service...
    return nil
}

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

// Any combination of formatter + writer
localJSON := logger.NewLogger(&logger.JSONFormatter{}, logger.NewFileWriter(os.Stdout))
remoteText := logger.NewLogger(&logger.TextFormatter{}, &logger.RemoteWriter{endpoint: "https://logs.example.com"})

Когда использовать

Используйте, когда:

  • Два или более измерения изменчивости
  • Нужно менять реализацию в runtime
  • Комбинаторный взрыв классов
  • Кросс-платформенная разработка

Не используйте, когда:

  • Одно измерение изменчивости (достаточно простого интерфейса)
  • Реализация не меняется (лишняя абстракция)
  • Связь между абстракцией и реализацией тривиальна

Сравнение с похожими паттернами

Паттерн Отличие
Adapter Адаптирует существующий несовместимый интерфейс; Bridge проектируется заранее
Strategy Меняет алгоритм; Bridge разделяет абстракцию и реализацию на двух уровнях
Abstract Factory Создаёт семейства объектов; Bridge соединяет абстракцию с реализацией

Проверь себя

Какую проблему решает паттерн Bridge?

Как Bridge реализуется в Go?