متى تستخدم نمط التصميم Strategy بدلاً من سلاسل الشروط الطويلة؟

مقدمة

يواجه المطوّرون كثيراً سلاسل طويلة من شروط if/else أو تعليمات switch عند الحاجة إلى تنفيذ سلوك مختلف بحسب نوع مستخدم، أو وسيلة دفع، أو طريقة شحن، أو صيغة تصدير، أو سياسة تسعير. في البداية تبدو هذه الشروط حلاً مباشراً وسريعاً، لكنها تتحول تدريجياً إلى مصدر للتعقيد وصعوبة الصيانة. هنا يبرز السؤال المهم: متى تستخدم نمط التصميم Strategy بدلاً من سلاسل الشروط الطويلة؟

نمط Strategy هو أحد أنماط التصميم السلوكية، ويهدف إلى فصل مجموعة من الخوارزميات أو قواعد العمل المتبادلة عن السياق الذي يستخدمها. بدلاً من أن تحتوي فئة واحدة على جميع الفروع الشرطية، توضع كل خوارزمية في مكوّن مستقل يلتزم بعقد موحّد. ثم يختار البرنامج الاستراتيجية المناسبة وقت التشغيل أو عند بناء الكائن. النتيجة هي كود أكثر وضوحاً، وأسهل اختباراً، وأقل عرضة للكسر عند إضافة حالات جديدة.

لكن هذا النمط ليس بديلاً تلقائياً لكل شرط في البرنامج. فالاستخدام غير المدروس له قد ينتج عدداً كبيراً من الفئات الصغيرة ويزيد التعقيد بلا فائدة. لذلك يتطلب اتخاذ القرار فهماً دقيقاً لطبيعة الشروط، واحتمال نموها، ومدى اختلاف سلوك كل فرع، ومتطلبات الاختبار والتوسعة المستقبلية.

كيف تتحول سلاسل الشروط إلى مشكلة تصميمية؟

ليست المشكلة في وجود الشرط نفسه؛ فالشرط أداة أساسية في البرمجة. تظهر المشكلة عندما تصبح الفروع الشرطية تمثيلاً لأنواع متعددة من السلوك الكامل، لا مجرد قرار بسيط. خذ مثال خدمة لحساب تكلفة الشحن. قد تبدأ بتفرع واحد بين الشحن المحلي والدولي، ثم تضيف شحن المنتجات الثقيلة، والشحن السريع، والشحن المجاني للحسابات المميزة، وقواعد خاصة لكل شركة نقل. خلال وقت قصير، تصبح الدالة طويلة ومتشابكة.

غالباً ما تكون سلسلة الشروط مرشحة لإعادة التصميم عندما تلاحظ مؤشرات متكررة، مثل تكرار الشرط نفسه في عدة مواضع، أو تضخم دالة واحدة إلى عشرات الأسطر، أو اضطرارك إلى تعديل الفئة المركزية كلما أضفت نوعاً جديداً، أو صعوبة اختبار كل فرع بمعزل عن بقية الفروع. كذلك، إذا كانت الفروع تعتمد على خدمات وبيانات مختلفة، مثل بوابة دفع خارجية أو محرك خصومات أو واجهة شركة شحن، فإن حشر هذه التبعيات في فئة واحدة يضعف تماسكها.

المثال التالي يوضح بداية المشكلة في نظام دفع بسيط:

class PaymentService {
    public function pay(string $method, float $amount): void {
        if ($method === 'card') {
            // التحقق من البطاقة واستدعاء مزود الدفع
            echo "تم الدفع بالبطاقة";
        } elseif ($method === 'bank_transfer') {
            // إنشاء بيانات التحويل البنكي
            echo "تم إنشاء تحويل بنكي";
        } elseif ($method === 'wallet') {
            // الاتصال بالمحفظة الرقمية
            echo "تم الدفع بالمحفظة";
        } elseif ($method === 'cash_on_delivery') {
            // تسجيل الدفع عند الاستلام
            echo "تم اختيار الدفع عند الاستلام";
        } else {
            throw new InvalidArgumentException("وسيلة دفع غير مدعومة");
        }
    }
}

هذا الكود ليس خاطئاً بالضرورة إذا كانت الحالات قليلة وثابتة. لكنه يصبح هشاً عندما تتوسع الأعمال داخل كل فرع: التحقق، التسجيل، معالجة الأخطاء، احتساب الرسوم، إعادة المحاولة، وإرسال الإشعارات. عندها يكون التفرع الطويل إشارة إلى أن الفئة تؤدي مسؤوليات متعددة، وهو ما يتعارض مع مبدأ المسؤولية الواحدة.

