Go Programming

تسلط بر Go: پیاده‌سازی عملی الگوهای طراحی ضروری

Go (Golang) به سرعت به زبان انتخابی برای زیرساخت‌های بومی ابری و سیستم‌های بک‌اند با عملکرد بالا تبدیل شده است. فلسفه آن بر سادگی، همزمانی صریح و حداقل‌گرایی تأکید دارد. برای توسعه‌دهندگانی که از زبان‌هایی مانند جاوا، C++ یا C# مهاجرت می‌کنند، تمایل به اعمال الگوهای طراحی شیءگرای سنگین طبیعی است. با این حال، هویت متمایز Go—که ترجیح می‌دهد ترکیب را به ارث‌بری و رابط‌ها را به کلاس‌ها ترجیح دهد—رویکردی ظریف را برای طراحی معماری نیاز دارد. این پست بررسی می‌کند که چگونه می‌توان الگوهای طراحی کلاسیک را به کد Go اصیل (Idiomatic) تطبیق داد، تا اطمینان حاصل شود که نرم‌افزار شما هم مستحکم و هم قابل نگهداری باقی می‌ماند.

روش Go: ترکیب به جای ارث‌بری

مهم‌ترین تغییر در تفکر هنگام پذیرش Go، دوری از رابطه «is-a» (ارث‌بری) و حرکت به سمت رابطه «has-a» (ترکیب) است. الگوهای طراحی سنتی اغلب به سلسله‌مراتب عمیق ارث‌بری متکی هستند که Go به صراحت آن‌ها را منع می‌کند. در عوض، توسعه‌دهندگان Go از فیلدهای ناشناس برای جاسازی رفتار و جاسازی رابط‌ها برای تعریف قراردادها استفاده می‌کنند.

برای مثال، اگر شما یک ساختار Database و یک ساختار RedisClient داشته باشید، به جای ایجاد یک کلاس والد StorageEngine، یک رابط تعریف می‌کنید. این کار کد شما را از پیاده‌سازی‌های خاص جدا می‌کند و به شما امکان می‌دهد اجزا را به صورت پویا در زمان اجرا جابجا کنید، ویژگی‌ای که Go را برای آزمایش و طراحی ماژولار به‌طور استثنایی انعطاف‌پذیر می‌سازد.

پیاده‌سازی الگوی استراتژی

الگوی استراتژی خانواده‌ای از الگوریتم‌ها را تعریف می‌کند، هر کدام را کپسوله می‌کند و آن‌ها را قابل تعویض می‌سازد. در Go، این کار به طور طبیعی از طریق رابط‌ها انجام می‌شود. این الگو زمانی که نیاز دارید رفتارها را بر اساس شرایط زمان اجرا تغییر دهید، به ویژه مفید است؛ مانند انتخاب فرمت‌های مختلف گزارش‌دهی یا درگاه‌های پرداخت.

یک سیستم پردازش پرداخت را در نظر بگیرید. ما می‌توانیم یک رابط Payer تعریف کنیم و استراتژی‌های مشخصی برای کارت اعتباری و PayPal پیاده‌سازی کنیم.

package main

import "fmt"

// رابط Payer قرارداد استراتژی را تعریف می‌کند
type Payer interface {
    Pay(amount float64) error
}

// ساختار CreditCard یک استراتژی مشخص را نشان می‌دهد
type CreditCard struct{}

func (c CreditCard) Pay(amount float64) error {
    fmt.Printf("Charging $%.2f via Credit Card\n", amount)
    return nil
}

// ساختار PayPal یک استراتژی مشخص دیگر را نشان می‌دهد
type PayPal struct{}

func (p PayPal) Pay(amount float64) error {
    fmt.Printf("Charging $%.2f via PayPal\n", amount)
    return nil
}

// ProcessPayment نحوه استفاده را نشان می‌دهد
func ProcessPayment(p Payer, amount float64) {
    p.Pay(amount)
}

func main() {
    processor := CreditCard{}
    ProcessPayment(processor, 99.99)
    
    payment := PayPal{}
    ProcessPayment(payment, 49.50)
}

این رویکرد تابع ProcessPayment را نسبت به روش پرداخت زیرین بی‌طرف نگه می‌دارد و از اصل باز/بسته (Open/Closed Principle) پیروی می‌کند. شما می‌توانید روش‌های پرداخت جدید را بدون تغییر در کد موجود اضافه کنید.

الگوی تک‌نمونه (Singleton) در Go

در بسیاری از زبان‌های شیءگرا، تک‌نمونه‌ها از طریق سازنده‌های خصوصی و متدهای استاتیک پیاده‌سازی می‌شوند. Go که فاقد متدهای استاتیک و سازنده‌ها است، وضعیت جهانی را به شیوه‌ای متفاوت مدیریت می‌کند. روش استاندارد برای رفتار شبیه به تک‌نمونه در Go، استفاده از پکیج sync.Once است. این کار تضمین می‌کند که منطق راه‌اندازی دقیقاً یک بار اجرا می‌شود، حتی در محیط‌های بسیار همزمان، و ایمنی رشته‌ای (Thread Safety) را بدون هزینه اضافی قفل‌گذاری صریح فراهم می‌کند.

package main

import (
    "fmt"
    "sync"
)

type Config struct {
    Host string
}

var (
    instance *Config
    once     sync.Once
)

func GetConfig() *Config {
    once.Do(func() {
        instance = &Config{Host: "localhost"}
    })
    return instance
}

این الگو برای مدیریت اتصالات پایگاه داده، نمونه‌های لاگر یا پیکربندی‌های سراسری برنامه که در آن‌ها تکرار ناکارآمد یا از نظر منطقی نادرست است، حیاتی می‌باشد.

الگوی کارخانه برای ایجاد اشیاء

الگوهای کارخانه منطق instantiation (ایجاد نمونه) را انتزاع می‌کنند و به شما امکان می‌دهند اشیاء را بدون افشای منطق ایجاد به مشتری بسازید. در Go، توابع کارخانه صرفاً توابعی هستند که رابط‌ها یا ساختارها را برمی‌گردانند. این روش اصیل است و اغلب بر کارخانه‌های مبتنی بر کلاس ترجیح داده می‌شود.

یک مثال عملی یک کارخانه Shape (شکل) است. بسته به ورودی کاربر، شما یا یک Circle (دایره) یا یک Rectangle (مستطیل) برمی‌گردانید. با برگرداندن یک رابط، کد مشتری تمیز و مستقل از انواع خاص باقی می‌ماند.

type Shape interface {
    Area() float64
}

func NewShape(shapeType string) Shape {
    switch shapeType {
    case "circle":
        return &Circle{radius: 5}
    case "rectangle":
        return &Rectangle{width: 10, height: 20}
    default:
        return nil
    }
}

نتیجه‌گیری

پیاده‌سازی الگوهای طراحی در Go بیشتر درباره پایبندی سخت‌گیرانه به تعاریف GoF (گنگ آو فور) است تا پذیرش اصول اصلی Go: سادگی، رابط‌ها و ترکیب. با ترجیح دادن رابط‌ها بر ارث‌بری و استفاده از اصول اولیه همزمانی مانند sync.Once، می‌توانید سیستم‌های مقیاس‌پذیر، قابل آزمایش و تمیز بسازید. به یاد داشته باشید، هر مشکلی نیاز به یک الگوی طراحی ندارد. گاهی اوقات، یک تابع یا ساختار ساده بهترین راه حل است. از این الگوها به عنوان ابزارهایی برای حل پیچیدگی استفاده کنید، نه به عنوان الزامی برای هر قطعه کد.

Share: