آموزش الگو های طراحی نرم افزار: راهنمای دیزاین پترن ها با کد

معماری و توسعه نرم‌افزار
آموزش الگو های طراحی نرم افزار: راهنمای دیزاین پترن ها با کد

مهندسی نرم افزار تکرار چرخ از ابتدا نیست؛ الگو های طراحی (Design Patterns) چکیده سه دهه تجربیات جمعی برجسته‌ترین معماران نرم افزار در حل مسائل تکرارشونده ساختار شیءگرا، ترکیب ماژول ها و تبادل پیام هستند. در این راهنمای جامع و مرجع، ۲۳ الگوی کلاسیک باند چهار نفره (GoF) را در سه دسته آفرینشی، ساختاری و رفتاری به همراه مدرن‌ترین الگو های ابری و توزیع شده با کد نویسی واقعی و کالبدشکافی عمیق معماری بررسی می‌کنیم.

پارادایم الگو های طراحی: از بحران کد اسپاگتی تا معماری ساخت یافته

در پروژه هایی نرم افزاری کوچک، نوشتن کدهای سریع و سرراست ممکن است در کوتاه‌مدت پاسخگوی نیازهای تجاری باشد. اما با گذشت زمان، اضافه شدن فیچرهای جدید، تغییر مداوم نیازمندی ها و بزرگ‌تر شدن تیم های فنی، کدهایی که فاقد مرزبندی ساخت‌یافته هستند دچار پدیده‌ای مرگبار به نام فرسایش معماری (Architectural Degradation) یا همان «کد اسپاگتی» می‌شوند. در این وضعیت، کوچک‌ترین تغییر در یک ماژول، اثرات جانبی پیش‌بینی‌نشده‌ای در گوشه های دورافتاده سیستم ایجاد می‌کند، تست پذیری به صفر نزدیک می‌شود و هزینه توسعه هر فیچر جدید تصاعدی افزایش می‌یابد.

الگو های طراحی (Design Patterns) زبان مشترک مهندسان نرم افزار در سراسر جهان هستند. یک الگوی طراحی، قطعه‌کدی آماده برای کپی‌پیست در پروژه نیست؛ بلکه یک چارچوب فکری، قالب انتزاعی و نقشه مهندسی اثبات‌شده برای حل یک چالش ساختاری در سیستم های نرم افزاری است. زمانی که یک مهندس در جلسه طراحی سیستم می‌گوید: «برای حل این مساله از الگوی Decorator استفاده می‌کنیم»، تمام اعضای تیم بلافاصله ساختار کلاس ها، نحوه تزریق وابستگی، شیوه ترکیب اشیاء و خطوط ارتباطی را بدون نیاز به صدها خط توضیح درک می‌کنند.

پیوند ناگسستنی اصول پنج گانه SOLID با الگو های طراحی

بسیاری از توسعه دهندگان الگو های طراحی را به عنوان مجموعه‌ای از ترفندهای ایزوله حفظ می‌کنند؛ در حالی که ریشه و شالوده تمامی الگو های طراحی در اصول پنج‌گانه SOLID نهفته است:

  • اصل مسئولیت واحد (SRP): هر کلاس یا الگو باید تنها یک دلیل برای تغییر داشته باشد. به عنوان مثال، الگوی Facade مسئولیت ساده سازی دسترسی را از منطق پیچیده زیرسیستم ها تفکیک می‌کند.
  • اصل باز-بسته (OCP): سیستم ها باید برای توسعه باز (Open for Extension) و برای تغییر بسته (Closed for Modification) باشند. الگوی Strategy بارزترین تجلی OCP است؛ به گونه‌ای که افزودن یک الگوریتم جدید هرگز نیازمند دستکاری کدهای تست‌شده قبلی نیست.
  • اصل جایگزینی لیسکوف (LSP): زیرکلاس ها باید بدون ایجاد خطا جایگزین کلاس پایه شوند. الگوی Template Method و Factory Method به شدت به رعایت این اصل پایبند هستند.
  • اصل تفکیک اینترفیس (ISP): کلاینت ها نباید به متدهایی که به آن ها نیازی ندارند وابسته شوند. الگوی Adapter پلی برای هماهنگ سازی اینترفیس های کوچک و متمرکز است.
  • اصل وارونگی وابستگی (DIP): ماژول های سطح بالا نباید به ماژول های سطح پایین وابسته باشند؛ هر دو باید به انتزاع (Interfaces) تکیه کنند. تمامی الگو های ساخت شیء با تکیه بر این اصل، کلاینت را از جزئیات کلاس های واقعی بی‌نیاز می‌سازند؛ اصلی که زیربنای اصول معماری تمیز (Clean Architecture) را نیز تشکیل می‌دهد.

تبارشناسی و تاریخچه: ۲۳ الگوی باند چهار نفره (GoF)

در سال ۱۹۹۴، چهار مهندس نرم افزار برجسته شامل اریک گاما (Erich Gamma)، ریچارد هلم (Richard Helm)، رالف جانسون (Ralph Johnson) و جان ولیسیدس (John Vlissides) که در جامعه مهندسی به عنوان Gang of Four (GoF) شناخته می‌شوند، کتاب جریان ساز خود را منتشر کردند. آن ها پس از تحلیل صدها سیستم نرم افزاری مقیاس پذیر در دنیای واقعی، ۲۳ الگوی کلاسیک را مستند کرده و آن ها را بر اساس هدف (Purpose) و قلمرو (Scope) به سه دسته بنیادین تقسیم نمودند:

ماتریس دسته بندی سه گانه الگو های طراحی GoF
ماتریس رده بندی الگو های کلاسیک GoF: تفکیک بر اساس اهداف آفرینشی، ساختاری و رفتاری اشیاء

تفاوت مرزی Pattern، Framework و Algorithm

برای جلوگیری از سوءتفاهم‌های معماری، درک تمایز میان این سه مفهوم اساسی ضرورت دارد:

  • الگوریتم (Algorithm): توالی گام‌به‌گام و دقیق از دستورالعمل ها برای حل یک مساله محاسباتی معین (مانند مرتب سازی، جستجو یا ضرب ماتریس) با تحلیل دقیق زمان و حافظه که مبانی آن در مبانی تحلیل پیچیدگی و نماد Big-O کالبدشکافی شده است. الگوریتم بر روی داده ها و جریان محاسبات تمرکز دارد.
  • الگوی طراحی (Design Pattern): توضیحی انتزاعی از نحوه سازمان‌دهی کلاس ها و اشیاء برای حل یک چالش ساختاری در طراحی سیستم. الگو ها وابسته به نوع زبان یا فریم ورک نیستند و نحوه اتصالات شیءگرا را هدایت می‌کنند.
  • فریم ورک (Framework): یک معماری نیمه‌کامل و ملموس از کدهای پیش‌نوشته‌شده است که اسکلت برنامه را در اختیارتان قرار داده و با استفاده از قانون «هالیوود» (Don't call us, we'll call you - Inversion of Control) کدهای شما را فراخوانی می‌کند. فریم ورک ها خود از ده ها الگوی طراحی در هسته درونی‌شان بهره می‌برند.

الگو های آفرینشی (Creational Patterns): مدیریت هوشمند چرخه حیات اشیاء

الگو های آفرینشی وظیفه مهار فرآیند ساخت و نمونه سازی اشیاء را بر عهده دارند. استفاده مستقیم و بی‌محابا از کلیدواژه new در سرتاسر کدهای تجاری، پیوستگی سخت (Tight Coupling) به وجود می‌آورد؛ زیرا کلاینت مستقیماً به یک پیاده سازی مشخص وابسته می‌شود. الگو های آفرینشی این نقطه اتصال را قطع کرده و فرآیند ساخت را در لایه‌ای کنترل‌شده انتزاع می‌کنند:

فلوچارت تصمیم گیری انتخاب الگوی آفرینشی مناسب
درخت تصمیم‌گیری الگو های آفرینشی: انتخاب بهینه میان Singleton، Factory Method، Abstract Factory، Builder و Prototype

۱. الگوی تک نمونه (Singleton): تیغ دو لبه معماری

الگوی Singleton تضمین می‌کند که از یک کلاس در کل چرخه حیات اپلیکیشن، دقیقا و تنها یک نمونه (Single Instance) ایجاد شود و یک نقطه دسترسی عمومی (Global Access Point) به آن فراهم می‌آورد.

کاربردهای پروداکشن: مدیریت مخزن کانکشن به پایگاه داده (Connection Pool)، مدیریت بارگذاری فایل های تنظیمات سراسری سیستم، کلاینت های رجیستری سرویس های و ماژول های کش درون‌حافظه‌ای.

چالش های Concurrency و مهار Race Condition: در زبان های چندریسمانی (Multi-threaded) مانند جاوا، سی‌شارپ و گو، پیاده سازی اولیه Singleton دچار تداخل همزمانی می‌شود؛ یعنی اگر دو ترد به طور همزمان متد getInstance() را صدا بزنند، ممکن است دو شیء مجزا ساخته شود. برای حل این مشکل از تکنیک های Double-Checked Locking و متغیرهای volatile یا sync.Once در زبان Go استفاده می‌شود.

هشدار معمارانه: استفاده افراطی از Singleton آن را به یک ضدالگوی خطرناک (Anti-Pattern) تبدیل می‌کند. سینگلتون تست های واحد را به شدت دشوار می‌سازد (زیرا وضعیت سراسری را بین تست ها حفظ می‌کند) و وابستگی های پنهان ایجاد می‌نماید. در سیستم های مدرن، چرخه حیات سینگلتون معمولاً به جای کد نویسی مستقیم، توسط کانتینر های تزریق وابستگی (DI Containers) مدیریت می‌شود.