الفكرة الأساسية في نمط Strategy ومكوّناته

يعالج نمط Strategy هذه المشكلة عبر تعريف واجهة مشتركة تمثل العملية المتغيرة، ثم إنشاء تنفيذ مستقل لكل سلوك. تسمى الفئة التي تستعمل هذه السلوكيات عادةً «السياق»، بينما تسمى الفئات المنفصلة «الاستراتيجيات». لا يحتاج السياق إلى معرفة تفاصيل التنفيذ؛ يكفيه أن يعرف العقد المشترك الذي تقدمه الاستراتيجية.

في مثال الدفع، يمكن تعريف واجهة باسم PaymentStrategy تحتوي عملية pay. بعد ذلك، تنفذ كل وسيلة دفع هذه الواجهة. بهذه الطريقة، يصبح الفرق بين الدفع بالبطاقة والتحويل البنكي والمحفظة الرقمية معزولاً في فئات مستقلة، بدلاً من تكديسه في خدمة واحدة.

interface PaymentStrategy {
    public function pay(float $amount): void;
}

class CardPaymentStrategy implements PaymentStrategy {
    public function pay(float $amount): void {
        echo "تمت معالجة دفعة بطاقة بقيمة {$amount}";
    }
}

class BankTransferStrategy implements PaymentStrategy {
    public function pay(float $amount): void {
        echo "تم إنشاء تحويل بنكي بقيمة {$amount}";
    }
}

class WalletPaymentStrategy implements PaymentStrategy {
    public function pay(float $amount): void {
        echo "تمت معالجة دفعة محفظة رقمية بقيمة {$amount}";
    }
}

class PaymentService {
    public function __construct(
        private PaymentStrategy $strategy
    ) {}

    public function pay(float $amount): void {
        $this->strategy->pay($amount);
    }
}

الآن أصبحت خدمة الدفع مركزة على تنسيق العملية فقط، بينما انتقلت تفاصيل كل وسيلة إلى الاستراتيجية المختصة بها. ويمكن تغيير السلوك دون تعديل منطق الخدمة:

$service = new PaymentService(new CardPaymentStrategy());
$service->pay(250.00);

$service = new PaymentService(new WalletPaymentStrategy());
$service->pay(250.00);

القاعدة الأهم هنا هي البرمجة مقابل التجريد لا مقابل التنفيذ الملموس. فخدمة الدفع تعتمد على واجهة PaymentStrategy، لا على فئة البطاقة أو المحفظة نفسها. وهذا يقلل الترابط بين أجزاء النظام ويفتح المجال للتبديل والاختبار والتطوير.

الحالات التي يكون فيها Strategy خياراً مناسباً

استخدم نمط Strategy عندما تكون لديك عدة طرق لتنفيذ الهدف ذاته، لكنها تختلف في خطوات التنفيذ أو في التبعيات أو في القواعد التجارية. الهدف في مثال الدفع واحد: إتمام عملية دفع. أما الطرق فتختلف جذرياً: بطاقة، تحويل، محفظة، أو دفع عند الاستلام. هذا الاختلاف الجوهري هو البيئة الطبيعية للنمط.

ويكون النمط مناسباً أيضاً عندما تتوقع إضافة أنواع جديدة بصورة متكررة. في منصة تجارة إلكترونية، قد تضيف بوابة دفع جديدة كل عدة أشهر. إذا كان النظام مبنياً على سلسلة switch مركزية، فسيتطلب كل إضافة تعديل الفئة القائمة وإعادة اختبار جميع الفروع. أما مع الاستراتيجيات، فيكفي إنشاء فئة جديدة وتنفيذ الواجهة ثم تسجيلها في آلية الاختيار المناسبة.

من الحالات المهمة كذلك الحاجة إلى تغيير السلوك أثناء التشغيل. قد يختار العميل طريقة شحن مختلفة قبل تأكيد الطلب، أو قد يتغير أسلوب احتساب الضريبة تبعاً لبلد الشحن، أو قد يفعّل النظام خوارزمية ضغط مختلفة بحسب نوع الملف. يسمح Strategy بتبديل الخوارزمية دون تغيير بنية السياق نفسه.

كما يفيد النمط عندما يصبح اختبار الفروع صعباً. في السلسلة الشرطية الكبيرة، قد تضطر إلى تجهيز بيانات كثيرة للوصول إلى فرع بعينه. أما الاستراتيجية المنفصلة فيمكن اختبارها كوحدة مستقلة، مع حقن نسخ تجريبية من خدماتها الخارجية. وهذا يجعل الاختبارات أدق ويقلل زمن تشخيص الأخطاء.

