Let's Go پیکربندی و مدیریت خطا › تزریق وابستگی (Dependency Injection)
قبلی · فهرست · بعدی
فصل 3.3.

تزریق وابستگی (Dependency Injection)

اگر فایل handlers.go خود را باز کنید، متوجه خواهید شد که تابع home هنوز از لاگر استاندارد Go (Standard Go Logger) برای نوشتن پیام‌های خطا استفاده می‌کند، نه لاگر ساختاری (Structured Logger) که اکنون می‌خواهیم استفاده کنیم.

func home(w http.ResponseWriter, r *http.Request) {
    ...

    ts, err := template.ParseFiles(files...)
    if err != nil {
        log.Print(err.Error()) // This isn't using our new structured logger.
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }

    err = ts.ExecuteTemplate(w, "base", nil)
    if err != nil {
        log.Print(err.Error()) // This isn't using our new structured logger.
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    }
}

این سوال خوبی را مطرح می‌کند: چگونه می‌توانیم لاگر ساختاری جدید خود را از main() در دسترس تابع home قرار دهیم؟

و این سوال بیشتر تعمیم می‌یابد. بیشتر برنامه‌های وب دارای وابستگی‌های متعددی هستند که هندلرهای آن‌ها باید به آن‌ها دسترسی داشته باشند، مانند یک اتصال پایگاه داده، مدیریت خطای متمرکز و کش‌های قالب. آنچه واقعاً می‌خواهیم پاسخ دهیم این است: چگونه می‌توانیم هر وابستگی را در دسترس هندلرهای خود قرار دهیم؟

چند رویکرد مختلف برای انجام این کار وجود دارد، ساده‌ترین آن‌ها این است که وابستگی‌ها را در متغیرهای جهانی قرار دهیم. اما به طور کلی، بهتر است وابستگی‌ها را به هندلرهای خود تزریق کنید. این کار کد شما را صریح‌تر، کمتر مستعد خطا و آسان‌تر برای تست واحد می‌کند تا اینکه از متغیرهای جهانی استفاده کنید.

برای برنامه‌هایی که همه هندلرهای شما در یک بسته هستند، مانند برنامه ما، یک رویکرد مناسب برای تزریق وابستگی‌ها این است که آن‌ها را در یک ساختار application سفارشی قرار دهید و سپس توابع هندلر خود را به عنوان متدهایی در برابر application تعریف کنید.

من نشان خواهم داد.

ابتدا فایل main.go خود را باز کنید و یک ساختار application جدید به صورت زیر ایجاد کنید:

File: cmd/web/main.go
package main

import (
    "flag"
    "log/slog"
    "net/http"
    "os"
)

// Define an application struct to hold the application-wide dependencies for the
// web application. For now we'll only include the structured logger, but we'll
// add more to this as the build progresses.
type application struct {
    logger *slog.Logger
}

func main() {
    ...
}

و سپس در فایل handlers.go، می‌خواهیم توابع هندلر را به‌روزرسانی کنیم تا به متدهایی در برابر ساختار application تبدیل شوند و از لاگر ساختاری که در آن قرار دارد استفاده کنند.

File: cmd/web/handlers.go
package main

import (
    "fmt"
    "html/template"
    "net/http"
    "strconv"
)

// Change the signature of the home handler so it is defined as a method against
// *application.
func (app *application) home(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Server", "Go")

    files := []string{
        "./ui/html/base.tmpl",
        "./ui/html/partials/nav.tmpl",
        "./ui/html/pages/home.tmpl",
    }

    ts, err := template.ParseFiles(files...)
    if err != nil {
        // Because the home handler is now a method against the application
        // struct it can access its fields, including the structured logger. We'll 
        // use this to create a log entry at Error level containing the error
        // message, also including the request method and URI as attributes to 
        // assist with debugging.
        app.logger.Error(err.Error(), "method", r.Method, "uri", r.URL.RequestURI())
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }

    err = ts.ExecuteTemplate(w, "base", nil)
    if err != nil {
        // And we also need to update the code here to use the structured logger
        // too.
        app.logger.Error(err.Error(), "method", r.Method, "uri", r.URL.RequestURI())
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    }
}

// Change the signature of the snippetView handler so it is defined as a method
// against *application.
func (app *application) snippetView(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil || id < 1 {
        http.NotFound(w, r)
        return
    }

    fmt.Fprintf(w, "Display a specific snippet with ID %d...", id)
}

// Change the signature of the snippetCreate handler so it is defined as a method
// against *application.
func (app *application) snippetCreate(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Display a form for creating a new snippet..."))
}

// Change the signature of the snippetCreatePost handler so it is defined as a method
// against *application.
func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusCreated)
    w.Write([]byte("Save a new snippet..."))
}

و در نهایت بیایید همه چیز را در فایل main.go خود به هم متصل کنیم:

File: cmd/web/main.go
package main

import (
    "flag"
    "log/slog"
    "net/http"
    "os"
)

type application struct {
    logger *slog.Logger
}