۲. الگو های کارخانه (Factory Method و Abstract Factory)

زمانی که کلاینت نباید بداند دقیقا چه کلاسی ساخته می‌شود، الگو های کارخانه وارد میدان می‌شوند:

  • Factory Method: یک اینترفیس برای ساخت شیء تعریف می‌کند، اما مسئولیت تصمیم‌گیری درباره نوع کلاس نهایی را به زیرکلاس ها واگذار می‌نماید. به عنوان مثال، در یک ماژول ارسال پیام، متد createNotificationDriver() بسته به تنظیمات کاربر می‌تواند شیء SmsDriver، EmailDriver یا PushDriver را تولید و بازگرداند.
  • Abstract Factory: اینترفیسی برای تولید خانواده‌ای از اشیاء وابسته یا مرتبط بدون مشخص کردن کلاس های عینی آن‌هاست. یک نمونه کلاسیک، طراحی رابط کاربری مولتی‌پلتفرم است؛ جایی که UIFactory می‌تواند برای ویندوز خانواده دکمه، چک‌باکس و پنجره ویندوزی، و برای مکینتاش خانواده کامپوننت های نیتیو مک را بازگرداند.

۳. الگوی سازنده (Builder): ساخت گام به گام اشیاء پیچیده

اگر کلاسی دارید که سازنده (Constructor) آن بیش از ۵ یا ۱۰ پارامتر اختیاری می‌گیرد (پدیده‌ای که اصطلاحاً به آن Telescoping Constructor می‌گویند)، ساختار کدهای شما به شدت شکننده خواهد شد. الگوی Builder فرآیند ساخت اشیاء پیچیده را به مراحل ترتیبی و خوانا تقسیم می‌کند.

بیلدر با ارائه متدهای زنجیره‌ای (Fluent API) مانند builder.setHost('db.internal').setPort(8080).enableTLS().build() خوانایی کد را به اوج می‌رساند و امکان ساخت اشیاء تغییرناپذیر (Immutable Objects) را پس از اعتبارسنجی کامل داده ها در مرحله نهایی فراهم می‌سازد.

۴. الگوی پیش نمونه و پروتوتایپ: تکثیر بهینه اشیاء سنگین در حافظه

هنگامی که هزینه ایجاد یک نمونه شیء جدید به دلیل کوئری های سنگین دیتابیس یا محاسبات فشرده بسیار بالا باشد، الگوی Prototype امکان همسان سازی سریع از روی نمونه موجود در حافظه را فراهم می‌سازد. در پیاده سازی این رویکرد، تفکیک دقیق میان کپی سطحی و کپی عمیق ساختار داده ها الزامی است تا ارجاعات حافظه ناخواسته میان نمونه ها به اشتراک گذاشته نشوند.

// Factory Method & Fluent Builder Implementation in TypeScript

// 1. Factory Method Pattern for Payment Processors
interface PaymentProcessor {
    processPayment(amount: number): Promise<string>;
}

class ZarinpalProcessor implements PaymentProcessor {
    async processPayment(amount: number): Promise<string> {
        return `پرداخت مبلغ ${amount} ریال با موفقیت از طریق درگاه زرین‌پال انجام شد.`;
    }
}

class StripeProcessor implements PaymentProcessor {
    async processPayment(amount: number): Promise<string> {
        return `Payment of $${amount} successfully processed via Stripe.`;
    }
}

abstract class PaymentService {
    // Factory Method
    abstract createProcessor(): PaymentProcessor;

    async checkout(amount: number): Promise<void> {
        const processor = this.createProcessor();
        const result = await processor.processPayment(amount);
        console.log(`[Checkout Audit]: ${result}`);
    }
}

class IranMarketPaymentService extends PaymentService {
    createProcessor(): PaymentProcessor {
        return new ZarinpalProcessor();
    }
}

// 2. Fluent Builder Pattern with Validation & Immutability
class DatabaseConnectionConfig {
    readonly host: string;
    readonly port: number;
    readonly database: string;
    readonly poolSize: number;
    readonly sslEnabled: boolean;

    constructor(builder: DatabaseConnectionBuilder) {
        this.host = builder.host;
        this.port = builder.port;
        this.database = builder.database;
        this.poolSize = builder.poolSize;
        this.sslEnabled = builder.sslEnabled;
    }
}

class DatabaseConnectionBuilder {
    host: string = "localhost";
    port: number = 5432;
    database: string = "app_db";
    poolSize: number = 10;
    sslEnabled: boolean = false;

    setHost(host: string): this {
        this.host = host;
        return this;
    }

    setPort(port: number): this {
        if (port <= 0 || port > 65535) throw new Error("شماره پورت نامعتبر است");
        this.port = port;
        return this;
    }