وأخيراً، يعد النمط مناسباً عندما تكون الشروط المتكررة موزعة في أكثر من فئة. إذا كانت طريقة تحديد «نوع العميل» تظهر في حساب الخصومات، وإرسال الرسائل، واحتساب النقاط، فإن المشكلة ليست مجرد شرط طويل، بل قاعدة عمل منتشرة. يمكن أن يوفّر تصميم استراتيجيات أو سياسات موحّدة مركزاً واضحاً لهذه القواعد.

مثال عملي: استراتيجيات احتساب الخصم

لنفترض أن متجراً إلكترونياً يقدم خصومات مختلفة للعملاء: لا خصم للعميل العادي، وخصم ثابت للعميل المميز، وخصم نسبي للعميل الذهبي، وخصم موسمي يعتمد على حد أدنى للطلب. كتابة ذلك ضمن دالة واحدة قد تبدو سهلة، لكن القواعد ستتغير غالباً مع نمو النشاط التجاري.

interface DiscountStrategy {
    public function calculate(float $subtotal): float;
}

class NoDiscountStrategy implements DiscountStrategy {
    public function calculate(float $subtotal): float {
        return 0.0;
    }
}

class PremiumDiscountStrategy implements DiscountStrategy {
    public function calculate(float $subtotal): float {
        return 25.0;
    }
}

class GoldDiscountStrategy implements DiscountStrategy {
    public function calculate(float $subtotal): float {
        return $subtotal * 0.15;
    }
}

class SeasonalDiscountStrategy implements DiscountStrategy {
    public function calculate(float $subtotal): float {
        return $subtotal >= 300 ? $subtotal * 0.20 : 0.0;
    }
}

class OrderPricingService {
    public function __construct(
        private DiscountStrategy $discountStrategy
    ) {}

    public function finalPrice(float $subtotal): float {
        $discount = $this->discountStrategy->calculate($subtotal);
        return max(0, $subtotal - $discount);
    }
}

يسهّل هذا التصميم قراءة كل قاعدة على حدة. كما يمكن لفريق الأعمال تعديل خصم العميل الذهبي دون لمس قواعد الخصم الموسمي. وإذا ظهرت قاعدة جديدة، مثل خصم خاص بالطلاب أو خصم يعتمد على رمز ترويجي، فلا حاجة لتغيير OrderPricingService. يكفي إنشاء استراتيجية جديدة.

لكن اختيار الاستراتيجية لا ينبغي أن يُخفى داخل السياق بطريقة عشوائية. من الأفضل غالباً أن تتولى طبقة منفصلة، مثل مصنع كائنات أو مسجل استراتيجيات أو طبقة التطبيق، مسؤولية تحويل نوع العميل إلى الاستراتيجية المناسبة. بذلك يبقى السياق خالياً من سلسلة شروط جديدة تعيد المشكلة إلى مكان آخر.

class DiscountStrategyFactory {
    public function create(string $customerType): DiscountStrategy {
        return match ($customerType) {
            'premium' => new PremiumDiscountStrategy(),
            'gold' => new GoldDiscountStrategy(),
            'seasonal' => new SeasonalDiscountStrategy(),
            default => new NoDiscountStrategy(),
        };
    }
}

وجود تفرع صغير داخل المصنع مقبول؛ لأنه يركز فقط على اختيار الكائن ولا يحتوي تفاصيل الخوارزميات. وفي التطبيقات الكبيرة يمكن استبدال هذا التفرع بتسجيل الاستراتيجيات في حاوية حقن الاعتمادات أو في خريطة إعدادات، خاصة إذا كانت الإضافات تأتي من وحدات مستقلة.

المزايا التصميمية والاختبارية التي يحققها النمط

أبرز مزايا Strategy هي الالتزام بمبدأ الانفتاح للامتداد والانغلاق للتعديل. أي أن النظام يمكن أن يستقبل سلوكاً جديداً بإضافة فئة، بدلاً من تعديل منطق قديم ومستقر. لا يلغي ذلك الحاجة إلى الاختبار، لكنه يقلل احتمال التأثير غير المقصود في السلوكيات الموجودة.

كذلك يعزز النمط مبدأ المسؤولية الواحدة. فاستراتيجية الشحن السريع مسؤولة عن حساب الشحن السريع فقط، واستراتيجية الدفع بالمحفظة مسؤولة عن التعامل مع المحفظة فقط. هذا الفصل يساعد على فهم الكود، ويجعل أسماء الفئات معبرة عن قواعد العمل بدلاً من وجود كتلة منطقية مبهمة داخل دالة عامة.

