Decorator (Декоратор)
Проблема
Нужно добавить поведение к объекту динамически, не изменяя его код. Decorator оборачивает объект, сохраняя тот же интерфейс, но добавляя функциональность.
В Go Decorator -- один из самых естественных паттернов. Вся система io.Reader/io.Writer построена на нём.
Диаграмма
Client --> LoggingReader --> GzipReader --> FileReader
| | |
(logs bytes) (decompresses) (reads file)
All implement io.Reader:
+------------+
| io.Reader |
+------------+
| Read(p) |
+------------+
^
+-----+------+-----+
| | |
FileReader GzipReader LoggingReader
wraps--> wraps-->
Реализация: io.Reader декораторы
package ioutil
import (
"fmt"
"io"
"log/slog"
"time"
)
// CountingReader counts the number of bytes read.
type CountingReader struct {
reader io.Reader
BytesRead int64
}
func NewCountingReader(r io.Reader) *CountingReader {
return &CountingReader{reader: r}
}
func (cr *CountingReader) Read(p []byte) (int, error) {
n, err := cr.reader.Read(p)
cr.BytesRead += int64(n)
return n, err
}
// TimingReader measures how long reads take.
type TimingReader struct {
reader io.Reader
Duration time.Duration
}
func NewTimingReader(r io.Reader) *TimingReader {
return &TimingReader{reader: r}
}
func (tr *TimingReader) Read(p []byte) (int, error) {
start := time.Now()
n, err := tr.reader.Read(p)
tr.Duration += time.Since(start)
return n, err
}
// LoggingReader logs each read operation.
type LoggingReader struct {
reader io.Reader
logger *slog.Logger
name string
}
func NewLoggingReader(r io.Reader, logger *slog.Logger, name string) *LoggingReader {
return &LoggingReader{reader: r, logger: logger, name: name}
}
func (lr *LoggingReader) Read(p []byte) (int, error) {
n, err := lr.reader.Read(p)
lr.logger.Debug("read",
"source", lr.name,
"bytes", n,
"error", err,
)
return n, err
}
// LimitedReader limits total bytes that can be read.
type LimitedReader struct {
reader io.Reader
remaining int64
}
func NewLimitedReader(r io.Reader, limit int64) *LimitedReader {
return &LimitedReader{reader: r, remaining: limit}
}
func (lr *LimitedReader) Read(p []byte) (int, error) {
if lr.remaining <= 0 {
return 0, io.EOF
}
if int64(len(p)) > lr.remaining {
p = p[:lr.remaining]
}
n, err := lr.reader.Read(p)
lr.remaining -= int64(n)
return n, err
}
Цепочка декораторов
func main() {
file, err := os.Open("data.gz")
if err != nil {
log.Fatal(err)
}
defer file.Close()
// Build a decorator chain: file -> counting -> gzip -> logging
counter := ioutil.NewCountingReader(file)
gzReader, _ := gzip.NewReader(counter)
timer := ioutil.NewTimingReader(gzReader)
limited := ioutil.NewLimitedReader(timer, 1<<20) // 1MB limit
// Client just uses io.Reader -- doesn't know about decorators
data, err := io.ReadAll(limited)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Read %d bytes (compressed: %d) in %v\n",
len(data), counter.BytesRead, timer.Duration)
}
Реализация: HTTP Middleware
HTTP middleware в Go -- это декоратор для http.Handler.
package middleware
import (
"log/slog"
"net/http"
"time"
)
// Logging decorates an http.Handler with request logging.
func Logging(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap ResponseWriter to capture status code
wrapped := &statusWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(wrapped, r)
logger.Info("request",
"method", r.Method,
"path", r.URL.Path,
"status", wrapped.status,
"duration", time.Since(start),
)
})
}
}
type statusWriter struct {
http.ResponseWriter
status int
}
func (w *statusWriter) WriteHeader(code int) {
w.status = code
w.ResponseWriter.WriteHeader(code)
}
// Recovery decorates a handler with panic recovery.
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
slog.Error("panic recovered", "error", err)
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}
// CORS adds Cross-Origin Resource Sharing headers.
func CORS(origin string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
Цепочка middleware
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/users", handleUsers)
// Chain decorators: CORS -> Logging -> Recovery -> Handler
logger := slog.Default()
handler := CORS("*")(Logging(logger)(Recovery(mux)))
http.ListenAndServe(":8080", handler)
}
Универсальная цепочка middleware
// Chain applies middlewares in order: first middleware is outermost.
func Chain(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
// Apply in reverse so first middleware wraps outermost
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
// Usage
handler := Chain(mux,
CORS("*"),
Logging(logger),
Recovery,
)
Примеры из стандартной библиотеки
// io package: decorators everywhere
bufio.NewReader(r) // buffering decorator
io.LimitReader(r, n) // limit decorator
io.TeeReader(r, w) // tee decorator (reads + writes copy)
gzip.NewReader(r) // decompression decorator
// http package
http.TimeoutHandler(h, d, msg) // timeout decorator
http.StripPrefix("/api", h) // URL prefix decorator
http.MaxBytesHandler(h, n) // body size limit decorator
Когда использовать
Используйте, когда:
- Нужно добавить поведение к объекту динамически
- Нужна комбинация поведений (логирование + метрики + сжатие)
- Подкласс нежелателен (а в Go его и нет)
- Поведение нужно включать/выключать в runtime
Не используйте, когда:
- Достаточно одной обёртки (лишняя абстракция)
- Порядок декораторов критичен и неочевиден
- Декоратор должен знать о внутренностях обёрнутого объекта
Сравнение с похожими паттернами
| Паттерн | Отличие |
|---|---|
| Adapter | Меняет интерфейс; Decorator сохраняет интерфейс |
| Proxy | Контролирует доступ; Decorator добавляет поведение |
| Composite | Организует дерево; Decorator добавляет обёртку |