    setDatabase(db: string): this {
        this.database = db;
        return this;
    }

    setPoolSize(size: number): this {
        this.poolSize = Math.max(1, size);
        return this;
    }

    enableSSL(): this {
        this.sslEnabled = true;
        return this;
    }

    build(): DatabaseConnectionConfig {
        return new DatabaseConnectionConfig(this);
    }
}

// Client Usage
const dbConfig = new DatabaseConnectionBuilder()
    .setHost("db.production.internal")
    .setPort(5432)
    .setDatabase("enterprise_orders")
    .setPoolSize(50)
    .enableSSL()
    .build();

الگو های ساختاری (Structural Patterns): مهندسی ترکیب و یکپارچه سازی اشیاء

الگو های ساختاری بر نحوه اتصال، ترکیب و سوار شدن کلاس ها و اشیاء بر روی یکدیگر تمرکز دارند تا ساختارهای بزرگ‌تر و در عین حال منعطف‌تری شکل گیرد. شعار بنیادین الگو های ساختاری این است: «ترکیب شیءگرا بر ارث‌بری ارجحیت دارد (Favor Composition over Inheritance)»؛ زیرا ارث‌بری صلب‌ترین شکل وابستگی در دنیای برنامه نویسی است:

مقایسه معماری الگو های ساختاری Adapter، Decorator، Facade و Proxy
کالبدشکافی تفاوت های الگو های ساختاری: انطباق با Adapter، گسترش با Decorator، ساده سازی با Facade و مهار دسترسی با Proxy

۱. الگوی آداپتور (Adapter): پل ارتباطی اینترفیس های ناهمگون

الگوی Adapter اینترفیس یک کلاس موجود را به اینترفیس دیگری که کلاینت انتظار دارد تبدیل می‌کند. آداپتور به کلاس‌هایی اجازه همکاری می‌دهد که در غیر این صورت به دلیل اینترفیس های ناسازگار هرگز نمی‌توانستند با هم کار کنند.

سناریوی پروداکشن: فرض کنید سیستم فروشگاهی شما با اینترفیس استاندارد PaymentGateway کار می‌کند که متدی به نام pay(amount: Money) دارد. حال می‌خواهید یک درگاه پرداخت بانکی قدیمی با ورودی های متفاوت ریالی و نام های متد عجیب ادغام کنید. به جای دستکاری کدهای تست‌شده کل فروشگاه، یک کلاس BankApiAdapter می‌نویسید که اینترفیس استاندارد سیستم را پیاده کرده و درخواست ها را به متدهای درگاه قدیمی ترجمه می‌نماید.

۲. الگوی دکوراتور (Decorator): گسترش رفتار در زمان اجرا

الگوی Decorator به شما اجازه می‌دهد تا قابلیت ها و رفتارهای جدید را در زمان اجرا (Runtime) و به صورت داینامیک به یک شیء منفرد متصل کنید، بدون آنکه بر رفتار سایر اشیاء همان کلاس تاثیری بگذارید.

دکوراتور جایگزین هوشمندانه زیرکلاس سازی است. به جای آنکه برای هر ترکیب از امکانات (مثلا پیامی که هم رمزنگاری شده باشد و هم فشرده سازی) یک زیرکلاس مجزا بسازید (که منجر به انفجار تعداد کلاس ها می‌شود)، دکوراتورهایی می‌سازید که شیء اصلی را مانند لایه های پیاز احاطه می‌کنند: new CompressionDecorator(new EncryptionDecorator(new BaseDataSource())).

۳. الگوی نما (Facade): ورودی ساده به دنیای پیچیده

الگوی Facade یک رابط ساده و سطح‌بالا به یک زیرسیستم پیچیده شامل ده ها کلاس، سرویس و تعامل ارائه می‌دهد. کلاینت نیازی ندارد بداند در پشت صحنه چه مراحلی از احراز هویت، اعتبارسنجی موجودی انبار، تماس با درگاه مالیات و ثبت اسناد حسابداری انجام می‌شود؛ فساد یک متد صریح مانند orderFacade.placeOrder(cart) در اختیار کلاینت می‌گذارد و هماهنگی زیرسیستم ها را کپسوله می‌کند.

۴. الگوی پروکسی (Proxy): مهار دسترسی و بهینه سازی

الگوی Proxy یک شیء واسط یا جانشین (Surrogate) برای شیء اصلی فراهم می‌آورد تا دسترسی به آن را کنترل کند. پروکسی دقیقا همان اینترفیس شیء هدف را پیاده سازی می‌کند تا کلاینت متوجه تفاوت نشود:

  • پروکسی مجازی (Virtual Proxy / Lazy Loading): تا زمانی که کلاینت متد سنگین شیء را صدا نزند، نمونه اصلی در حافظه بارگذاری نمی‌شود (مانند لود تصاویر حجیم یا اوراق بهادار در سامانه های مالی).
  • پروکسی امنیتی (Protection Proxy): بررسی سطح دسترسی کاربر (RBAC) قبل از تفویض درخواست به شیء عملیاتی اصلی.
  • پروکسی کشینگ (Caching Proxy): نگه داری نتایج فراخوانی های سنگین دیتابیس در حافظه موقت و بازگرداندن سریع پاسخ های تکراری بدون فشار به پایگاه داده.
