تزریق وابستگی (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 جدید به صورت زیر ایجاد کنید:
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 تبدیل شوند و از لاگر ساختاری که در آن قرار دارد استفاده کنند.
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 خود به هم متصل کنیم:
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 | آزمایش بخشهای مجزای کد به صورت مستقل |