ایجاد توابع کمکی اعتبارسنجی
خوب، حالا در موقعیتی هستیم که برنامه ما دادههای فرم را طبق قوانین کسبوکار اعتبارسنجی میکند و هرگونه خطای اعتبارسنجی را به صورت مناسب مدیریت میکند. این عالی است، اما برای رسیدن به اینجا کار زیادی انجام شده است.
و در حالی که رویکردی که اتخاذ کردهایم به عنوان یک مورد یکباره خوب است، اگر برنامه شما فرمهای زیادی دارد، میتوانید با تکرار زیادی در کد و قوانین اعتبارسنجی مواجه شوید. گذشته از این، نوشتن کد برای اعتبارسنجی فرمها دقیقاً هیجانانگیزترین راه برای گذراندن وقت شما نیست.
بنابراین برای کمک به ما در اعتبارسنجی در سراسر بقیه این پروژه، یک بسته کوچک internal/validator خودمان ایجاد میکنیم تا برخی از این رفتار را انتزاع کنیم و کد تکراری را در handlerهای خود کاهش دهیم. در واقع نحوه کار برنامه برای کاربر را تغییر نمیدهیم؛ این واقعاً فقط یک بازسازی از کدبیس ما است.
افزودن یک بسته اعتبارسنجی
اگر همراه میآیید، لطفاً پیش بروید و دایرکتوری و فایل زیر را روی ماشین خود ایجاد کنید:
$ mkdir internal/validator $ touch internal/validator/validator.go
سپس در این فایل جدید internal/validator/validator.go کد زیر را اضافه کنید:
package validator import ( "slices" "strings" "unicode/utf8" ) // Define a new Validator struct which contains a map of validation error messages // for our form fields. type Validator struct { FieldErrors map[string]string } // Valid() returns true if the FieldErrors map doesn't contain any entries. func (v *Validator) Valid() bool { return len(v.FieldErrors) == 0 } // AddFieldError() adds an error message to the FieldErrors map (so long as no // entry already exists for the given key). func (v *Validator) AddFieldError(key, message string) { // Note: We need to initialize the map first, if it isn't already // initialized. if v.FieldErrors == nil { v.FieldErrors = make(map[string]string) } if _, exists := v.FieldErrors[key]; !exists { v.FieldErrors[key] = message } } // CheckField() adds an error message to the FieldErrors map only if a // validation check is not 'ok'. func (v *Validator) CheckField(ok bool, key, message string) { if !ok { v.AddFieldError(key, message) } } // NotBlank() returns true if a value is not an empty string. func NotBlank(value string) bool { return strings.TrimSpace(value) != "" } // MaxChars() returns true if a value contains no more than n characters. func MaxChars(value string, n int) bool { return utf8.RuneCountInString(value) <= n } // PermittedValue() returns true if a value is in a list of specific permitted // values. func PermittedValue[T comparable](value T, permittedValues ...T) bool { return slices.Contains(permittedValues, value) }
برای خلاصه کردن این:
در کد بالا یک نوع ساختار Validator تعریف کردهایم که شامل یک نقشه از پیامهای خطا است. نوع Validator یک متد CheckField() برای افزودن شرطی خطاها به نقشه فراهم میکند،
و یک متد Valid() که برمیگرداند که آیا نقشه خطاها خالی است یا نه. همچنین توابع NotBlank()، MaxChars() و PermittedValue() را اضافه کردهایم تا به ما در انجام برخی بررسیهای اعتبارسنجی خاص کمک کنند.
از نظر مفهومی، این نوع Validator نسبتاً ساده است، اما این چیز بدی نیست. همانطور که در طول این کتاب خواهیم دید، در عمل به طرز شگفتانگیزی قدرتمند است و انعطافپذیری و کنترل زیادی بر بررسیهای اعتبارسنجی و نحوه انجام آنها به ما میدهد.
استفاده از کمککنندهها
خوب، بیایید شروع کنیم و از نوع Validator استفاده کنیم!
به فایل cmd/web/handlers.go خود برمیگردیم و آن را برای جاسازی یک ساختار Validator در ساختار snippetCreateForm خود بهروزرسانی میکنیم، و سپس از این برای انجام بررسیهای اعتبارسنجی لازم روی دادههای فرم استفاده میکنیم.
به این صورت:
package main import ( "errors" "fmt" "net/http" "strconv" "snippetbox.alexedwards.net/internal/models" "snippetbox.alexedwards.net/internal/validator" // New import ) ... // Remove the explicit FieldErrors struct field and instead embed the Validator // struct. Embedding this means that our snippetCreateForm "inherits" all the // fields and methods of our Validator struct (including the FieldErrors field). type snippetCreateForm struct { Title string Content string Expires int validator.Validator } func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { app.clientError(w, http.StatusBadRequest) return } expires, err := strconv.Atoi(r.PostForm.Get("expires")) if err != nil { app.clientError(w, http.StatusBadRequest) return } form := snippetCreateForm{ Title: r.PostForm.Get("title"), Content: r.PostForm.Get("content"), Expires: expires, // Remove the FieldErrors assignment from here. } // Because the Validator struct is embedded by the snippetCreateForm struct, // we can call CheckField() directly on it to execute our validation checks. // CheckField() will add the provided key and error message to the // FieldErrors map if the check does not evaluate to true. For example, in // the first line here we "check that the form.Title field is not blank". In // the second, we "check that the form.Title field has a maximum character // length of 100" and so on. form.CheckField(validator.NotBlank(form.Title), "title", "This field cannot be blank") form.CheckField(validator.MaxChars(form.Title, 100), "title", "This field cannot be more than 100 characters long") form.CheckField(validator.NotBlank(form.Content), "content", "This field cannot be blank") form.CheckField(validator.PermittedValue(form.Expires, 1, 7, 365), "expires", "This field must equal 1, 7 or 365") // Use the Valid() method to see if any of the checks failed. If they did, // then re-render the template passing in the form in the same way as // before. if !form.Valid() { data := app.newTemplateData(r) data.Form = form app.render(w, r, http.StatusUnprocessableEntity, "create.tmpl", data) return } id, err := app.snippets.Insert(form.Title, form.Content, form.Expires) if err != nil { app.serverError(w, r, err) return } http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther) }
پس این واقعاً خوب شکل میگیرد.
حالا یک بسته internal/validator با قوانین و منطق اعتبارسنجی داریم که میتواند در سراسر برنامه ما دوباره استفاده شود، و به راحتی میتوان آن را برای شامل کردن قوانین اضافی در آینده گسترش داد. هم دادههای فرم و هم خطاها به خوبی در یک ساختار واحد snippetCreateForm کپسوله شدهاند — که میتوانیم به راحتی به قالبهای خود بدهیم — و نحو برای نمایش پیامهای خطا و پر کردن مجدد دادهها در قالبهای ما ساده و سازگار است.
اگر میخواهید، همین حالا برنامه را دوباره اجرا کنید. اگر همه چیز خوب باشد، باید ببینید که فرم و قوانین اعتبارسنجی به درستی و دقیقاً به همان روش قبلی کار میکنند.
اطلاعات اضافی
عمومیها
Go 1.18 اولین نسخه زبان بود که از عمومیها پشتیبانی میکرد — که با نام فنیتر چندریختی پارامتری نیز شناخته میشود. به طور بسیار گسترده، عمومیها به شما اجازه میدهند کدی بنویسید که با انواع مشخص مختلف کار میکند.
به عنوان مثال، در نسخههای قدیمیتر Go، اگر میخواستید تعداد دفعاتی که یک مقدار خاص در یک برش []string و یک برش []int ظاهر میشود را بشمارید، باید دو تابع جداگانه مینوشتید — یک تابع برای نوع []string و دیگری برای []int. کمی شبیه این:
// Count how many times the value v appears in the slice s. func countString(v string, s []string) int { count := 0 for _, vs := range s { if v == vs { count++ } } return count } func countInt(v int, s []int) int { count := 0 for _, vs := range s { if v == vs { count++ } } return count }
حالا، با عمومیها، میتوان یک تابع واحد count() نوشت که برای []string، []int، یا هر برش دیگری از یک نوع قابل مقایسه کار میکند. کد به این صورت خواهد بود:
func count[T comparable](v T, s []T) int { count := 0 for _, vs := range s { if v == vs { count++ } } return count }
اگر با نحو کد عمومی در Go آشنا نیستید، اطلاعات عالی زیادی در دسترس است که نحوه کار عمومیها را توضیح میدهد و شما را از طریق نحو نوشتن کد عمومی راهنمایی میکند.
برای بهروز ماندن، به شدت توصیه میکنم آموزش رسمی عمومیهای Go را بخوانید، و همچنین 15 دقیقه اول این ویدیو را تماشا کنید تا آنچه یاد گرفتهاید را تثبیت کنید.
به جای تکرار همان اطلاعات در اینجا، در عوض میخواهم به طور خلاصه درباره یک موضوع کمتر رایج (اما به همان اندازه مهم!) صحبت کنم: چه زمانی از عمومیها استفاده کنیم.
حداقل برای الان، باید هدف شما استفاده از عمومیها با قضاوت و احتیاط باشد.
میدانم که ممکن است کمی خستهکننده به نظر برسد، اما عمومیها یک ویژگی نسبتاً جدید زبان هستند و بهترین شیوهها در مورد نوشتن کد عمومی هنوز در حال تثبیت هستند. اگر در یک تیم کار میکنید، یا کد را به صورت عمومی مینویسید، همچنین ارزش دارد به خاطر داشته باشید که همه توسعهدهندگان Go دیگر لزوماً با نحوه کار کد عمومی آشنا نخواهند بود.
شما نیازی به استفاده از عمومیها ندارید، و اشکالی ندارد که استفاده نکنید.
اما حتی با این احتیاطها، نوشتن کد عمومی میتواند در سناریوهای خاصی واقعاً مفید باشد. به طور بسیار کلی، ممکن است بخواهید آن را در نظر بگیرید:
- اگر خود را در حال نوشتن کد تکراری برای انواع داده مختلف میبینید. نمونههایی از این ممکن است عملیات رایج روی برشها، نقشهها یا کانالها باشد — یا کمککنندههایی برای انجام بررسیهای اعتبارسنجی یا ادعاهای تست روی انواع داده مختلف.
- وقتی در حال نوشتن کد هستید و خود را در حال استفاده از نوع
any(خالیinterface{}) میبینید. نمونهای از این ممکن است زمانی باشد که یک ساختار داده (مانند یک صف، کش یا لیست پیوندی) ایجاد میکنید که نیاز به کار روی انواع مختلف دارد.
در مقابل، احتمالاً نمیخواهید از عمومیها استفاده کنید:
- اگر کد شما را سختتر برای درک یا کمتر واضح میکند.
- اگر همه انواعی که باید با آنها کار کنید مجموعه مشترکی از متدها دارند — در این صورت بهتر است به جای آن یک نوع
interfaceمعمولی تعریف و استفاده کنید. - فقط چون میتوانید. در عوض ترجیح دهید به طور پیشفرض کد غیرعمومی بنویسید، و بعداً فقط اگر واقعاً لازم باشد به نسخه عمومی تغییر دهید.