// Decorator & Adapter Patterns in Go

package main

import (
	"fmt"
	"time"
)

// 1. Decorator Pattern for Caching & Logging
type UserRepository interface {
	GetUserByID(id string) (string, error)
}

// Concrete Component
type PostgresUserRepository struct{}

func (p *PostgresUserRepository) GetUserByID(id string) (string, error) {
	// شبیه سازی کوئری زمان‌بر به پایگاه داده
	time.Sleep(50 * time.Millisecond)
	return fmt.Sprintf("کاربر شماره %s از پایگاه داده PostgreSQL", id), nil
}

// Logging Decorator
type LoggingUserRepoDecorator struct {
	inner UserRepository
}

func (l *LoggingUserRepoDecorator) GetUserByID(id string) (string, error) {
	start := time.Now()
	res, err := l.inner.GetUserByID(id)
	fmt.Printf("[METRICS] GetUserByID(%s) executed in %v\n", id, time.Since(start))
	return res, err
}

// Caching Decorator
type CachingUserRepoDecorator struct {
	inner UserRepository
	cache map[string]string
}

func (c *CachingUserRepoDecorator) GetUserByID(id string) (string, error) {
	if val, found := c.cache[id]; found {
		fmt.Printf("[CACHE HIT] بازخوانی سریع کاربر %s از حافظه موقت\n", id)
		return val, nil
	}
	res, err := c.inner.GetUserByID(id)
	if err == nil {
		c.cache[id] = res
	}
	return res, err
}

// 2. Adapter Pattern for Legacy Third-Party API
type LegacyBillingAPI struct{}

func (l *LegacyBillingAPI) MakeOldSoapPayment(accountNumber string, cents int) bool {
	fmt.Printf("[SOAP Legacy] شارژ حساب %s به مبلغ %d سنت\n", accountNumber, cents)
	return true
}

// Modern Payment Interface
type ModernPaymentGateway interface {
	Charge(accountID string, dollars float64) error
}

// Adapter
type LegacyBillingAdapter struct {
	legacyService *LegacyBillingAPI
}

func (a *LegacyBillingAdapter) Charge(accountID string, dollars float64) error {
	cents := int(dollars * 100)
	success := a.legacyService.MakeOldSoapPayment(accountID, cents)
	if !success {
		return fmt.Errorf("خطا در پرداخت با درگاه قدیمی")
	}
	return nil
}

الگو های رفتاری (Behavioral Patterns): دینامیک تبادل پیام و توزیع مسئولیت

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

گردش کار الگو های رفتاری Strategy، Observer و State در مدیریت رویدادها
جریان تبادل پیام در الگو های رفتاری: مقایسه تعویض الگوریتم با Strategy، جریان رویدادی با Observer و انتقال وضعیت در State

۱. الگوی استراتژی (Strategy): رهایی از شرط های تودرتو

الگوی Strategy خانواده‌ای از الگوریتم ها را تعریف می‌کند، هر یک را در یک کلاس مستقل کپسوله می‌سازد و آن ها را در زمان اجرا قابل تعویض می‌نماید.

اگر در کد خود با بلاک های غول‌پیکر switch/case یا زنجیره های بی‌پایان if-else برای انتخاب شیوه محاسبه تخفیف، روش مرتب سازی، یا نحوه تسویه‌حساب مواجه هستید، سیستم شما تشنه الگوی Strategy است. با این الگو، کلاینت یک شیء Context را با یک استراتژی معین مقداردهی کرده و بدون تغییر منطق فراخوانی، الگوریتم محاسباتی را تغییر می‌دهد.

۲. الگوی ناظر (Observer): بنیان معماری رویداد محور

الگوی Observer یک رابطه یک‌به‌چند (One-to-Many) میان اشیاء تعریف می‌کند، به طوری که اگر وضعیت یک شیء (Subject) تغییر کند، تمامی اشیاء وابسته به آن (Observers) به صورت خودکار مطلع و به‌روزرسانی می‌شوند.

این الگو سنگ‌بنای سیستم های رویداد محور، Event Listeners در فرانت اند و سیستم های انتشار وقایع دامنه (Domain Events) در متدولوژی طراحی دامنه محور (DDD) است. شیء Subject هیچ اطلاعی از کلاس های بتنی ناظران ندارد و صرفا متد notify() را روی اینترفیس ناظر فراخوانی می‌کند که بالاترین سطح استقلال ارتباطی را تضمین می‌نماید.

