الگوی استراتژی (Strategy Pattern) در #C: جایگزینی رفتار در زمان اجرا
تعریف و هدف اصلی
در بسیاری از سناریوهای برنامهنویسی، یک کلاس ممکن است نیاز به انجام عملی داشته باشد که روش اجرای آن (الگوریتم) در شرایط مختلف، فرق میکند. بدون الگوی استراتژی، توسعهدهنده ممکن است از عبارات شرطی بزرگ (مانند if-else یا switch) برای انتخاب الگوریتم مناسب استفاده کند. این رویکرد به سرعت منجر به کدی میشود که نگهداری آن دشوار، درک آن پیچیده و گسترش آن سخت است (نقض اصل باز-بسته - Open/Closed Principle).
هدف اصلی الگوی استراتژی این است که:
-
خانوادهای از الگوریتمها را تعریف کند (به عنوان مثال، الگوریتمهای مختلف مرتبسازی، روشهای مختلف پرداخت، یا فرمتهای مختلف ذخیرهسازی فایل).
-
هر الگوریتم را در یک کلاس مجزا کپسوله کند.
-
این الگوریتمها را در زمان اجرا قابل تعویض سازد، بدون نیاز به تغییر در کلاس اصلی (Context).
ساختار و اجزای الگو
الگوی استراتژی از سه جزء اصلی تشکیل شده است:
1. استراتژی (Strategy - Interface/Abstract Class)
رابط (Interface) یا کلاس انتزاعی (Abstract Class) که یک قرارداد مشترک برای تمام الگوریتمهای پشتیبانیشده تعریف میکند. این جزء، متدی را اعلام میکند که Context برای اجرای الگوریتم از آن استفاده میکند.
// مثال: رابط استراتژی پرداخت
public interface IPaymentStrategy
{
void Pay(decimal amount);
}
2. استراتژیهای بتنی (Concrete Strategies - Classes)
کلاسهایی که رابط Strategy را پیادهسازی میکنند و حاوی پیادهسازی خاص یک الگوریتم هستند. در حقیقت، هر کلاس Concrete Strategy یک رفتار متفاوت را نشان میدهد.
// مثال: استراتژی پرداخت با کارت اعتباری
public class CreditCardPayment : IPaymentStrategy
{
public void Pay(decimal amount)
{
Console.WriteLine($"پرداخت مبلغ {amount:C} با استفاده از کارت اعتباری.");
// منطق واقعی پرداخت با کارت...
}
}
// مثال: استراتژی پرداخت با PayPal
public class PayPalPayment : IPaymentStrategy
{
public void Pay(decimal amount)
{
Console.WriteLine($"پرداخت مبلغ {amount:C} با استفاده از PayPal.");
// منطق واقعی پرداخت با PayPal...
}
}
3. زمینه (Context - Class)
کلاسی که از یکی از اشیاء Strategy استفاده میکند. Context یک ارجاع به شیء Strategy نگهداری میکند و اجرای عملیات را به آن واگذار میکند. Context معمولاً دارای یک متد (یا سازنده) برای تنظیم و تغییر استراتژی در زمان اجرا است.
// مثال: کلاس زمینه (Context) پردازشگر پرداخت
public class PaymentProcessor
{
private IPaymentStrategy _paymentStrategy;
// سازنده برای تنظیم استراتژی اولیه
public PaymentProcessor(IPaymentStrategy strategy)
{
_paymentStrategy = strategy;
}
// متدی برای تغییر استراتژی در زمان اجرا
public void SetStrategy(IPaymentStrategy strategy)
{
_paymentStrategy = strategy;
}
// متدی که اجرای عملیات را به استراتژی فعلی واگذار میکند
public void ProcessPayment(decimal amount)
{
Console.WriteLine("شروع فرآیند پرداخت...");
_paymentStrategy.Pay(amount);
Console.WriteLine("فرآیند پرداخت تکمیل شد.");
}
}
نحوه استفاده (مثال کلاینت)
در کد کلاینت، میتوانیم به راحتی استراتژی مورد نیاز را انتخاب و در صورت لزوم آن را در زمان اجرا تغییر دهیم:
// 1. تنظیم استراتژی اولیه (مثلاً پرداخت با کارت اعتباری)
var creditCardStrategy = new CreditCardPayment();
var processor = new PaymentProcessor(creditCardStrategy);
Console.WriteLine("پردازش پرداخت اول:");
processor.ProcessPayment(100.00m); // خروجی: پرداخت مبلغ 100.00 با استفاده از کارت اعتباری.
// 2. تغییر استراتژی در زمان اجرا (مثلاً تغییر به PayPal)
Console.WriteLine("\nتغییر استراتژی به PayPal:");
processor.SetStrategy(new PayPalPayment());
Console.WriteLine("پردازش پرداخت دوم:");
processor.ProcessPayment(45.50m); // خروجی: پرداخت مبلغ 45.50 با استفاده از PayPal.
مزایای کلیدی الگوی استراتژی
استفاده از الگوی استراتژی چندین مزیت عمده در طراحی سیستمهای C# فراهم میکند:
-
انطباق با اصول SOLID:
-
اصل باز/بسته (Open/Closed Principle - OCP): سیستم برای توسعه (افزودن استراتژیهای جدید) باز و برای تغییر (در Context و استراتژیهای موجود) بسته است. برای افزودن یک روش پرداخت جدید (مثلاً بیتکوین)، کافی است یک کلاس استراتژی جدید تعریف کنیم بدون دستکاری Context یا استراتژیهای فعلی.
-
اصل مسئولیت یگانه (Single Responsibility Principle - SRP): هر کلاس استراتژی تنها مسئول اجرای یک الگوریتم خاص است.
-
-
کاهش پیچیدگی (کاهش عبارتهای شرطی): از شر درختهای بزرگ if-else یا switch خلاص میشوید، که کد را بسیار خواناتر و سادهتر میکند.
-
قابلیت تستپذیری بالاتر: هر استراتژی به صورت مجزا کپسوله شده است و میتوان آن را بدون نیاز به وابستگیهای Context به راحتی تست واحد (Unit Test) کرد.
-
قابلیت استفاده مجدد از الگوریتم: الگوریتمهای مجزا را میتوان در Contextهای مختلف در سیستم استفاده کرد.
معایب احتمالی
با وجود مزایای فراوان، این الگو معایبی نیز دارد که باید در نظر گرفته شوند:
-
افزایش تعداد کلاسها: حتی برای الگوریتمهای ساده، باید یک رابط و حداقل دو کلاس بتنی تعریف شود که منجر به افزایش حجم کد میشود.
-
پیچیدگی در انتخاب استراتژی: کلاینت باید بداند که کدام استراتژی مناسبترین است و انتخاب و پیکربندی آن را انجام دهد.
تفاوت با الگوی کارخانه (Factory Pattern)
اغلب الگوی استراتژی با الگوهای خلق (Creational Patterns) مانند الگوی کارخانه ساده (Simple Factory) ترکیب میشود.
-
الگوی استراتژی: به چگونگی انجام کار (الگوریتم) مربوط است و انتخاب بین چندین رفتار متفاوت را در زمان اجرا ممکن میسازد.
-
الگوی کارخانه: به خلق اشیاء مربوط است. در یک سیستم مبتنی بر استراتژی، میتوان از یک کارخانه (Factory) استفاده کرد تا بر اساس برخی شرایط ورودی (مثلاً نوع پرداخت)، شیء استراتژی مناسب را تولید کند، و بدین ترتیب مسئولیت انتخاب استراتژی را از کلاینت دور سازد.
استفاده از Delegates و Lambda در C#
در نسخههای مدرن C#، برای پیادهسازی استراتژیهای بسیار ساده و سبک، میتوان از نمایندهها (Delegates) و عبارات لامبدا (Lambda Expressions) به جای تعریف کلاسهای بتنی کامل استفاده کرد. این رویکرد، بخصوص در .NET Core و .NET 5 به بالا، برای صرفهجویی در تعریف کلاسهای کوچک، رایج شده است.
// 1. تعریف یک Delegate به عنوان استراتژی
public delegate void PaymentAction(decimal amount);
// 2. Context جدید برای استفاده از Delegate
public class DelegatePaymentProcessor
{
private PaymentAction _paymentStrategy;
public DelegatePaymentProcessor(PaymentAction strategy)
{
_paymentStrategy = strategy;
}
public void ProcessPayment(decimal amount)
{
Console.WriteLine("شروع فرآیند پرداخت با Delegate...");
_paymentStrategy(amount);
Console.WriteLine("فرآیند پرداخت تکمیل شد.");
}
}
// 3. استفاده در کلاینت با Lambda/متدهای استاتیک
public static class PaymentMethods
{
public static void PayWithCreditCard(decimal amount)
{
Console.WriteLine($"Delegate: پرداخت {amount:C} با کارت.");
}
public static void PayWithPayPal(decimal amount)
{
Console.WriteLine($"Delegate: پرداخت {amount:C} با PayPal.");
}
}
// کلاینت با Lambda:
var delegateProcessor = new DelegatePaymentProcessor(PaymentMethods.PayWithCreditCard);
delegateProcessor.ProcessPayment(50.00m); // استفاده از متد استاتیک
// تغییر استراتژی با Lambda:
delegateProcessor.SetStrategy(
amount => Console.WriteLine($"Delegate: پرداخت {amount:C} با یک سرویس جدید و سریع.")
);
delegateProcessor.ProcessPayment(25.00m); // استفاده از Lambda
این روش با حفظ قابلیت تعویض رفتار در زمان اجرا، نیاز به تعریف کلاسهای اضافی برای استراتژیهای کوچک را از بین میبرد و کد را مختصرتر میسازد.
نتیجهگیری
الگوی استراتژی یک ابزار قدرتمند در جعبه ابزار هر برنامهنویس C# است. با جدا کردن رفتار (الگوریتم) از کلاس میزبان (Context)، این الگو به ما این امکان را میدهد که سیستمی انعطافپذیر، ماژولار و قابل نگهداری بسازیم. این الگو به ویژه در سناریوهایی که نیاز به پیادهسازی چندین روش یا الگوریتم برای یک عمل واحد وجود دارد (مانند مرتبسازی، احراز هویت، یا روشهای محاسبه) بسیار مفید و حیاتی است و به طور مؤثری اصل مهم OCP را در معماری نرمافزار پیاده میکند.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.