أنماط التصميم في جافا

نمط الاستراتيجية

15 دقيقة الدرس 5 من 13

نمط الاستراتيجية

نمط الاستراتيجية (Strategy) هو أحد أكثر الأنماط السلوكية توظيفًا في مشاريع Java الاحترافية. فكرته الجوهرية بسيطة: عرّف عائلةً من الخوارزميات، وأخفِ كل خوارزمية خلف واجهة مشتركة، واجعلها قابلة للتبادل في وقت التشغيل — دون الحاجة إلى تعديل المكوّنات التي تستخدمها. يُجسّد النمط مبدأ الفتح/الإغلاق مباشرةً: الكود مفتوح للتوسعة (أضف خوارزمية جديدة) ومغلق للتعديل (الكود القائم لا يتغيّر).

المشكلة في غياب Strategy

تخيّل وحدة دفع تدعم بطاقات الائتمان وPayPal والتحويل البنكي. بدون Strategy، تضع المسوّدة الأولى كل المنطق في كلاس واحد بسلسلة if/else أو switch:

// Anti-pattern: توزيع بالـ switch public class PaymentProcessor { public void pay(String method, double amount) { if (method.equals("CARD")) { // منطق البطاقة } else if (method.equals("PAYPAL")) { // منطق PayPal } else if (method.equals("BANK")) { // منطق التحويل البنكي } // كل طريقة دفع جديدة تستلزم تعديل هذا الكلاس } }

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

بنية النمط

يتكوّن النمط من ثلاثة أدوار:

  • واجهة Strategy — العقد الذي يجب أن تلتزم به كل خوارزمية.
  • الاستراتيجيات الملموسة — كلاس لكل خوارزمية يُنفّذ الواجهة.
  • السياق (Context) — يحمل مرجعًا لاستراتيجية ما، يفوّض العمل إليها، ويستطيع تبديلها في وقت التشغيل.
الفكرة المحورية: السياق يعرف أن لديه استراتيجيةً لكنه لا يعرف أيّها بالتحديد. الكود الاستدعائي هو من يتّخذ هذا القرار ويحقنها — مثال واضح على مبدأ "البرمجة للواجهة لا للتنفيذ".

مثال Java كنوني — معالجة المدفوعات

// واجهة Strategy @FunctionalInterface public interface PaymentStrategy { void pay(double amount); } // الاستراتيجيات الملموسة public class CreditCardPayment implements PaymentStrategy { private final String cardNumber; public CreditCardPayment(String cardNumber) { this.cardNumber = cardNumber; } @Override public void pay(double amount) { System.out.printf("Charged $%.2f to card ending %s%n", amount, cardNumber.substring(cardNumber.length() - 4)); } } public class PayPalPayment implements PaymentStrategy { private final String email; public PayPalPayment(String email) { this.email = email; } @Override public void pay(double amount) { System.out.printf("Sent $%.2f via PayPal to %s%n", amount, email); } } // السياق public class PaymentContext { private PaymentStrategy strategy; public PaymentContext(PaymentStrategy strategy) { this.strategy = strategy; } // تبديل الاستراتيجية في وقت التشغيل public void setStrategy(PaymentStrategy strategy) { this.strategy = strategy; } public void checkout(double amount) { strategy.pay(amount); } }

الاستخدام واضح ولا يحتوي على أي تفريع شرطي داخل PaymentContext:

PaymentContext ctx = new PaymentContext(new CreditCardPayment("4111111111111234")); ctx.checkout(99.99); // Charged $99.99 to card ending 1234 ctx.setStrategy(new PayPalPayment("user@example.com")); ctx.checkout(49.00); // Sent $49.00 via PayPal to user@example.com

Lambda كاستراتيجية (Java 8+)

لاحظ تعليق @FunctionalInterface على PaymentStrategy — لها تحديدًا دالة مجردة واحدة. هذا يجعل أي تعبير lambda استراتيجيةً ملموسة صالحة، ممّا يُغني عن كتابة كلاسات منفصلة للخوارزميات البسيطة:

// كلاس مجهول — أسلوب قديم ومطوّل PaymentStrategy cryptoOld = new PaymentStrategy() { @Override public void pay(double amount) { System.out.printf("Paying $%.2f in crypto%n", amount); } }; // Lambda — أسلوب Java الحديث والنظيف PaymentStrategy crypto = amount -> System.out.printf("Paying $%.2f in crypto%n", amount); ctx.setStrategy(crypto); ctx.checkout(200.00);