أما في الاختبارات، فيمكن اختبار كل استراتيجية بحالات بسيطة ومحددة. ويمكن أيضاً اختبار السياق باستراتيجية وهمية للتأكد من أنه يفوض العمل بشكل صحيح:

class FixedDiscountStrategy implements DiscountStrategy {
    public function calculate(float $subtotal): float {
        return 10.0;
    }
}

$service = new OrderPricingService(new FixedDiscountStrategy());

assert($service->finalPrice(100.0) === 90.0);

هذا النوع من الاختبار لا يحتاج إلى معرفة تفاصيل خصومات العملاء الحقيقية. كما يمكن استبدال الاستراتيجيات الفعلية بنسخ تحاكي فشل خدمة خارجية أو استجابة بطيئة، وهو أمر بالغ الأهمية في أنظمة الدفع والشحن والتكاملات الشبكية.

ومن الفوائد العملية أيضاً إمكان إعادة استخدام الاستراتيجية في أكثر من سياق، بشرط أن يكون عقدها مصمماً بعناية. فخوارزمية احتساب ضريبة معينة قد تستخدمها خدمة الفواتير وخدمة عرض الأسعار معاً، بدلاً من نسخ المنطق في كل مكان.

متى لا يكون Strategy مناسباً وما البدائل الأبسط؟

لا ينبغي تحويل كل تعليمة if إلى مجموعة استراتيجيات. إذا كان لديك شرطان أو ثلاثة، وكانت الفروع قصيرة وثابتة ولا تتوقع توسعاً قريباً، فقد يكون الشرط المباشر أو match أكثر وضوحاً وأقل تكلفة ذهنية. إنشاء واجهة وعدة فئات من أجل طباعة رسالة مختلفة بحسب قيمة منطقية ليس تصميماً أفضل؛ بل هو إفراط في الهندسة.

كذلك لا يكون النمط مثالياً إذا كانت الاختلافات سطحية جداً، مثل تغيير قيمة ثابتة واحدة. في هذه الحالة قد يكون جدول إعدادات أو خريطة قيم كافياً. وإذا كان السلوك يتغير عبر مراحل مترابطة وحالة داخلية معقدة، فقد يكون نمط الحالة State أنسب، لأنه يركز على انتقال الكائن بين حالات متعددة. أما إذا كان المطلوب بناء كائنات مختلفة لا تنفيذ خوارزميات مختلفة، فقد يكون نمط المصنع أكثر ملاءمة.

من سلبيات Strategy أنه يزيد عدد الفئات والكائنات، ويتطلب فهماً إضافياً لمسار اختيار الاستراتيجية. وقد تتحول الواجهة المشتركة إلى واجهة ضخمة إذا كانت بعض الاستراتيجيات تحتاج بيانات لا تحتاجها أخرى. الحل هو تصميم عقد صغير ومتماسك، وتمرير كائن طلب واضح عند الحاجة، بدلاً من زيادة المعاملات غير المستخدمة.

لذلك، قبل تطبيق النمط، اسأل: هل تختلف الفروع في خوارزميات حقيقية؟ هل ستتغير أو تتوسع؟ هل يمكن اختبارها مستقلة؟ هل تتكرر قواعد الاختيار في النظام؟ إذا كانت الإجابات في معظمها بالنعم، فإن الاستثمار في الاستراتيجيات مبرر. أما إذا كانت المشكلة صغيرة ومحدودة، فالبساطة تبقى الخيار الأفضل.

خاتمة

استخدم نمط التصميم Strategy بدلاً من سلاسل الشروط الطويلة عندما تمثل تلك الشروط سلوكيات أو خوارزميات بديلة قابلة للنمو والتغيير، لا قرارات بسيطة عابرة. فهو يفصل تفاصيل التنفيذ عن منطق التنسيق، ويجعل إضافة السلوكيات الجديدة أكثر أماناً، ويعزز قابلية الاختبار والقراءة والصيانة. ومع ذلك، لا تقاس جودة التصميم بعدد الأنماط المستخدمة، بل بمدى ملاءمة الحل للمشكلة. ابدأ بشرط بسيط عندما تكون المشكلة بسيطة، وانتقل إلى Strategy عندما تشير بنية الكود وتطور المتطلبات إلى أن السلوك المتغير أصبح مسؤولية مستقلة تستحق العزل.

تعليقات