Strategy Pattern
Strategy Pattern هو نمط تصميم سلوكي يسمح للمطورين بتعريف مجموعة من الخوارزميات أو السلوكيات، ووضع كل منها في class منفصل، وجعلها قابلة للتبديل في runtime. بدلاً من كتابة كتل شرطية كبيرة للاختيار بين سلوكيات مختلفة، يتيح Strategy Pattern للتطبيق تحديد كائن strategy الصحيح واستخدامه من خلال interface مشتركة.
يعد هذا النمط مفيدًا جدًا في البرمجة كائنية التوجه عندما يكون لدى التطبيق طرق متعددة لتنفيذ نفس المهمة. تشمل الأمثلة طرق الدفع، وحسابات الخصم، وقواعد تكلفة الشحن، وخوارزميات الفرز، وتنسيقات التصدير، وقنوات الإعلام، وقواعد التحقق من الصحة، واستراتيجيات التسعير.
مقدمة
في المقالات السابقة من سلسلة Design Patterns، ناقشنا الأنماط الإبداعية مثل Factory، Abstract Factory، Builder، وPrototype، بالإضافة إلى الأنماط الهيكلية مثل Adapter، Decorator، وFacade، وRepository. تساعد هذه الأنماط المطورين على إنشاء الكائنات، وربط الأنظمة، وتنظيم الكود.
ينتمي Strategy Pattern إلى أنماط التصميم السلوكية. تركز الأنماط السلوكية على كيفية تواصل الكائنات، وكيفية توزيع المسؤوليات، وكيف يمكن أن يتغير السلوك دون صعوبة الحفاظ على الكود.
يعد Strategy أحد الأنماط السلوكية الأكثر عملية لأن العديد من التطبيقات الحقيقية تحتاج إلى التبديل بين القواعد أو الخوارزميات المختلفة. بدون Strategy Pattern، غالبًا ما يصبح هذا المنطق قائمة طويلة من عبارات if أو حالات التبديل. باستخدام Strategy Pattern، يتم وضع كل سلوك في class الخاص به، مما يجعل الكود أكثر وضوحًا وأسهل في التوسيع.
ما هو Strategy Pattern؟
Strategy Pattern هو نمط تصميم يحدد interface مشتركة لمجموعة من السلوكيات القابلة للتبديل. يتم تنفيذ كل سلوك في class منفصل يسمى strategy. يستخدم class الرئيسي، والذي يُطلق عليه غالبًا السياق، strategy من خلال interface دون معرفة تفاصيل التنفيذ الدقيقة.
بعبارات بسيطة، يسمح Strategy Pattern للتطبيق باختيار كيفية القيام بشيء ما دون تغيير class الذي يستخدم السلوك.
على سبيل المثال، قد يقوم أحد تطبيقات التجارة الإلكترونية بحساب الخصومات بشكل مختلف للعملاء العاديين والعملاء المميزين والحملات الموسمية ورموز القسيمة. بدلاً من وضع جميع قواعد الخصم داخل طريقة واحدة كبيرة، يمكن تنفيذ كل قاعدة خصم باعتبارها strategy منفصلة.
لماذا يعد Strategy Pattern مهمًا
يعد Strategy Pattern مهمًا لأنه يساعد في تقليل المنطق الشرطي المعقد. عندما تتم معالجة العديد من السلوكيات داخل class باستخدام عبارات if أو Switch، يصبح من الصعب قراءة class واختباره وتوسيعه.
في كل مرة تتم إضافة سلوك جديد، يجب تعديل class الأصلي. وهذا يزيد من خطر كسر السلوك الحالي. يحل Strategy Pattern هذه المشكلة من خلال السماح بإضافة سلوك جديد باعتباره class جديد يتبع نفس interface.
وهذا يدعم المبدأ المفتوح المغلق: يجب أن يكون البرنامج مفتوحًا للتوسيع ولكنه مغلق للتعديل.
مشكلة بدون Strategy Pattern
تخيل تطبيقًا يحسب تكلفة الشحن بناءً على طريقة التسليم. بدون Strategy Pattern، قد يبدو الرمز كما يلي:
class ShippingCalculator
{
public function calculate(string $method, float $weight): float
{
if ($method === 'standard') {
return $weight * 2;
}
if ($method === 'express') {
return $weight * 5;
}
if ($method === 'international') {
return $weight * 10 + 20;
}
throw new InvalidArgumentException('Invalid shipping method.');
}
}يعمل هذا الرمز، ولكن يصبح من الصعب الحفاظ عليه عند إضافة المزيد من طرق الشحن. يجب أن يتغير ShippingCalculator class في كل مرة يتم فيها تقديم طريقة جديدة.
يقوم Strategy Pattern بنقل كل حساب شحن إلى class الخاص به.
مثال Strategy Pattern الأساسي في PHP
أولاً، حدد strategy interface المشترك:
interface ShippingStrategy
{
public function calculate(float $weight): float;
}الآن قم بإنشاء concrete strategy classes:
class StandardShippingStrategy implements ShippingStrategy
{
public function calculate(float $weight): float
{
return $weight * 2;
}
}
class ExpressShippingStrategy implements ShippingStrategy
{
public function calculate(float $weight): float
{
return $weight * 5;
}
}
class InternationalShippingStrategy implements ShippingStrategy
{
public function calculate(float $weight): float
{
return ($weight * 10) + 20;
}
}يحتوي كل strategy على قاعدة واحدة محددة لحساب الشحن. القواعد منفصلة وأسهل في الاختبار.
إنشاء السياق Class
يستخدم السياق class strategy من خلال interface:
class ShippingCalculator
{
public function __construct(
private ShippingStrategy $strategy
) {
}
public function calculate(float $weight): float
{
return $this->strategy->calculate($weight);
}
}لا تعرف ShippingCalculator ما إذا كانت تستخدم الشحن القياسي أو السريع أو الدولي. إنها تعرف فقط أن لديها ShippingStrategy.
باستخدام Strategy Pattern
يمكن للتطبيق اختيار strategy الصحيح وتمريره إلى السياق:
$strategy = new ExpressShippingStrategy();
$calculator = new ShippingCalculator($strategy);
$cost = $calculator->calculate(3.5);يمكن أن تعمل نفس ShippingCalculator مع أي strategy الذي يقوم بتنفيذ ShippingStrategy interface.
إذا تمت إضافة طريقة تسليم جديدة لاحقًا، فيمكن إنشاء strategy class جديد دون تعديل ShippingCalculator.
الأجزاء الرئيسية من Strategy Pattern
يتضمن Strategy Pattern عادةً هذه الأجزاء الرئيسية:
- Strategy interface: يحدد الطريقة الشائعة التي يجب على كافة الاستراتيجيات تنفيذها.
- استراتيجيات Concrete: Classes التي تنفذ إصدارات مختلفة من السلوك أو الخوارزمية.
- السياق: class الذي يستخدم كائن strategy لتنفيذ السلوك.
- Client code: الكود الذي يحدد أو يوفر strategy الصحيح.
يفصل هذا الهيكل اختيار السلوك عن تنفيذ السلوك.
مثال من العالم الحقيقي: الخصم Strategy
يعد حساب الخصم أحد الأمثلة الأكثر شيوعًا لـ Strategy Pattern. قد يستخدم العملاء أو الحملات المختلفة قواعد خصم مختلفة.
interface DiscountStrategy
{
public function calculate(float $total): float;
}
class NoDiscountStrategy implements DiscountStrategy
{
public function calculate(float $total): float
{
return 0;
}
}
class RegularCustomerDiscountStrategy implements DiscountStrategy
{
public function calculate(float $total): float
{
return $total * 0.05;
}
}
class PremiumCustomerDiscountStrategy implements DiscountStrategy
{
public function calculate(float $total): float
{
return $total * 0.15;
}
}
class SeasonalDiscountStrategy implements DiscountStrategy
{
public function calculate(float $total): float
{
return $total * 0.25;
}
}يتم الآن وضع كل قاعدة خصم في class منفصل. وهذا يجعل من السهل إضافة سلوك الخصم أو إزالته أو تعديله.
باستخدام الخصم Strategy
class DiscountService
{
public function __construct(
private DiscountStrategy $strategy
) {
}
public function getFinalTotal(float $total): float
{
$discount = $this->strategy->calculate($total);
return $total - $discount;
}
}
$service = new DiscountService(new PremiumCustomerDiscountStrategy());
$finalTotal = $service->getFinalTotal(200);لا تحتاج خدمة DiscountService إلى احتواء جميع قواعد الخصم الممكنة. يستخدم فقط strategy المحدد.
مثال من العالم الحقيقي: الدفع Strategy
أنظمة الدفع هي حالة استخدام عملية أخرى. قد يدعم التطبيق طرق دفع مختلفة مثل بطاقة الائتمان أو PayPal أو التحويل البنكي أو الدفع نقدًا عند التسليم.
interface PaymentStrategy
{
public function pay(float $amount): bool;
}
class CreditCardPaymentStrategy implements PaymentStrategy
{
public function pay(float $amount): bool
{
// Pay by credit card
return true;
}
}
class PayPalPaymentStrategy implements PaymentStrategy
{
public function pay(float $amount): bool
{
// Pay by PayPal
return true;
}
}
class BankTransferPaymentStrategy implements PaymentStrategy
{
public function pay(float $amount): bool
{
// Pay by bank transfer
return true;
}
}كل طريقة دفع لها strategy الخاصة بها. يمكن لعملية الدفع استخدام أي دفعة strategy من خلال نفس interface.
الخروج مع الدفع Strategy
class CheckoutService
{
public function __construct(
private PaymentStrategy $paymentStrategy
) {
}
public function checkout(float $amount): bool
{
return $this->paymentStrategy->pay($amount);
}
}
$checkout = new CheckoutService(new PayPalPaymentStrategy());
$checkout->checkout(150);يسهل هذا التصميم إضافة طرق دفع جديدة لاحقًا دون إعادة كتابة خدمة CheckoutService.
اختيار Strategy في Runtime
في التطبيقات الحقيقية، قد يعتمد strategy المحدد على إدخال المستخدم أو التكوين أو قيم قاعدة البيانات أو معلمات request أو قواعد العمل.
يمكن استخدام factory البسيط لاختيار strategy الصحيح:
class PaymentStrategyFactory
{
public function create(string $method): PaymentStrategy
{
return match ($method) {
'credit_card' => new CreditCardPaymentStrategy(),
'paypal' => new PayPalPaymentStrategy(),
'bank_transfer' => new BankTransferPaymentStrategy(),
default => throw new InvalidArgumentException('Invalid payment method.'),
};
}
}يمكن لـ Factory Pattern إنشاء كائن Strategy الصحيح، ويعالج Strategy Pattern هذا السلوك. غالبًا ما يعمل هذان النمطان معًا بشكل جيد.
Strategy Pattern مع Dependency Injection
يعمل Strategy Pattern بشكل جيد مع dependency injection. بدلاً من إنشاء strategy داخل السياق class، يتم تمرير strategy من الخارج.
وهذا يجعل السياق class أسهل في الاختبار وأسهل في التكوين. أثناء الاختبارات، يمكن للمطورين اجتياز ملف strategy المزيف. في الإنتاج، يمكن للتطبيق اجتياز strategy الحقيقي المحدد بواسطة التكوين أو اختيار المستخدم.
class ExportService
{
public function __construct(
private ExportStrategy $strategy
) {
}
public function export(array $data): string
{
return $this->strategy->export($data);
}
}تعتمد خدمة التصدير على abstraction وتظل مستقلة عن تنسيقات التصدير concrete.
مثال من العالم الحقيقي: تصدير Strategy
قد يقوم التطبيق بتصدير البيانات بتنسيقات مختلفة مثل PDF أو Excel أو CSV أو JSON. يتطلب كل تنسيق منطقًا مختلفًا.
interface ExportStrategy
{
public function export(array $data): string;
}
class PdfExportStrategy implements ExportStrategy
{
public function export(array $data): string
{
return 'PDF export content';
}
}
class CsvExportStrategy implements ExportStrategy
{
public function export(array $data): string
{
return 'CSV export content';
}
}
class JsonExportStrategy implements ExportStrategy
{
public function export(array $data): string
{
return json_encode($data);
}
}بدلاً من وضع كل منطق التصدير في class واحد، يكون لكل تنسيق strategy الخاص به. يؤدي هذا إلى تحسين قابلية الصيانة ويجعل اختبار كل تنسيق تصدير أسهل.
Strategy Pattern في Laravel
في تطبيقات Laravel، يعد Strategy Pattern مفيدًا لبوابات الدفع وقواعد الخصم وطرق الشحن وقنوات الإعلام ومصدري الملفات وقواعد التحقق من الصحة ومولدات التقارير.
يمكن لـ Laravel الخاص بـ service container إدخال تطبيق strategy بناءً على التكوين. على سبيل المثال، يمكن لموفر الخدمة ربط interface بـ strategy محدد:
$this->app->bind(
PaymentStrategy::class,
PayPalPaymentStrategy::class
);بالنسبة للاختيار الديناميكي استنادًا إلى بيانات request، يمكن لـ factory أو محلل class اختيار strategy الصحيح وتمريره إلى الخدمة.
Laravel Strategy مثال للمحلل
class PaymentStrategyResolver
{
public function resolve(string $method): PaymentStrategy
{
return match ($method) {
'credit_card' => app(CreditCardPaymentStrategy::class),
'paypal' => app(PayPalPaymentStrategy::class),
'bank_transfer' => app(BankTransferPaymentStrategy::class),
default => throw new InvalidArgumentException('Invalid payment method.'),
};
}
}هذا يبقي وحدة التحكم نظيفة. يمكن لوحدة التحكم أن تطلب من محلل strategy الصحيح بدلاً من احتواء منطق اختيار الدفع.
Strategy Pattern في Symfony
يمكن لتطبيقات Symfony استخدام Strategy Pattern من خلال الخدمات وdependency injection. يمكن تسجيل strategy مختلف كخدمات، ويمكن للمحلل تحديد الخدمة الصحيحة بناءً على التكوين أو إدخال runtime.
يمكن أيضًا استخدام علامات Symfony لتجميع خدمات strategy المتعددة وتنظيمها حسب المفتاح. يعد هذا مفيدًا في التطبيقات الكبيرة حيث يجب أن تكون الاستراتيجيات قابلة للتمديد دون تعديل factory class واحد كبير.
يتناسب Strategy Pattern بشكل طبيعي مع بنية الخدمة Symfony ويساعد في الحفاظ على نظافة منطق العمل.
Strategy Pattern والمبدأ المغلق المفتوح
يدعم Strategy Pattern بقوة المبدأ المفتوح المغلق. تم إغلاق السياق class للتعديل لأنه لا يحتاج إلى التغيير عند إضافة استراتيجيات جديدة. يظل النظام مفتوحًا للتوسيع لأنه يمكن للمطورين إضافة strategy classes جديد.
على سبيل المثال، إذا كان أحد التطبيقات يدعم تصدير PDF وCSV، فيمكن إجراء إضافة تصدير Excel عن طريق إنشاء ExcelExportStrategy class الذي يقوم بتنفيذ نفس interface. لا تحتاج خدمة التصدير إلى التغيير.
وهذا يجعل التصميم أكثر أمانًا وأسهل في الصيانة بمرور الوقت.
Strategy Pattern وPolymorphism
يعتمد Strategy Pattern بشكل كبير على polymorphism. تطبق كافة strategy classes نفس interface، ولكن كل class يوفر سلوكًا مختلفًا.
يستخدم السياق class interface، ويعتمد السلوك الفعلي على كائن concrete strategy الذي تم تمريره في runtime.
هذا مثال عملي لـ polymorphism في تصميم البرامج الحقيقي. يمكن أن ينتج عن استدعاء الأسلوب نفسه نتائج مختلفة اعتمادًا على strategy المحدد.
Strategy Pattern مقابل Factory Pattern
يختلف Strategy Pattern وFactory Pattern، لكن غالبًا ما يتم استخدامهما معًا.
يركز Factory Pattern على إنشاء الكائنات. فهو يقرر الكائن الذي يجب إنشاؤه. يركز Strategy Pattern على السلوك. وهو يحدد خوارزميات أو قواعد قابلة للتبديل.
على سبيل المثال، قد يقوم PaymentStrategyFactory بإنشاء PayPalPaymentStrategy. يعالج factory الإنشاء، بينما يعالج strategy سلوك الدفع.
باختصار، يقوم Factory بإنشاء strategy، ويقوم Strategy بتنفيذ السلوك.
Strategy Pattern مقابل نمط الحالة
يمكن أن يبدو كل من Strategy Pattern وState Pattern متشابهين لأن كلاهما يستخدم كائنات قابلة للتبديل. ومع ذلك، فإن نيتهم مختلفة.
يسمح Strategy Pattern للعميل أو التطبيق باختيار خوارزمية أو سلوك. عادةً ما يمثل strategy المحدد خيارًا مثل طريقة الدفع أو نوع الخصم أو تنسيق التصدير.
يغير نمط الحالة السلوك بناءً على الحالة الداخلية للكائن. على سبيل المثال، قد يتصرف الطلب بشكل مختلف عندما يكون معلقًا أو مدفوعًا أو مشحونًا أو مُلغى.
باختصار، Strategy يتعلق باختيار السلوك، بينما الحالة تتعلق بتغيير السلوك عندما يتحرك الكائن عبر الحالات.
Strategy Pattern مقابل نمط أسلوب القالب
يستخدم نمط أسلوب القالب inheritance لتحديد بنية الخوارزمية في class الأصل مع السماح للطفل classes بتخصيص خطوات محددة.
يستخدم Strategy Pattern التركيب وinterfaces لجعل السلوكيات بأكملها قابلة للتبديل.
عادةً ما يكون Strategy أكثر مرونة لأنه يمكن تغيير الاستراتيجيات في runtime. تكون طريقة القالب مفيدة عندما تكون بنية الخوارزمية ثابتة وتختلف خطوات معينة فقط.
فوائد Strategy Pattern
يوفر Strategy Pattern العديد من الفوائد في تصميم البرامج الموجهة للكائنات.
تشمل الفوائد الرئيسية ما يلي:
- يقلل من عبارات if والتبديل الكبيرة.
- يفصل الخوارزميات المختلفة إلى classes المركزة.
- يجعل السلوك أسهل للاختبار بشكل مستقل.
- يدعم المبدأ المفتوح المغلق.
- يعمل على تحسين المرونة من خلال السماح باختيار سلوك runtime.
- يعمل بشكل جيد مع dependency injection وinterfaces.
- يحسن إمكانية القراءة والصيانة.
- يجعل إضافة سلوك جديد أكثر أمانًا ونظافة.
هذه الفوائد تجعل Strategy Pattern واحدًا من أكثر أنماط التصميم السلوكية فائدة.
عيوب Strategy Pattern
يمكن أن يكون لـ Strategy Pattern أيضًا عيوب. قد يؤدي ذلك إلى زيادة عدد classes في المشروع لأن كل سلوك يحتاج عادةً إلى class الخاص به.
بالنسبة للمنطق البسيط الذي يحتوي على شرط أو شرطين صغيرين فقط، قد يكون Strategy Pattern غير ضروري. إن إضافة interfaces والعديد من classes لمشكلة صغيرة جدًا يمكن أن يجعل فهم الكود أكثر صعوبة.
عيب آخر هو أن كود العميل أو المحلل يجب أن يختار strategy الصحيح. إذا أصبح منطق اختيار strategy معقدًا، فيجب تنظيمه بعناية في factory أو محلل.
متى يتم استخدام Strategy Pattern
استخدم Strategy Pattern عندما يكون لدى التطبيق طرق متعددة لتنفيذ نفس المهمة وقد تتغير هذه السلوكيات أو تنمو بمرور الوقت.
يكون Strategy Pattern مفيدًا عندما:
- لديك خوارزميات متعددة لنفس العملية.
- تريد تجنب الكتل الشرطية الكبيرة.
- يجب تحديد السلوك في runtime.
- يمكن إضافة سلوكيات جديدة في المستقبل.
- تريد اختبار كل سلوك على حدة.
- يجب ألا يعرف class الرئيسي جميع تفاصيل التنفيذ.
- تريد اتباع التصميم المستند إلى interface.
في حالة وجود هذه الشروط، يمكن لـ Strategy Pattern تحسين التصميم بشكل ملحوظ.
متى لا تستخدم Strategy Pattern
لا تستخدم Strategy Pattern عندما يكون السلوك بسيطًا ومن غير المرجح أن يتغير. إذا كانت هناك خوارزمية واحدة فقط أو كانت الشروط صغيرة جدًا، فقد يكون الكود المباشر أكثر وضوحًا.
تجنب Strategy Pattern عندما:
- لا يوجد سوى تنفيذ سلوك واحد.
- المنطق بسيط جدًا بحيث لا يمكن تبرير classes الإضافي.
- هذا النمط يجعل المشروع أكثر صعوبة في التنقل.
- لا يحتاج السلوك إلى التغيير ديناميكيًا.
- طريقة بسيطة أو قيمة التكوين كافية.
Design patterns يجب أن يجعل الكود أكثر نظافة، وليس أكثر تعقيدًا.
الأخطاء الشائعة في Strategy Pattern
أحد الأخطاء الشائعة هو إنشاء استراتيجيات للاختلافات الصغيرة جدًا التي لا تحتاج إلى classes منفصلة. وهذا يمكن أن يخلق تعقيدًا غير ضروري.
هناك خطأ آخر وهو وضع منطق التحديد strategy داخل سياق class. يجب أن يستخدم السياق strategy، ولا يحتوي على بيان تبديل كبير لاختياره.
الخطأ الثالث هو جعل strategy interfaces واسعًا جدًا. يجب أن يحدد strategy interface فقط السلوك الذي تشترك فيه جميع الاستراتيجيات حقًا.
الخطأ الرابع هو خلط سير عمل الأعمال مع منطق strategy. يجب أن يركز كل strategy على سلوك واحد قابل للتبديل.
أفضل الممارسات لـ Strategy Pattern
لاستخدام Strategy Pattern بشكل فعال، يجب على المطورين الحفاظ على تركيز الاستراتيجيات ووضوحها وسهولة استبدالها.
تتضمن أفضل الممارسات المفيدة ما يلي:
- حدد strategy interface بشكل صغير وواضح.
- اجعل كل strategy يركز على سلوك واحد.
- استخدم dependency injection لتوفير الاستراتيجيات.
- استخدم factory أو محلل لاختيار الاستراتيجيات عند الحاجة.
- تجنب وضع منطق التحديد strategy داخل سياق class.
- استخدم أسماء ذات معنى مثل PayPalPaymentStrategy أو PremiumDiscountStrategy.
- اختبر كل strategy بشكل مستقل.
- لا تفرط في استخدام Strategy Pattern للظروف البسيطة.
تساعد هذه الممارسات في إبقاء Strategy Pattern عمليًا وقابلاً للصيانة.
قائمة مرجعية عملية قبل استخدام Strategy Pattern
قبل استخدام Strategy Pattern، يمكن للمطورين طرح هذه الأسئلة:
- هل لدي طرق متعددة لأداء نفس المهمة؟
- هل الكود الحالي مليء بعبارات if أو Switch؟
- هل سيتم إضافة سلوكيات جديدة لاحقا؟
- هل يجب تحديد السلوك عند runtime؟
- هل يمكن اختبار كل سلوك على حدة؟
- هل سيجعل interface التصميم أنظف؟
- هل الهيكل الإضافي يستحق المرونة؟
إذا كانت الإجابة بنعم على العديد من هذه الأسئلة، فقد يكون Strategy Pattern اختيارًا جيدًا للتصميم.
الاستنتاج
Strategy Pattern هو نمط تصميم سلوكي يسمح للمطورين بتحديد خوارزميات أو سلوكيات قابلة للتبديل والتبديل بينها دون تعديل رمز التطبيق الرئيسي. وهو مفيد بشكل خاص لطرق الدفع وقواعد الخصم وحسابات الشحن وتنسيقات التصدير وقواعد التحقق من الصحة وتغيرات السلوك الأخرى.
يعمل Strategy Pattern على تحسين تنظيم الكود عن طريق استبدال الكتل الشرطية الكبيرة بـ strategy المركزة classes. وهو يدعم polymorphism، وdependency injection، والكود النظيفة، والمبدأ المفتوح المغلق.
ومع ذلك، يجب استخدام Strategy Pattern فقط عندما يكون تغير السلوك حقيقيًا وذا معنى. بالنسبة للمنطق البسيط، قد يضيف تعقيدًا غير ضروري. عند تطبيقه بشكل صحيح، يعد Strategy Pattern واحدًا من أقوى الأدوات لإنشاء برامج موجهة للكائنات مرنة وقابلة للصيانة وقابلة للتطوير.