۳. الگوی وضعیت (State): مهار ماشین های وضعیت متناهی

الگوی State به یک شیء اجازه می‌دهد تا زمانی که وضعیت درونی‌اش تغییر می‌کند، رفتار خود را به کلی دگرگون سازد؛ به گونه‌ای که به نظر برسد کلاس شیء تغییر کرده است.

به عنوان مثال، یک سفارش خرید (Order) وضعیت های مختلفی دارد: Draft، Paid، Processing، Delivered و Cancelled. در هر وضعیت، متد cancel() رفتاری کاملا متفاوت از خود نشان می‌دهد (در وضعیت پیش نویس سریع لغو می‌شود، اما در وضعیت در حال ارسال نیازمند فرآیند مرجوعی است). به جای نوشتن ده ها شرط درون‌خطی، هر وضعیت در یک کلاس مستقل پیاده شده و انتقالات حالت (State Transitions) به شکلی تمیز و ایمن کنترل می‌شوند.

۴. الگوی فرمان (Command): کپسوله سازی درخواست و تاریخچه عملیات

الگوی Command یک درخواست را در قالب یک شیء مستقل کپسوله می‌کند. این کپسوله سازی به شما اجازه می‌دهد تا درخواست ها را به عنوان پارامتر به متدها پاس دهید، آن ها را در صف (Queue) زمان بندی کنید، لاگ اجرای آن ها را ذخیره نمایید و قابلیت حیاتی Undo / Redo را با ذخیره پشته فرمان ها پیاده سازی فرمایید. تغییرات اعمال‌شده در این الگو به راحتی در مخازن کدهای توزیع شده با استفاده از سیستم کنترل نسخه گیت (Git) رهگیری و پایش می‌شوند.

# Strategy & Observer Patterns in Python

from abc import ABC, abstractmethod
from typing import List, Dict, Any

# 1. Strategy Pattern for Dynamic Discount Calculations
class DiscountStrategy(ABC):
    @abstractmethod
    def calculate(self, order_total: float) -> float:
        pass

class RegularCustomerStrategy(DiscountStrategy):
    def calculate(self, order_total: float) -> float:
        return order_total  # بدون تخفیف

class VIPCustomerStrategy(DiscountStrategy):
    def calculate(self, order_total: float) -> float:
        return order_total * 0.85  # ۱۵ درصد تخفیف ویژه مشتریان VIP

class BlackFridayStrategy(DiscountStrategy):
    def calculate(self, order_total: float) -> float:
        return order_total * 0.70  # ۳۰ درصد تخفیف بلک فرایدی

class OrderContext:
    def __init__(self, total_amount: float, strategy: DiscountStrategy):
        self.total_amount = total_amount
        self.strategy = strategy

    def set_strategy(self, strategy: DiscountStrategy) -> None:
        self.strategy = strategy

    def get_final_price(self) -> float:
        return self.strategy.calculate(self.total_amount)


# 2. Observer Pattern for Order Status Events
class OrderObserver(ABC):
    @abstractmethod
    def on_order_status_changed(self, order_id: str, new_status: str) -> None:
        pass

class EmailNotifier(OrderObserver):
    def on_order_status_changed(self, order_id: str, new_status: str) -> None:
        print(f"[Email Notification] ارسال ایمیل به کاربر: وضعیت سفارش {order_id} به '{new_status}' تغییر یافت.")

class InventoryManager(OrderObserver):
    def on_order_status_changed(self, order_id: str, new_status: str) -> None:
        if new_status == "PAID":
            print(f"[Inventory Audit] کسر موجودی فیزیکی انبار برای سفارش تاییدشده {order_id}.")

class OrderSubject:
    def __init__(self, order_id: str):
        self.order_id = order_id
        self.status = "DRAFT"
        self._observers: List[OrderObserver] = []

    def attach(self, observer: OrderObserver) -> None:
        self._observers.append(observer)

    def detach(self, observer: OrderObserver) -> None:
        self._observers.remove(observer)

    def set_status(self, new_status: str) -> None:
        self.status = new_status
        self.notify_all()

    def notify_all(self) -> None:
        for obs in self._observers:
            obs.on_order_status_changed(self.order_id, self.status)

الگو های مدرن ابری و سیستم های توزیع شده (Cloud-Native Patterns)

با گذار صنعت نرم افزار از معماری های مونولیتیک به میکرو سرویس هایی ابری، چالش های سیستم به لایه شبکه، خطاهای مقطعی و عدم یکپارچگی داده ها منتقل شد. مهندسان مدرن کاتالوگ الگو های GoF را با الگو های توزیع شده غنی‌تر ساختند:

الگو های مدرن ابری و سیستم های توزیع شده: Circuit Breaker، Outbox و Sidecar
الگو های معماری ابری: مهار شکست آبشاری با Circuit Breaker، ارسال تضمینی داده با Outbox و ایزولاسیون با Sidecar

