Command Pattern
Command Pattern هو نمط تصميم سلوكي يحول request أو إجراء أو عملية إلى كائن. بدلاً من تنفيذ عملية مباشرة، يقوم التطبيق بإنشاء كائن command الذي يحتوي على كافة المعلومات اللازمة لتنفيذ تلك العملية.
يكون هذا النمط مفيدًا عندما يحتاج البرنامج إلى وضع الإجراءات في queue، أو تأخير التنفيذ، أو عمليات السجل، أو دعم undo وredo، أو تنظيم إجراءات المستخدم، أو فصل الكائن الذي يقوم request بإجراء عملية عن الكائن الذي ينفذها.
مقدمة
في المقالات السابقة من سلسلة Design Patterns، ناقشنا الأنماط السلوكية مثل Strategy Pattern وObserver Pattern. يساعد Strategy Pattern على التبديل بين الخوارزميات أو السلوكيات المختلفة. يسمح Observer Pattern لعدة listener بالتفاعل عند حدوث event.
يحل Command Pattern مشكلة مختلفة. يركز على تمثيل الإجراء ككائن. وهذا يعني أنه يمكن تخزين الإجراء أو تمريره أو queued أو تسجيله أو إعادة محاولته أو تنفيذه لاحقًا.
في مشاريع البرامج الحقيقية، تعد commands شائعة في وظائف الخلفية، والمهام queues، وCLI commands، وإجراءات الأزرار، وأنظمة المعاملات، وميزات undo وredo، ومحركات سير العمل، وحالات استخدام التطبيقات.
ما هو Command Pattern؟
Command Pattern هو نمط تصميم يقوم بتغليف request ككائن. يحتوي هذا الكائن عادةً على طريقة مثل التنفيذ أو المعالجة أو التشغيل. عندما يتم استدعاء الأسلوب، يقوم command بتنفيذ الإجراء المطلوب.
بعبارات بسيطة، يجيب كائن command على السؤال: ما الإجراء الذي يجب تنفيذه؟
على سبيل المثال، بدلاً من الاتصال مباشرة بخدمة البريد الإلكتروني من العديد من الأماكن في التطبيق، يمكن للمطورين إنشاء SendWelcomeEmailCommand. يحتوي command على بيانات المستخدم ويعرف كيفية إرسال البريد الإلكتروني الترحيبي عند تنفيذه.
الفكرة الرئيسية لـ Command Pattern
الفكرة الرئيسية لـ Command Pattern هي فصل request لإجراء ما عن تنفيذ هذا الإجراء. لا يحتاج الكائن الذي يقوم بإنشاء أو تشغيل command إلى معرفة كل التفاصيل حول كيفية تنفيذ الإجراء.
يتضمن النمط عادةً ما يلي:
- Command interface: يحدد طريقة شائعة مثل التنفيذ.
- Concrete command: ينفذ الإجراء.
- المستقبل: الكائن الذي يؤدي العمل الحقيقي.
- المستدعي: الكائن الذي يقوم بتشغيل command.
- العميل: الكود الذي يقوم بإنشاء وتكوين command.
هذا الهيكل يجعل الإجراءات أكثر مرونة وأسهل في التنظيم.
لماذا يعد Command Pattern مهمًا
يعد Command Pattern مهمًا لأنه يمنح العمليات هيكلها الخاص. عندما يتم تمثيل الإجراءات ككائنات، يصبح من الأسهل تخزينها أو تنفيذها لاحقًا أو إعادة المحاولة أو تسجيلها أو دمجها مع إجراءات أخرى.
بدون Command Pattern، قد يقوم منطق التطبيق باستدعاء العديد من الخدمات مباشرة في العديد من الأماكن. يؤدي هذا إلى إنشاء tight coupling ويجعل التحكم في تدفق التنفيذ أكثر صعوبة.
مع Command Pattern، تصبح الإجراءات كائنات مستقلة. يمكن للتطبيق أن يقرر متى وكيف يتم تنفيذها.
مشكلة بدون Command Pattern
تخيل تطبيقًا يرسل رسائل البريد الإلكتروني، وينشئ الفواتير، ويحدث المخزون، ويسجل الإجراءات مباشرة داخل وحدة التحكم أو الخدمة:
class OrderController
{
public function placeOrder(Request $request)
{
$order = $this->orderService->create($request->all());
$this->paymentService->charge($order);
$this->inventoryService->decreaseStock($order);
$this->invoiceService->createInvoice($order);
$this->emailService->sendConfirmation($order);
return $order;
}
}يعمل هذا الرمز، لكن وحدة التحكم تقوم الآن بتنسيق العديد من الإجراءات مباشرةً. إذا كانت بعض الإجراءات تحتاج إلى queued أو إعادة المحاولة أو التسجيل أو undone لاحقًا، فستصبح إدارة التصميم أكثر صعوبة.
يمكن لـ Command Pattern نقل كل إجراء إلى كائن command منفصل.
مثال Command Pattern الأساسي في PHP
أولاً، قم بتعريف command interface:
interface Command
{
public function execute(): void;
}الآن قم بإنشاء جهاز الاستقبال class الذي يقوم بالعمل الحقيقي:
class EmailService
{
public function send(string $email, string $message): void
{
// Send email logic
echo 'Email sent to ' . $email;
}
}ثم قم بإنشاء concrete command:
class SendEmailCommand implements Command
{
public function __construct(
private EmailService $emailService,
private string $email,
private string $message
) {
}
public function execute(): void
{
$this->emailService->send($this->email, $this->message);
}
}يحتوي كائن SendEmailCommand على كل ما هو مطلوب لتنفيذ إجراء إرسال البريد الإلكتروني.
باستخدام Command
يمكن إنشاء command وتنفيذه على النحو التالي:
$emailService = new EmailService();
$command = new SendEmailCommand(
$emailService,
'user@example.com',
'Welcome to our platform.'
);
$command->execute();لا يحتاج الكود الذي ينفذ command إلى معرفة التفاصيل الداخلية لخدمة البريد الإلكتروني. فإنه يدعو فقط التنفيذ.
Command Pattern مع المستدعي
المستدعي هو كائن يستقبل command ويقوم بتشغيله. لا يعرف المستحضر تفاصيل command. إنه يعرف فقط أنه يمكن تنفيذ command.
class CommandInvoker
{
public function run(Command $command): void
{
$command->execute();
}
}
$invoker = new CommandInvoker();
$invoker->run($command);تجعل هذه البنية من الممكن تنفيذ commands مختلفة من خلال نفس المستدعي.
مثال من العالم الحقيقي: اطلب Commands
غالبًا ما تتضمن معالجة الطلب إجراءات متعددة. يمكن تمثيل كل إجراء كـ command.
class ChargePaymentCommand implements Command
{
public function __construct(
private PaymentService $paymentService,
private Order $order
) {
}
public function execute(): void
{
$this->paymentService->charge($this->order);
}
}
class CreateInvoiceCommand implements Command
{
public function __construct(
private InvoiceService $invoiceService,
private Order $order
) {
}
public function execute(): void
{
$this->invoiceService->create($this->order);
}
}
class SendOrderConfirmationCommand implements Command
{
public function __construct(
private EmailService $emailService,
private Order $order
) {
}
public function execute(): void
{
$this->emailService->send(
$this->order->customerEmail,
'Your order has been confirmed.'
);
}
}يتحمل كل command مسؤولية واحدة. وهذا يجعل سير عمل الطلب أسهل في التنظيم والاختبار.
مثال Command Queue
أحد الاستخدامات القوية لـ Command Pattern هو command queues. يمكن تخزين Commands في قائمة وتنفيذها لاحقًا.
class CommandQueue
{
private array $commands = [];
public function add(Command $command): void
{
$this->commands[] = $command;
}
public function run(): void
{
foreach ($this->commands as $command) {
$command->execute();
}
}
}يمكن للتطبيق إضافة commands إلى queue:
$queue = new CommandQueue();
$queue->add(new ChargePaymentCommand($paymentService, $order));
$queue->add(new CreateInvoiceCommand($invoiceService, $order));
$queue->add(new SendOrderConfirmationCommand($emailService, $order));
$queue->run();وهذا يسمح بتنظيم الإجراءات وتنفيذها بترتيب خاضع للرقابة.
Command Pattern ووظائف الخلفية
تعتبر وظائف الخلفية نموذجًا عمليًا لـ Command Pattern. يمثل كائن الوظيفة عادة الإجراء الذي يجب تنفيذه لاحقًا بواسطة عامل queue.
على سبيل المثال، يمكن وضع إرسال بريد إلكتروني أو إنشاء ملف PDF أو معالجة ملف تم تحميله أو الاتصال بـ API خارجي داخل مهمة command.
وهذا يحافظ على سرعة request الرئيسية. بدلاً من القيام بكل شيء على الفور، يرسل التطبيق command أو مهمة إلى queue.
Command Pattern في Laravel
يستخدم Laravel بنيات تشبه command في عدة أماكن. وظائف Laravel ووحدة التحكم commands وqueued listeners والإجراء classes كلها مرتبطة بفكرة تمثيل العمل ككائن قابل للتنفيذ.
قد تبدو مهمة Laravel كما يلي:
class SendWelcomeEmailJob implements ShouldQueue
{
public function __construct(
public User $user
) {
}
public function handle(EmailService $emailService): void
{
$emailService->send(
$this->user->email,
'Welcome to our platform.'
);
}
}تمثل هذه المهمة إجراءً يمكن أن يكون queued ويتم تنفيذه لاحقًا. طريقة المقبض مشابهة للتنفيذ في classic Command Pattern.
وحدة التحكم Laravel Commands
وحدة التحكم Laravel command هي أيضًا كائنات تشبه command. وهي تمثل الإجراءات التي يمكن تنفيذها من سطر command.
class GenerateReportsCommand extends Command
{
protected $signature = 'reports:generate';
public function handle(): int
{
// Generate reports
return self::SUCCESS;
}
}يحتوي كائن command على المنطق الخاص بعملية CLI محددة. وهذا يحافظ على تنظيم مهام خط command وإمكانية إعادة استخدامها.
Command Pattern في Symfony
يستخدم Symfony أيضًا كائنات command لوحدة التحكم commands. وحدة التحكم Symfony commands هي classes التي تمثل مهام CLI القابلة للتنفيذ.
Symfony Messenger هو مثال قوي آخر. يمكن أن تمثل الرسائل وhandlers commands التي يتم إرسالها ومعالجتها بشكل متزامن أو غير متزامن.
يوضح هذا كيف يظهر Command Pattern بشكل طبيعي في أطر عمل PHP الحديثة.
عمليات Command Pattern وUndo
حالة استخدام classic أخرى لـ Command Pattern هي undo وredo. إذا تم تمثيل كل إجراء ككائن command، فيمكن للتطبيق تخزين commands المنفذة وعكسها لاحقًا.
لدعم undo، يمكن لـ commands تعريف طريقة undo:
interface UndoableCommand
{
public function execute(): void;
public function undo(): void;
}على سبيل المثال، يمكن لمحرر النصوص تمثيل إجراءات مثل كتابة نص أو حذف نص أو تنسيق النص كـ commands. يمكن لكل command معرفة كيفية عمل undo الخاص به.
مثال Undo في PHP
class TextEditor
{
public string $content = '';
}
class AddTextCommand implements UndoableCommand
{
public function __construct(
private TextEditor $editor,
private string $text
) {
}
public function execute(): void
{
$this->editor->content .= $this->text;
}
public function undo(): void
{
$this->editor->content = substr(
$this->editor->content,
0,
-strlen($this->text)
);
}
}يضيف command نصًا عند تنفيذه ويزيل نفس النص عند تنفيذ undone.
مثال على تاريخ Command
يمكن لسجل command تخزين commands لدعم undo:
class CommandHistory
{
private array $history = [];
public function execute(UndoableCommand $command): void
{
$command->execute();
$this->history[] = $command;
}
public function undoLast(): void
{
$command = array_pop($this->history);
if ($command) {
$command->undo();
}
}
}يعد هذا الأسلوب مفيدًا للمحررين وأدوات التصميم وأنظمة سير العمل والتطبيقات التي تحتاج إلى إجراءات قابلة للعكس.
Command Pattern والتسجيل
نظرًا لأن commands عبارة عن كائنات، فيمكن تسجيلها قبل التنفيذ أو بعده. يعد هذا مفيدًا في الأنظمة التي تحتاج إلى سجلات التدقيق أو تتبع العمليات.
على سبيل المثال، يمكن تسجيل إجراء إداري مثل RemoveUserCommand قبل تنفيذه. يمكن للنظام تسجيل من قام بتنفيذ command ومتى حدث ذلك وما هي البيانات التي تم تضمينها.
وهذا يجعل Command Pattern مفيدًا لتطبيقات المؤسسات ولوحات الإدارة والأنظمة المالية وسير العمل الحساسة للأمان.
Command Pattern وإعادة محاولة المنطق
يمكن أن تدعم Commands أيضًا منطق إعادة المحاولة. إذا فشل الإجراء بسبب خطأ مؤقت، مثل انتهاء مهلة API، فيمكن إعادة محاولة command لاحقًا.
وهذا أمر شائع في أنظمة queue. قد تفشل مهمة command ثم تتم إعادة المحاولة تلقائيًا بناءً على تكوين queue.
يجعل Command Pattern هذا الأمر أسهل لأن كل إجراء يتم عزله داخل الكائن الخاص به.
Command Pattern والمعاملات
تقوم بعض commands بتنفيذ عمليات مهمة يجب تضمينها في معاملات قاعدة البيانات. على سبيل المثال، قد يقوم CreateOrderCommand بإنشاء أمر وتقليل المخزون وتسجيل بيانات الدفع.
يمكن لـ command handler أو الخدمة تنفيذ command داخل المعاملة لضمان اتساق البيانات.
class TransactionalCommandBus
{
public function __construct(
private DatabaseConnection $database
) {
}
public function dispatch(Command $command): void
{
$this->database->transaction(function () use ($command) {
$command->execute();
});
}
}يسمح هذا بتنفيذ commands بأمان عندما يجب أن تنجح العديد من تغييرات قاعدة البيانات أو تفشل معًا.
حافلة Command
ناقل command هو كائن مسؤول عن إرسال كائنات command إلى command handler الصحيح. وهو شائع في الهندسة المعمارية النظيفة وCQRS وتطبيقات المؤسسات.
بدلاً من استدعاء handlers مباشرة، يرسل التطبيق command إلى الناقل command. يعثر الناقل command على handler الصحيح ويقوم بتنفيذه.
يمكن لهذا النهج مركزية البرامج الوسيطة مثل التحقق من الصحة والترخيص والتسجيل والمعاملات وإعادة المحاولة.
مثال Command وHandler
تقوم بعض التطبيقات بفصل بيانات command عن command handler. يقوم command بتخزين البيانات، ويقوم handler بتنفيذ الإجراء.
class RegisterUserCommand
{
public function __construct(
public string $name,
public string $email,
public string $password
) {
}
}
class RegisterUserHandler
{
public function __construct(
private UserRepositoryInterface $users,
private PasswordHasher $passwordHasher
) {
}
public function handle(RegisterUserCommand $command): User
{
return $this->users->create([
'name' => $command->name,
'email' => $command->email,
'password' => $this->passwordHasher->hash($command->password),
]);
}
}هذا النمط شائع في خدمات التطبيقات والتصميمات القائمة على CQRS.
Command Pattern وCQRS
يشير CQRS إلى الفصل بين مسؤولية الاستعلام Command. فهو يفصل commands، التي تغير الحالة، عن الاستعلامات، التي تقرأ البيانات.
في CQRS، تمثل commands عمليات مثل RegisterUser أو PlaceOrder أو CancelSubscription أو UpdateProfile. تمثل الاستعلامات عمليات القراءة مثل GetUserProfile أو ListOrders.
يتناسب Command Pattern بشكل طبيعي مع CQRS لأن commands عبارة عن كائنات صريحة تمثل إجراءات تغيير الحالة.
Command Pattern مقابل Observer Pattern
يعد كل من Command Pattern وObserver Pattern من الأنماط السلوكية، لكنهما يحلان مشكلات مختلفة.
يمثل Command Pattern إجراءً ككائن. إنه مفيد في قائمة الانتظار أو التسجيل أو إعادة المحاولة أو undoing أو تنفيذ الإجراءات لاحقًا.
يقوم Observer Pattern بإعلام observers المتعددة عند حدوث event. يكون ذلك مفيدًا عند حدوث عدة تفاعلات مستقلة بعد event.
باختصار، يمثل Command request لفعل شيء ما، بينما يتفاعل Observer مع شيء حدث بالفعل.
Command Pattern مقابل Strategy Pattern
يحدد Strategy Pattern الخوارزميات أو السلوكيات القابلة للتبديل. يختار التطبيق strategy واحدًا لأداء مهمة بطريقة معينة.
يقوم Command Pattern بتغليف الإجراء أو request ككائن. قد يتم تخزين command أو queued أو تنفيذه أو undone أو إعادة المحاولة.
على سبيل المثال، يختار PaymentStrategy كيفية معالجة الدفع. يمثل ChargePaymentCommand إجراء تحصيل الدفع.
باختصار، Strategy يتعلق باختيار السلوك، بينما Command يتعلق بتمثيل الإجراء.
Command Pattern مقابل نمط أسلوب القالب
يحدد نمط أسلوب القالب الهيكل العظمي للخوارزمية في class الأصل ويتيح للطفل classes تخصيص بعض الخطوات. ويستخدم inheritance.
يقوم Command Pattern بتغليف الإجراء الكامل ككائن وعادةً ما يستخدم التركيب. إنه يركز أكثر على التحكم في التنفيذ من بنية الخوارزمية.
استخدم طريقة القالب عندما يتم إصلاح خطوات الخوارزمية. استخدم Command عندما يلزم تخزين الإجراءات أو تنفيذها لاحقًا أو تسجيلها أو queued أو undone.
فوائد Command Pattern
يوفر Command Pattern العديد من الفوائد في تصميم البرامج الموجهة للكائنات.
تشمل الفوائد الرئيسية ما يلي:
- يغلف الإجراءات ككائنات.
- يفصل إنشاء request عن تنفيذها.
- يدعم queues وتأخير التنفيذ.
- يجعل تنفيذ undo وredo أسهل.
- يحسن تسجيل وتدقيق العمليات.
- يدعم منطق إعادة المحاولة للمهام الفاشلة.
- يبقي الإجراءات مركزة وقابلة للاختبار.
- يعمل بشكل جيد مع حافلات command والوظائف والعاملين في الخلفية.
هذه الفوائد تجعل Command Pattern مفيدًا في العديد من تطبيقات العالم الحقيقي.
عيوب Command Pattern
يمكن أن يكون لـ Command Pattern أيضًا عيوب. قد يزيد عدد classes لأن كل إجراء قد يحتاج إلى command class الخاص به.
بالنسبة للعمليات البسيطة، قد تكون هذه البنية الإضافية غير ضرورية. إذا تم استخدام الإجراء مرة واحدة فقط ولا يحتاج إلى الانتظار أو التسجيل أو undo أو التنفيذ المؤجل، فقد يكون استدعاء الأسلوب المباشر أكثر وضوحًا.
عيب آخر هو أن الأنظمة المستندة إلى command يمكن أن تصبح أكثر صعوبة في التتبع إذا لم يتم تنظيم commands وhandlers بشكل جيد.
متى يتم استخدام Command Pattern
استخدم Command Pattern عندما يلزم التعامل مع الإجراءات ككائنات أو عندما يحتاج التنفيذ إلى مزيد من التحكم.
يكون Command Pattern مفيدًا عندما:
- تحتاج إلى مهام queue للتنفيذ لاحقًا.
- أنت بحاجة إلى وظائف undo وredo.
- تحتاج إلى تسجيل أو تدقيق الإجراءات.
- تحتاج إلى منطق إعادة المحاولة للعمليات الفاشلة.
- تريد فصل الإجراء request عن التنفيذ.
- أنت تقوم ببناء CLI commands أو وظائف أو إجراءات سير العمل.
- تريد تنظيم حالات استخدام التطبيق بشكل واضح.
في حالة وجود هذه الشروط، يمكن لـ Command Pattern أن يجعل التصميم أكثر وضوحًا ومرونة.
متى لا تستخدم Command Pattern
لا تستخدم Command Pattern عندما تكون العملية بسيطة للغاية ولا تحتاج إلى تخزينها أو queued أو تسجيلها أو إعادة المحاولة أو undone.
تجنب Command Pattern عندما:
- استدعاء الطريقة المباشرة أبسط وأكثر وضوحًا.
- لا يتم إعادة استخدام الإجراء أو تأخيره.
- يضيف النمط العديد من classes دون فائدة حقيقية.
- المشروع صغير ولا يحتاج إلى بنية command.
- يقوم كائن command بتغليف سطر واحد بسيط من الكود فقط.
يجب أن يحل Design patterns المشكلات الحقيقية. إذا كان Command Pattern يضيف التعقيد فقط، فيجب تجنبه.
الأخطاء الشائعة في Command Pattern
أحد الأخطاء الشائعة هو إنشاء commands لكل استدعاء أسلوب صغير. هذا يمكن أن يجعل المشروع معقدًا للغاية.
خطأ آخر هو وضع الكثير من المسؤوليات داخل command. يجب أن يمثل command إجراءً واحدًا واضحًا.
الخطأ الثالث هو خلط بيانات command والعديد من تفاصيل البنية التحتية في نفس class. في الأنظمة الأكبر حجمًا، يمكن أن يؤدي فصل بيانات command عن command handlers إلى تحسين الوضوح.
الخطأ الرابع هو عدم التعامل مع حالات الفشل بشكل صحيح في queued commands. Commands التي تعتمد على APIs خارجيةة يجب أن تأخذ في الاعتبار عمليات إعادة المحاولة والمهلات ومعالجة الأخطاء.
أفضل الممارسات لـ Command Pattern
لاستخدام Command Pattern بشكل فعال، يجب على المطورين إبقاء commands مركزة وذات معنى.
تتضمن أفضل الممارسات المفيدة ما يلي:
- استخدم أسماء واضحة مثل RegisterUserCommand أو SendInvoiceCommand.
- اجعل كل command يركز على إجراء واحد.
- استخدم command handlers لمنطق التطبيق المعقد.
- استخدم queues لـ commands البطيئة أو المتأخرة.
- أضف التسجيل وأعد محاولة المنطق عند الحاجة.
- استخدم المعاملات الخاصة بـ commands التي تغير سجلات متعددة.
- تجنب command classes للعمليات البسيطة جدًا.
- تنظيم commands حسب المجال أو الميزة.
تساعد هذه الممارسات في الحفاظ على الأنظمة المستندة إلى command نظيفة وقابلة للصيانة.
قائمة مرجعية عملية قبل استخدام Command Pattern
قبل استخدام Command Pattern، يمكن للمطورين طرح هذه الأسئلة:
- هل يجب تنفيذ هذا الإجراء لاحقًا؟
- هل يلزم أن يكون queued أو إعادة المحاولة؟
- هل يحتاج إلى تسجيل أو تدقيق؟
- هل يحتاج إلى دعم undo أو redo؟
- هل سيؤدي تمثيل الإجراء ككائن إلى تحسين الوضوح؟
- هل الإجراء مهم بما يكفي ليكون له class الخاص به؟
- هل سيؤدي هذا إلى تقليل الاقتران بين request والتنفيذ؟
إذا كانت الإجابة بنعم على العديد من هذه الأسئلة، فقد يكون Command Pattern اختيارًا جيدًا للتصميم.
الاستنتاج
Command Pattern هو نمط تصميم سلوكي يحول الإجراءات أو request إلى كائنات. ويساعد على تنظيم العمليات، وفصل إنشاء request عن التنفيذ، ودعم الميزات المتقدمة مثل queues، وundo، وredo، والتسجيل، والتدقيق، وإعادة المحاولة، والمعاملات، وحافلات command.
يعد Command Pattern مفيدًا في تطبيقات PHP ووظائف Laravel وSymfony commands والعاملين في الخلفية وأدوات CLI وأنظمة CQRS ومحركات سير العمل والتطبيقات التي تحتاج إلى تنفيذ إجراء متحكم فيه.
ومع ذلك، يجب استخدام Command Pattern بعناية. يمكنه إضافة عدد كبير جدًا من classes إذا تم تطبيقه على العمليات البسيطة. عند استخدامها لإجراءات ذات معنى، فهي أداة قوية لبناء برامج موجهة للكائنات نظيفة ومنظمة وقابلة للصيانة.

