9.1.6.1 توضیحات #
در دنیای طراحی نرمافزار، یکی از چالشهای رایج، ساختاشیاء پیچیده با پارامترهای متعدد و متنوع است. فرض کنید قصد دارید یک شیء پیکربندی برای اتصال به پایگاه داده بسازید. بسته به نوع پایگاه داده ای که می خواهید (MySQL، PostgreSQL، SQLite و …)، ممکن است به مجموعهای متفاوت از پارامترها نیاز داشته باشید: نام کاربری و گذرواژه، میزبان و پورت، نام پایگاه داده یا حتی مسیر فایل. اگر بخواهیم همهی این موارد را با یک سازنده ساده مدیریت کنیم، به زودی با توابعی پر از پارامترهای اختیاری و ترتیبهای گیجکننده مواجه خواهیم شد.
اینجاست که الگوی Builder وارد عمل میشود. این الگو با تفکیک فرآیند ساخت از شیء نهایی، به ما اجازه میدهد تا اشیاء را به صورت گام به گام، خوانا و قابل انعطاف بسازیم. با استفاده از این الگو نگهداری کد سادهتر است و آن را برای توسعهدهندگان دیگر قابل فهمتر میکند.
الگوی بیلدر وقتی کمک کننده است که بخواهید یک شیء پیچیده با پارامترهای زیاد را مرحله به مرحله و خواناتر بسازید، بدون اینکه درگیر سازندههای طولانی و گیجکننده بشوید.
9.1.6.2 دیاگرام #
9.1.6.3 مثال #
وقتش رسیده که یک نمونهی واقعی را ببینیم. در کدی که در ادامه میآید، ما یک ساختار DBConfig
داریم که تنظیمات اتصال به پایگاه داده را نگه میدارد. یک Builder به نام DBBuilder
ایجاد کردهایم که به ما اجازه میدهد با استفاده از متدهای زنجیرهای (SetUser
, SetHost
, …) تنها فیلدهای مورد نیاز خود را مقداردهی کنیم و در پایان با فراخوانی Build()
شیء نهایی را تحویل بگیریم.
package main
import (
"fmt"
"strings"
)
type DBConfig struct {
Driver string
User string
Password string
Host string
Port int
DBName string
FilePath string
SSLMode string
}
type DBBuilder struct {
config DBConfig
errs []string
}
func NewDBBuilder(driver string) *DBBuilder {
d := strings.ToLower(strings.TrimSpace(driver))
return &DBBuilder{config: DBConfig{Driver: d}}
}
func (b *DBBuilder) SetUser(user string) *DBBuilder {
b.config.User = user
return b
}
func (b *DBBuilder) SetPassword(pass string) *DBBuilder {
b.config.Password = pass
return b
}
func (b *DBBuilder) SetHost(host string) *DBBuilder {
b.config.Host = host
return b
}
func (b *DBBuilder) SetPort(port int) *DBBuilder {
b.config.Port = port
return b
}
func (b *DBBuilder) SetDBName(db string) *DBBuilder {
b.config.DBName = db
return b
}
func (b *DBBuilder) SetFilePath(path string) *DBBuilder {
b.config.FilePath = path
return b
}
func (b *DBBuilder) SetSSLMode(mode string) *DBBuilder {
b.config.SSLMode = mode
return b
}
func (b *DBBuilder) Build() (DBConfig, error) {
// reset previous validation state to avoid stale errors on repeated Build calls
b.errs = b.errs[:0]
switch b.config.Driver {
case "mysql", "postgres", "postgresql":
if b.config.User == "" {
b.errs = append(b.errs, "user is required for SQL drivers")
}
if b.config.DBName == "" {
b.errs = append(b.errs, "dbname is required for SQL drivers")
}
case "sqlite":
if b.config.FilePath == "" {
b.errs = append(b.errs, "file path is required for sqlite")
}
default:
b.errs = append(b.errs, fmt.Sprintf("unknown driver: %s", b.config.Driver))
}
// general validation
if b.config.Port != 0 && (b.config.Port < 1 || b.config.Port > 65535) {
b.errs = append(b.errs, "port must be in [1, 65535]")
}
if len(b.errs) > 0 {
return DBConfig{}, fmt.Errorf("invalid configuration: %s", strings.Join(b.errs, "; "))
}
return b.config, nil
}
func main() {
// MySQL Example
mysqlCfg, err := NewDBBuilder("mysql").
SetUser("admin").
SetPassword("s3cr3t").
SetHost("127.0.0.1").
SetPort(3306).
SetDBName("shop").
Build()
if err != nil {
fmt.Println("MySQL build error:", err)
} else {
fmt.Printf("MySQL config: %+v\n", mysqlCfg)
}
// SQLite example
sqliteCfg, err := NewDBBuilder("sqlite").
SetFilePath("/tmp/app.db").
Build()
if err != nil {
fmt.Println("SQLite build error:", err)
} else {
fmt.Printf("SQLite config: %+v\n", sqliteCfg)
}
}
این کد، مفهوم الگوی بیلدر را در Go به شکلی بسیار ساده و شفاف پیادهسازی کرده است.
ساختار DBConfig
شامل تمام فیلدهای لازم اتصال به پایگاه داده مثل نوع درایور، نام کاربری، رمز عبور، هاست، پورت، نام پایگاه داده، مسیر فایل (برای SQLite) و SSLMode است. DBBuilder
یک سازنده مرحلهای است که این فیلدها را به صورت زنجیروار مقداردهی میکند.
متدهای SetUser
، SetPassword
، SetHost
و بقیه، امکان پر کردن فیلدها به شکل خوانا و زنجیروار را فراهم میکنند. متد Build
در پایان پیکربندی را اعتبارسنجی میکند، برای SQL فیلدهای User
و DBName
باید پر شوند، برای SQLite مسیر فایل الزامی است و پورت باید بین ۱ تا ۶۵۵۳۵ باشد. اگر خطایی باشد، ارور باز میگردد و در غیر این صورت پیکربندی معتبر تحویل داده میشود.
در تابع main
دو مثال واقعی وجود دارد: یکی برای MySQL که تمام فیلدهای مرتبط پر شده و دیگری برای SQLite که تنها مسیر فایل مشخص شده است. این روش باعث میشود کد خوانا، قابل گسترش و ایمن باشد و ساخت پیکربندیهای مختلف پایگاه داده ساده و منعطف انجام شود. این پیادهسازی در عین سادگی نشان میدهد که چگونه میتوان با استفاده از الگوی Builder از پیچیدگیهای ایجاد اشیای بزرگ و متنوع کاست.
9.1.6.4 کاربرد ها #
در مواردی که اشیاء ما پارامترهای زیادی دارند و پیچیده هستند، استفاده از الگوی سازنده به کمک ما میآید. در مواردی مثل:
- کانکشن های دیتابیس
- فرم یا UIهای پیچیده
- لاگر
9.1.6.4.1 چه زمانی نباید از الگوی Builder استفاده کنیم؟ #
ساخت اشیاء ساده
- اگر شیء شما تنها چند پارامتر ساده دارد و ساخت آن راحت است، Builder پیچیدگی را زیاد میکند.
ملاحظات عملکردی
- در برنامههایی که کارایی مهم است، فراخوانیهای اضافی و ایجاد آبجکتهای موقت در Builder ممکن است باعث کاهش کارایی شود، بهویژه وقتی ساخت شیء مکرر است.
اشیاء immutable و ساده
- اگر شیء ثابت و با فیلدهای نهایی است و ساخت آن ساده است، میتوان از سازندههای معمولی یا factory method استفاده کرد.
افزایش پیچیدگی کد
- ایجاد یک Builder برای هر شیء پیچیده ممکن است کد را طولانی و پیچیده کند.
- اگر شیء نیاز به ساخت مرحلهای ندارد، Builder میتواند اضافه باشد.
وابستگی زیاد به محصول
- اگر Builder و محصول خیلی به هم وابسته باشند، تغییر در محصول نیازمند تغییر در Builder است و انعطافپذیری کاهش مییابد.