۱. الگوی مدارشکن (Circuit Breaker): جلوگیری از فروپاشی آبشاری

در سامانه های توزیع شده، اگر یکی از سرویس هایی وابسته (Downstream Service) دچار قطعی یا کندی شدید شود، سرویس هایی فراخوان با انباشته شدن ریسمان ها (Thread Starvation) و اتمام منابع مواجه شده و کل کلاستر دچار فروپاشی زنجیره‌ای خواهد شد.

الگوی Circuit Breaker مانند فیوز برق مینیاتوری در ساختمان عمل می‌کند. این الگو به صورت یک ماشین وضعیت دارای ۳ فاز عمل می‌کند:

  • حالت بسته (Closed): در شرایط نرمال، درخواست ها آزادانه عبور می‌کنند و نرخ شکست مانیتور می‌شود.
  • حالت باز (Open): اگر درصد خطای فراخوانی ها در یک پنجره زمانی مشخص از حد آستانه (Threshold) فراتر رود، فیوز می‌پرد. در این حالت، تمام درخواست ها بلافاصله و بدون تماس با سرور مقصد رد می‌شوند (Fail-Fast) و کدهای Fallback پاسخ جایگزین را به کاربر برمی‌گردانند.
  • حالت نیمه‌باز (Half-Open): پس از گذشت یک زمان استراحت (مثلا ۳۰ ثانیه)، مدار به تعداد اندکی درخواست اجازه عبور می‌دهد تا وضعیت ریکاوری سرویس بررسی شود. در صورت سلامت، مدار به حالت Closed بازمی‌گردد.

۲. الگوی جعبه ارسال تراکنشی (Transactional Outbox)

هنگامی که یک میکرو سرویس باید هم پایگاه داده داخلی خود را به‌روزرسانی کند و هم پیامی را به صف پیام ها (مانند کافکا یا RabbitMQ) ارسال کند، با چالش نوشتن دوگانه (Dual-Write Problem) روبرو می‌شویم؛ زیرا هیچ تراکنش توزیع شده سراسری برای هماهنگی هر دو سیستم وجود ندارد.

الگوی Transactional Outbox این مشکل را با ظرافت ریاضی حل می‌کند: تغییرات تجاری و رویداد خروجی هر دو در یک تراکنش اتمیک محلی (ACID Transaction) در دیتابیس رابطه‌ای ثبت می‌شوند (رویداد در جدولی به نام outbox نوشته می‌شود). سپس یک پردازشگر پس‌زمینه با تکنیک Change Data Capture (CDC) رویدادها را خوانده و با تضمین تحویل حداقل یک‌بار (At-Least-Once Delivery) به بروکر پیام ارسال می‌کند.

۳. الگوی سایدکار (Sidecar Pattern) در ارکستراسیون کانتینر ها

الگوی Sidecar اجزای کمکی سیستم (مانند مدیریت لاگ، احراز هویت دوجانبه mTLS، مانیتورینگ متریک ها و پروکسی ترافیک) را در یک کانتینر مجزا در کنار کانتینر اصلی اپلیکیشن مستقر می‌کند. این الگو استاندارد استقرار ابزار های مش سرویس (مانند Envoy و Istio) در محیط های کانتینری مدرن است که اصول زیرساختی آن با کانتینرسازی استاندارد در راهنمای جامع داکر از صفر تا پروداکشن همگام است.

// Production Circuit Breaker Pattern Implementation in TypeScript

enum CircuitState {
    CLOSED,    // نرمال: درخواست ها ارسال می‌شوند
    OPEN,      // مدار قطع: درخواست ها سریعا رد می‌شوند (Fail-Fast)
    HALF_OPEN  // نیمه‌باز: تست تدریجی سلامت سرویس مقصد
}

interface CircuitBreakerOptions {
    failureThreshold: number;  // تعداد خطای متوالی مجاز قبل از قطع مدار
    recoveryTimeout: number;   // زمان انتظار قبل از ورود به حالت نیمه‌باز (میلی‌ثانیه)
}

class CircuitBreaker<T> {
    private state: CircuitState = CircuitState.CLOSED;
    private failureCount: number = 0;
    private lastFailureTime: number = 0;

    constructor(
        private readonly action: () => Promise<T>,
        private readonly fallback: () => Promise<T>,
        private readonly options: CircuitBreakerOptions = { failureThreshold: 3, recoveryTimeout: 10000 }
    ) {}

