نمط Strategy
جزء من قسم Object Oriented Programming في رحلة C++ على Coddy — الدرس 95 من 104.
يحدد نمط الاستراتيجية (Strategy pattern) عائلة من الخوارزميات، ويغلف كل واحدة منها، ويجعلها قابلة للتبديل. يتيح لك ذلك تغيير سلوك الكائن في وقت التشغيل دون تعديل الكود الخاص به - حيث تختلف الخوارزمية بشكل مستقل عن العملاء الذين يستخدمونها.
يتكون النمط من ثلاثة أجزاء: واجهة الاستراتيجية (Strategy interface) التي تعلن عن طريقة الخوارزمية، والاستراتيجيات الملموسة (Concrete Strategies) التي تنفذ تنويعات مختلفة، وسياق (Context) يستخدم استراتيجية:
#include <iostream>
#include <memory>
// Strategy interface (واجهة الاستراتيجية)
class PaymentStrategy {
public:
virtual void pay(int amount) = 0;
virtual ~PaymentStrategy() = default;
};
// Concrete strategies (الاستراتيجيات الملموسة)
class CreditCardPayment : public PaymentStrategy {
public:
void pay(int amount) override {
std::cout << "Paid " << amount << " via Credit Card\n";
}
};
class PayPalPayment : public PaymentStrategy {
public:
void pay(int amount) override {
std::cout << "Paid " << amount << " via PayPal\n";
}
};
// Context (السياق)
class ShoppingCart {
std::unique_ptr<PaymentStrategy> strategy;
public:
void setPaymentMethod(std::unique_ptr<PaymentStrategy> s) {
strategy = std::move(s);
}
void checkout(int total) {
if (strategy) strategy->pay(total);
}
};
int main() {
ShoppingCart cart;
cart.setPaymentMethod(std::make_unique<CreditCardPayment>());
cart.checkout(100);
cart.setPaymentMethod(std::make_unique<PayPalPayment>());
cart.checkout(50);
}لا يعرف ShoppingCart أي وسيلة دفع يستخدمها - فهو ببساطة يستدعي pay() على أي استراتيجية تم تعيينها. يمكنك تبديل الاستراتيجيات أثناء وقت التشغيل (runtime) باستخدام setPaymentMethod()، مما يجعل النظام مرناً وسهل التوسيع بخيارات دفع جديدة.
استخدم نمط Strategy عندما يكون لديك خوارزميات متعددة لمهمة محددة وتريد التبديل بينها ديناميكيًا، أو عندما تريد تجنب العبارات الشرطية لاختيار السلوك.
التحدي
سهللنقم ببناء حاسبة شحن (Shipping Calculator) تستخدم نمط الاستراتيجية (Strategy pattern) لحساب تكاليف التوصيل بناءً على طرق شحن مختلفة. هذا سيناريو عملي حيث تحتاج إلى تبديل الخوارزميات أثناء وقت التشغيل — فقد يتم شحن نفس الطرد عبر البر، أو الجو، أو الشحن السريع، ولكل منها منطق تسعير خاص به.
ستقوم بتنظيم الكود الخاص بك عبر ثلاثة ملفات:
ShippingStrategy.h: حدد واجهة الاستراتيجية واستراتيجيات الشحن الملموسة.أنشئ فئة مجردة
ShippingStrategyمع دالة افتراضية بحتة (pure virtual method) باسمcalculateCost(double weight)تعيد تكلفة الشحن كـ double، بالإضافة إلى مدمر افتراضي (virtual destructor).ثم قم بتنفيذ ثلاث استراتيجيات ملموسة:
GroundShipping— تكلفته1.5لكل وحدة وزن (weight * 1.5)AirShipping— تكلفته4.0لكل وحدة وزن (weight * 4.0)ExpressShipping— تكلفته6.5لكل وحدة وزن بالإضافة إلى رسوم ثابتة قدرها10.0(weight * 6.5 + 10.0)
ShippingService.h: أنشئ فئة السياق (context class) التي تستخدم استراتيجية الشحن.يجب أن تحتوي فئة
ShippingServiceالخاصة بك علىstd::unique_ptr<ShippingStrategy>كعضو خاص. قم بتنفيذ:- دالة
setStrategy(std::unique_ptr<ShippingStrategy> strategy)لتغيير طريقة الشحن - دالة
calculateShipping(double weight)تستخدم الاستراتيجية الحالية لحساب وإرجاع التكلفة
إذا لم يتم تعيين أي استراتيجية عند استدعاء
calculateShipping، فقم بإرجاع0.0.- دالة
main.cpp: توضيح تبديل الاستراتيجيات أثناء وقت التشغيل.اقرأ مدخلين:
- وزن الطرد (double)
- طريقة الشحن:
ground، أوair، أوexpress
أنشئ
ShippingServiceوقم بتعيين الاستراتيجية المناسبة بناءً على طريقة الإدخال. احسب واطبع تكلفة الشحن.ثم انتقل إلى استراتيجية مختلفة (استخدم
airإذا لم يكن الإدخالair، وإلا استخدمground) واحسب التكلفة مرة أخرى لنفس الوزن. هذا يوضح قوة تبديل الاستراتيجيات أثناء وقت التشغيل.اطبع كل تكلفة في سطر خاص بها مع منزلة عشرية واحدة بالضبط، مسبوقة باسم الطريقة:
[Method]: $[cost]
على سبيل المثال، مع المدخلات 5.0 و ground:
Ground: $7.5
Air: $20.0مع المدخلات 3.0 و express:
Express: $29.5
Air: $12.0مع المدخلات 10.0 و air:
Air: $40.0
Ground: $15.0لاحظ كيف أن ShippingService لا يحتاج إلى معرفة تفاصيل كل خوارزمية تسعير — فهو ببساطة يفوض المهمة إلى أي استراتيجية محددة حاليًا. يمكنك بسهولة إضافة طرق شحن جديدة (مثل التوصيل بالطائرات بدون طيار أو التوصيل في نفس اليوم) دون تعديل فئة الخدمة على الإطلاق.
ورقة مرجعية
يحدد نمط الاستراتيجية (Strategy pattern) عائلة من الخوارزميات، ويغلف كل واحدة منها، ويجعلها قابلة للتبديل. يتيح ذلك تغيير سلوك الكائن في وقت التشغيل دون تعديل الكود الخاص به.
يتكون النمط من ثلاثة أجزاء:
- واجهة الاستراتيجية (Strategy interface): فئة مجردة تعلن عن طريقة الخوارزمية
- الاستراتيجيات الملموسة (Concrete Strategies): فئات تنفذ متغيرات مختلفة من الخوارزمية
- السياق (Context): فئة تستخدم استراتيجية ويمكنها التبديل بينها
// واجهة الاستراتيجية
class PaymentStrategy {
public:
virtual void pay(int amount) = 0;
virtual ~PaymentStrategy() = default;
};
// الاستراتيجيات الملموسة
class CreditCardPayment : public PaymentStrategy {
public:
void pay(int amount) override {
std::cout << "Paid " << amount << " via Credit Card\n";
}
};
class PayPalPayment : public PaymentStrategy {
public:
void pay(int amount) override {
std::cout << "Paid " << amount << " via PayPal\n";
}
};
// السياق
class ShoppingCart {
std::unique_ptr<PaymentStrategy> strategy;
public:
void setPaymentMethod(std::unique_ptr<PaymentStrategy> s) {
strategy = std::move(s);
}
void checkout(int total) {
if (strategy) strategy->pay(total);
}
};لا يعرف السياق أي استراتيجية ملموسة يستخدمها—فهو يكتفي باستدعاء طريقة الواجهة. يمكن تبديل الاستراتيجيات في وقت التشغيل باستخدام طريقة تعيين (setter method):
ShoppingCart cart;
cart.setPaymentMethod(std::make_unique<CreditCardPayment>());
cart.checkout(100);
cart.setPaymentMethod(std::make_unique<PayPalPayment>());
cart.checkout(50);متى يُستخدم: استخدم نمط الاستراتيجية عندما يكون لديك خوارزميات متعددة لمهمة محددة وتريد التبديل بينها ديناميكيًا، أو عندما تريد تجنب الجمل الشرطية لاختيار السلوك.
جرّب بنفسك
#include <iostream>
#include <string>
#include <iomanip>
#include <memory>
#include "ShippingStrategy.h"
#include "ShippingService.h"
int main() {
double weight;
std::string method;
std::cin >> weight;
std::cin >> method;
// ضبط المخرجات لتكون بمنزلة عشرية واحدة
std::cout << std::fixed << std::setprecision(1);
ShippingService service;
// TODO: بناءً على طريقة الإدخال ("ground" أو "air" أو "express"):
// 1. قم بتعيين الاستراتيجية المناسبة في الـ service
// 2. احسب واطبع التكلفة بالتنسيق التالي: "[Method]: $[cost]"
// TODO: التبديل إلى استراتيجية مختلفة:
// - إذا كان الإدخال "air"، فقم بالتبديل إلى GroundShipping
// - خلاف ذلك، قم بالتبديل إلى AirShipping
// احسب واطبع التكلفة الجديدة
return 0;
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP
الملفات الخارجيةبناء وتجميع (Compilation) لغة C++ملفات الـ Header وملفات الـ Sourceالـ Namespaces والـ Scopeمقدمة في الـ OOP في C++الـ Classes مقابل الـ Objectsالمؤشر 'this'الـ Methods (الدوال الأعضاء)الـ Attributes (بيانات الأعضاء)أساسيات الـ Ctors والـ Dtorsمراجعة - آلة حاسبة بسيطة4خصائص الـ Class
أعضاء الـ Instance مقابل الـ Staticدوال الـ Getters والـ Settersدوال الأعضاء الثابتة (Const)الكلمة المفتاحية Mutableالدوال والمتغيرات الساكنة (Static)الدوال والفئات الصديقة (Friend)مراجعة - مدير الحساب البنكي7الوراثة
الوراثة الأساسيةمستويات الوصول في الوراثةترتيب استدعاء الـ Ctor والـ Dtorإعادة تعريف الدوال (Method Overriding)الدوال الافتراضية والـ VTableالوراثة المتعددةالوراثة الافتراضيةمراجعة - هيكلية الموظفين10نظرة عامة على STL
نظرة عامة وفلسفة STLحاويات STLالـ Iteratorsخوارزميات STLالـ Functors وتعبيرات Lambdaمراجعة - تكرار الكلمات13أنماط التصميم - الجزء الأول
مقدمة في أنماط التصميمنمط Singletonنمط Factory و Abstract Factoryنمط Builderنمط Observerنمط Strategy2إدارة الذاكرة
ذاكرة Stack مقابل Heapالمؤشرات والمراجعالذاكرة الديناميكية (new/delete)المؤشرات الذكية في C++RAII في C++مراجعة - مدير المصفوفات الديناميكية5التغليف (Encapsulation)
محددات الوصول في C++محددات الوصول بشكل متعمقإخفاء المعلوماتStruct مقابل Classالأصناف المتداخلة والداخليةمراجعة - نظام سجلات الطلاب8تعدد الأشكال (Polymorphism)
تعدد الأشكال: وقت التجميع مقابل وقت التشغيلالتحميل الزائد للدوال (Function Overloading)مراجعة الدوال الافتراضية (Virtual Functions)الدوال الافتراضية البحتة (Pure Virtual Functions)الأصناف المجردة (Abstract Classes)تصميم الواجهات (Interface) في C++التحويل الديناميكي (Dynamic Casting) و RTTIملخص - حاسبة الأشكال11مفاهيم OOP المتقدمة
التركيب مقابل الوراثةالـ Mixins عبر CRTPنمط Pimplمحو النوع (Type Erasure)أصناف Enum والأنواع القويةمعالجة الاستثناءات في OOPتسلسلات الاستثناءات المخصصة14أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط قالب الطريقةنمط الحالةنمط التركيبRAII كنمط3المنشئات والموادم
المنشئ الافتراضيالمنشئ ذو المعاملاتمنشئ النسخمنشئ النقلقوائم تهيئة المنشئالمنشئات المفوضةتعمق في الموادمقاعدة الثلاثة / الخمسة / الصفرمراجعة - فئة String6تحميل العوامل (Operator Overloading)
مقدمة في تحميل العواملتحميل العوامل الحسابيةتحميل عوامل المقارنةعوامل Streamتحميل عامل التعيينتحميل العوامل [] و ()عوامل تحويل النوعمراجعة - Matrix Class9القوالب
قوالب الدوالقوالب الأصنافتخصيص القوالبالقوالب المتغيرةأساسيات SFINAE و Type Traitsمراجعة - الحاويات العامة