9.1.6 الگو Builder

9.1.6 الگو Builder

9.1.6.1 توضیحات #

در دنیای طراحی نرم‌افزار، یکی از چالش‌های رایج، ساختاشیاء پیچیده با پارامترهای متعدد و متنوع است. فرض کنید قصد دارید یک شیء پیکربندی برای اتصال به پایگاه داده بسازید. بسته به نوع پایگاه داده ای که می خواهید (MySQL، PostgreSQL، SQLite و …)، ممکن است به مجموعه‌ای متفاوت از پارامترها نیاز داشته باشید: نام کاربری و گذرواژه، میزبان و پورت، نام پایگاه داده یا حتی مسیر فایل. اگر بخواهیم همه‌ی این موارد را با یک سازنده ساده مدیریت کنیم، به زودی با توابعی پر از پارامترهای اختیاری و ترتیب‌های گیج‌کننده مواجه خواهیم شد.

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

الگوی بیلدر وقتی کمک کننده است که بخواهید یک شیء پیچیده با پارامترهای زیاد را مرحله به مرحله و خواناتر بسازید، بدون اینکه درگیر سازنده‌های طولانی و گیج‌کننده بشوید.

9.1.6.2 دیاگرام #

sequenceDiagram autonumber actor Client participant Builder as DBBuilder participant Config as DBConfig Note over Client,Builder #D3E4CD: Chainable setters Client->>Builder: NewDBBuilder(driver) Client->>Builder: SetHost(host) Client->>Builder: SetPort(port) Client->>Builder: SetUser(user) / SetDBName(db) / SetFilePath(path) Note over Builder,Config #FCE9D5: Build validates inputs Client->>Builder: Build() Builder->>Builder: validate driver rules (user/dbname or filepath) Builder->>Builder: validate port range alt validations pass Builder->>Config: return configured DBConfig Client->>Config: use config (e.g., connect) else validations fail Builder-->>Client: return error end

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 کاربرد ها #

در مواردی که اشیاء ما پارامترهای زیادی دارند و پیچیده هستند، استفاده از الگوی سازنده به کمک ما می‌آید. در مواردی مثل:

  1. کانکشن های دیتابیس
  2. فرم یا UI‌های پیچیده
  3. لاگر

9.1.6.4.1 چه زمانی نباید از الگوی Builder استفاده کنیم؟ #

  1. ساخت اشیاء ساده

    • اگر شیء شما تنها چند پارامتر ساده دارد و ساخت آن راحت است، Builder پیچیدگی را زیاد می‌کند.
  2. ملاحظات عملکردی

    • در برنامه‌هایی که کارایی مهم است، فراخوانی‌های اضافی و ایجاد آبجکت‌های موقت در Builder ممکن است باعث کاهش کارایی شود، به‌ویژه وقتی ساخت شیء مکرر است.
  3. اشیاء immutable و ساده

    • اگر شیء ثابت و با فیلدهای نهایی است و ساخت آن ساده است، می‌توان از سازنده‌های معمولی یا factory method استفاده کرد.
  4. افزایش پیچیدگی کد

    • ایجاد یک Builder برای هر شیء پیچیده ممکن است کد را طولانی و پیچیده کند.
    • اگر شیء نیاز به ساخت مرحله‌ای ندارد، Builder می‌تواند اضافه باشد.
  5. وابستگی زیاد به محصول

    • اگر Builder و محصول خیلی به هم وابسته باشند، تغییر در محصول نیازمند تغییر در Builder است و انعطاف‌پذیری کاهش می‌یابد.