    async execute(): Promise<T> {
        const now = Date.now();

        // بررسی گذار از OPEN به HALF_OPEN پس از اتمام زمان استراحت
        if (this.state === CircuitState.OPEN) {
            if (now - this.lastFailureTime > this.options.recoveryTimeout) {
                console.log("[Circuit Breaker] تغییر وضعیت به HALF-OPEN: تست احیای سرویس...");
                this.state = CircuitState.HALF_OPEN;
            } else {
                console.warn("[Circuit Breaker] مدار در وضعیت OPEN است؛ فراخوانی به سمت Fallback هدایت شد.");
                return this.fallback();
            }
        }

        try {
            const result = await this.action();
            // موفقیت در حالت HALF_OPEN مدار را کاملا ریست می‌کند
            if (this.state === CircuitState.HALF_OPEN) {
                console.log("[Circuit Breaker] تست موفق بود؛ مدار به وضعیت CLOSED بازگشت.");
                this.reset();
            }
            return result;
        } catch (error) {
            this.handleFailure();
            return this.fallback();
        }
    }

    private handleFailure(): void {
        this.failureCount++;
        this.lastFailureTime = Date.now();

        if (this.state === CircuitState.HALF_OPEN || this.failureCount >= this.options.failureThreshold) {
            console.error(`[Circuit Breaker] آستانه خطا (${this.failureCount}) رد شد؛ مدار به وضعیت OPEN تغییر یافت!`);
            this.state = CircuitState.OPEN;
        }
    }

    private reset(): void {
        this.failureCount = 0;
        this.state = CircuitState.CLOSED;
    }
}

ضدالگو ها (Anti-Patterns) و تله های خطرناک در مهندسی نرم افزار

الگو های طراحی ابزار های قدرتمندی هستند، اما استفاده نادرست از آن ها می‌تواند اثراتی مخرب‌تر از کدهای بی‌ساختار بر جای بگذارد:

  • تله بیش‌مهندسی (Over-Engineering / Patternitis): برخی مهندسان تازه‌کار تلاش می‌کنند تمام ۲۳ الگوی GoF را در یک پروژه ساده اعمال کنند! اصل YAGNI (You Aren't Gonna Need It) و اصل سادگی KISS همواره بر پیچیدگی الگو ها اولویت دارند. از الگو زمانی استفاده کنید که درد واقعی آن را احساس کرده باشید، نه برای نشان دادن تسلط فنی.
  • هیولای Singleton به عنوان متغیر سراسری: استفاده از Singleton برای دور زدن معماری پاک و ایجاد انبار متغیرهای سراسری، بدترین ضدالگوی کد نویسی است که تست پذیری را منهدم می‌سازد.
  • تبدیل Facade به کلاس خدا (God Object): اگر یک کلاس Facade به تنهایی هزاران خط کد پیدا کند و خودش منطق تجاری را اجرا نماید، از هدف انتزاعی خود منحرف شده است. وظیفه فساد تنها هدایت ترافیک است، نه اجرای مستقیم الگوریتم ها.

ماتریس جامع مقایسه الگو های کلیدی

الگوی طراحی دسته اصلی هدف محوری پیچیدگی پیاده سازی میزان کاربرد در پروداکشن
Singleton آفرینشی تک‌نمونه سراسری و مدیریت منابع محدود پایین (حساس به Concurrency) متوسط (با احتیاط)
Factory Method آفرینشی تفکیک منطق ساخت از منطق تجاری متوسط بسیار بالا
Builder آفرینشی ساخت گام‌به‌گام اشیاء پیچیده و پارامتریک متوسط بسیار بالا
Adapter ساختاری انطباق اینترفیس های ناهمخوان و کدهای Legacy پایین بسیار بالا
Decorator ساختاری گسترش پویا رفتارها بدون وراثت صلب متوسط بسیار بالا
Strategy رفتاری کپسوله سازی الگوریتم های قابل تعویض در ران‌تایم پایین تا متوسط فوق‌العاده بالا
Observer رفتاری اطلاع‌رسانی یک‌به‌چند و رویداد محوری متوسط فوق‌العاده بالا
Circuit Breaker ابری / توزیع شده جلوگیری از شکست آبشاری در ریزسرویس های پیشرفته حیاتی در کلود

نقشه راه تسلط و جمع بندی: بازآفرینی کد به سمت الگو ها (Refactoring to Patterns)

تسلط بر الگو های طراحی یک مهارت شناختی است که تفکر مهندسی شما را از نوشتن کدهای متوالی (Imperative) به سمت طراحی معماری های منعطف، مقیاس پذیر و پایدار هدایت می‌کند. بهترین رویکرد در توسعه نرم افزار مدرن، «بازآفرینی کد به سمت الگو ها (Refactoring to Patterns)» است؛ یعنی کار را با ساده‌ترین ساختار ممکن آغاز کنید و زمانی که بوی بد کد (Code Smell) مانند تکثیر کدهای تکراری، شرط های طولانی یا وابستگی های پیچیده پدیدار شد، سیستم را به سمت الگوی طراحی متناسب سوق دهید.

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

امتیاز و نظرات

۰.۰ (۰ نظر)
برای ثبت دیدگاه باید وارد حساب کاربری شوید. ورود

هنوز نظری ثبت نشده است. اولین نفر باشید.