مراجع الدوال (method references) تعمل بنفس الأسلوب حين تمتلك دالة قائمة مسبقًا التوقيع الصحيح:

public class PaymentLogger { public static void logPayment(double amount) { System.out.println("Audit: $" + amount); } } // مرجع دالة كاستراتيجية ctx.setStrategy(PaymentLogger::logPayment); ctx.checkout(150.0);
متى تختار lambda ومتى تختار كلاسًا كاملًا: استخدم lambda حين تكون الخوارزمية تعبيرًا واحدًا متماسكًا أو بضعة أسطر بلا حالة داخلية. أنشئ كلاسًا مسمّى حين تحتاج الاستراتيجية لحقن تبعيات (كـ cardNumber)، أو تمتلك حالة قابلة للتغيّر، أو تحتاج لاختبار وحدة مستقل بالاسم.

مثال حقيقي — الترتيب بـ Comparator

أنت تستخدم Strategy يوميًا بالفعل. java.util.Comparator هي واجهة Strategy. Collections.sort() وList.sort() هما السياق:

record Employee(String name, int salary) {} List<Employee> team = List.of( new Employee("Zara", 95_000), new Employee("Alice", 80_000), new Employee("Bob", 110_000) ); // الاستراتيجية الأولى: ترتيب بالاسم List<Employee> byName = team.stream() .sorted(Comparator.comparing(Employee::name)) .toList(); // الاستراتيجية الثانية: ترتيب تنازلي بالراتب List<Employee> bySalaryDesc = team.stream() .sorted(Comparator.comparingInt(Employee::salary).reversed()) .toList();

خط الـ stream (السياق) لا يتغيّر؛ فقط Comparator (الاستراتيجية) يختلف. طبّق مصمّمو JDK هذا النمط حتى لا تضطر أبدًا لتعديل List لإضافة ترتيب جديد.

Strategy مقارنةً بأنماط مشابهة

  • Template Method — يُثبّت الهيكل في كلاس أساسي ويترك الخطوات للوراثة. Strategy يفوّض الخوارزمية كاملةً عبر التركيب. الأفضل دومًا التركيب على الوراثة.
  • State — يبدو متطابقًا في الكود (واجهة + سياق بمرجع)، لكن State ينتقل من تلقاء نفسه؛ Strategy تُبدَّل بواسطة الكود الاستدعائي. إن قرّر الكائن أي استراتيجية يستخدم تاليًا فهو State.
  • Command — يُغلّف طلبًا ككائن وكثيرًا ما يدعم التراجع. Strategy يُغلّف خوارزمية؛ والنمطان يُدمجان كثيرًا (Command يختار من بين Strategies).

المقايضات ومتى لا تستخدم النمط

النمط قوي لكنه ليس بلا ثمن:

  • العملاء بحاجة لمعرفة الاستراتيجيات. المنادي يحتاج أن يعرف أي استراتيجية يحقن. إن كان منطق الاختيار معقدًا، انقله إلى مصنع أو طبقة تهيئة لتحافظ على نظافة العميل.
  • مبالغة لخيارَين فقط. إن كان علمٌ منطقي (boolean) بين فرعَين بسيطَين مستقرًّا، تضيف هرمية Strategy تعقيدًا دون فائدة. طبّقه حين تتوقع عائلةً متنامية من الخوارزميات أو حاجةً لتبديل وقت تشغيل.
  • انفجار الواجهات. كل عائلة خوارزميات متمايزة تحتاج واجهتها الخاصة. احتفظ بحجم مناسب — واجهة واحدة لكل محور تباين.
لا تلجأ إلى Strategy بشكل انعكاسي. اسأل أولًا: هل الخوارزميات قابلة للتبادل فعلًا؟ وهل تحتاج حقًا للتبديل في وقت التشغيل؟ إن كانت الإجابة نعم لكلتيهما، يستحق النمط مكانه. وإلّا، فدالة مساعدة بسيطة أو تعبير switch عادي قد يكون أوضح.

الخلاصة

يُغلّف نمط Strategy الخوارزميات القابلة للتبادل خلف واجهة مشتركة، مما يتيح تبديل السلوك في وقت التشغيل دون تعديل السياق. في Java الحديثة، تحوّل @FunctionalInterface أي Strategy إلى هدف للـ lambdas ومراجع الدوال، مما يُقلّل الكود الزائد تقليلًا كبيرًا. تراه في كل مكان في JDK — Comparator وPredicate وRunnable وExecutorService كلها Strategy في العمل. طبّقه حين يكون لديك عائلة خوارزميات تتباين مستقلةً عن الكود الذي يستخدمها.