func main() {
    addr := flag.String("addr", ":4000", "HTTP network address")
    flag.Parse()

    logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

    // Initialize a new instance of our application struct, containing the
    // dependencies (for now, just the structured logger).
    app := &application{
        logger: logger,
    }

    mux := http.NewServeMux()

    fileServer := http.FileServer(http.Dir("./ui/static/"))
    mux.Handle("GET /static/", http.StripPrefix("/static", fileServer))
    
    // Swap the route declarations to use the application struct's methods as the
    // handler functions.
    mux.HandleFunc("GET /{$}", app.home)
    mux.HandleFunc("GET /snippet/view/{id}", app.snippetView)
    mux.HandleFunc("GET /snippet/create", app.snippetCreate)
    mux.HandleFunc("POST /snippet/create", app.snippetCreatePost)
    

    logger.Info("starting server", "addr", *addr)
    
    err := http.ListenAndServe(*addr, mux)
    logger.Error(err.Error())
    os.Exit(1)
}

می‌دانم که این رویکرد ممکن است کمی پیچیده و پیچیده به نظر برسد، به خصوص زمانی که یک جایگزین این است که به سادگی logger را به یک متغیر جهانی تبدیل کنید. اما با من بمانید. همانطور که برنامه رشد می‌کند و هندلرهای ما شروع به نیاز به وابستگی‌های بیشتری می‌کنند، این الگو ارزش خود را نشان خواهد داد.

افزودن یک خطای عمدی (Adding a Deliberate Error)

بیایید این را امتحان کنیم و به سرعت یک خطای عمدی به برنامه خود اضافه کنیم.

ترمینال خود را باز کنید و ui/html/pages/home.tmpl را به ui/html/pages/home.bak تغییر نام دهید. هنگامی که برنامه خود را اجرا می‌کنیم و درخواست صفحه اصلی را می‌دهیم، این باید منجر به خطا شود زیرا فایل ui/html/pages/home.tmpl دیگر وجود ندارد.

بروید و تغییر را انجام دهید:

$ cd $HOME/code/snippetbox
$ mv ui/html/pages/home.tmpl ui/html/pages/home.bak

سپس برنامه را اجرا کنید و درخواست http://localhost:4000 را بدهید. شما باید یک پاسخ HTTP Internal Server Error در مرورگر خود دریافت کنید و یک ورودی لاگ مربوطه در سطح Error در ترمینال خود مشاهده کنید که شبیه به این است:

$ go run ./cmd/web
time=2024-03-18T11:29:23.000+00:00 level=INFO msg="starting server" addr=:4000
time=2024-03-18T11:29:23.000+00:00 level=ERROR msg="open ./ui/html/pages/home.tmpl: no such file or directory" method=GET uri=/

این به خوبی نشان می‌دهد که لاگر ساختاری ما اکنون به عنوان یک وابستگی به هندلر home ما منتقل می‌شود و همانطور که انتظار می‌رفت کار می‌کند.

خطای عمدی را فعلاً در جای خود بگذارید؛ ما دوباره به آن در فصل بعدی نیاز خواهیم داشت.


اطلاعات اضافی (Additional Information)

بسته‌ها برای تزریق وابستگی (Closures for Dependency Injection)

الگویی که ما برای تزریق وابستگی‌ها استفاده می‌کنیم، زمانی که هندلرهای شما در چندین بسته پخش شده‌اند، کار نخواهد کرد. در این صورت، یک رویکرد جایگزین این است که یک بسته config مستقل ایجاد کنید که یک ساختار Application را صادر کند و توابع هندلر شما بر روی این بسته بسته شوند تا یک بسته تشکیل دهند. به طور تقریبی:

// package config

type Application struct {
    Logger *slog.Logger
}
// package foo

func ExampleHandler(app *config.Application) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ...
        ts, err := template.ParseFiles(files...)
        if err != nil {
            app.Logger.Error(err.Error(), "method", r.Method, "uri", r.URL.RequestURI())
            http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            return
        }
        ...
    }
}
// package main

func main() {
    app := &config.Application{
        Logger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
    }
    ...
    mux.Handle("/", foo.ExampleHandler(app))
    ...
}

می‌توانید یک مثال کامل و ملموس‌تر از نحوه استفاده از الگوی بسته را در این Gist پیدا کنید.

واژه‌نامه اصطلاحات فنی

اصطلاح فارسی معادل انگلیسی توضیح
تزریق وابستگی Dependency Injection الگویی برای ارائه وابستگی‌های مورد نیاز یک شیء یا تابع از خارج
لاگر ساختاری Structured Logger سیستم لاگ‌گیری که داده‌ها را در قالبی ساختاریافته ذخیره می‌کند
هندلر Handler تابعی که درخواست‌های HTTP را پردازش می‌کند
متغیر جهانی Global Variable متغیری که در سراسر برنامه قابل دسترسی است
ساختار برنامه Application Structure ساختار داده‌ای که وابستگی‌های برنامه را نگهداری می‌کند
متد Method تابعی که روی یک نوع داده خاص تعریف می‌شود
بسته Package مجموعه‌ای از کدهای مرتبط در Go
خطای عمدی Deliberate Error خطایی که به صورت عمدی برای آزمایش سیستم ایجاد می‌شود
وابستگی Dependency منبع یا سرویسی که یک بخش از برنامه به آن نیاز دارد
تست واحد Unit Testing آزمایش بخش‌های مجزای کد به صورت مستقل