Menu
Coddy logo textTech
flag Ar iconالعربيةdown icon

نمط الأمر

جزء من قسم Object Oriented Programming في رحلة GO على Coddy — الدرس 91 من 107.

يقوم نمط الأمر (Command pattern) بتغليف الطلب ككائن، مما يسمح لك بتمرير العمليات كمعاملات، أو وضعها في طابور، أو دعم خاصية التراجع. وبينما يقوم نمط Strategy بتغليف الخوارزميات، يقوم نمط Command بتغليف إجراءات كاملة مع معاملاتها.

في لغة Go، نقوم بتعريف واجهة Command تحتوي على طريقة Execute، ثم نقوم بإنشاء أوامر ملموسة (concrete commands) تحتوي على جميع المعلومات اللازمة لتنفيذ إجراء ما:

type Command interface {
    Execute() string
}

type Light struct {
    IsOn bool
}

type TurnOnCommand struct {
    light *Light
}

func (c *TurnOnCommand) Execute() string {
    c.light.IsOn = true
    return "Light turned on"
}

type TurnOffCommand struct {
    light *Light
}

func (c *TurnOffCommand) Execute() string {
    c.light.IsOn = false
    return "Light turned off"
}

يقوم المستدعي (invoker) بتخزين الأوامر وتنفيذها دون معرفة ما تفعله:

type RemoteControl struct {
    command Command
}

func (r *RemoteControl) SetCommand(c Command) {
    r.command = c
}

func (r *RemoteControl) PressButton() string {
    return r.command.Execute()
}

يتم فصل المُستدعي (invoker) تماماً عن المُستقبل (receiver) (المصباح):

light := &Light{}
remote := &RemoteControl{}

remote.SetCommand(&TurnOnCommand{light: light})
fmt.Println(remote.PressButton())  // تم تشغيل المصباح

remote.SetCommand(&TurnOffCommand{light: light})
fmt.Println(remote.PressButton())  // تم إيقاف تشغيل المصباح

يُعد نمط الأمر (Command) مثاليًا لتنفيذ أنظمة التراجع/الإعادة، أو طوابير المهام، أو تسجيل الماكرو حيث تحتاج إلى تخزين العمليات أو تأجيلها أو إعادة تشغيلها.

challenge icon

التحدي

سهل

لنقم ببناء محرر نصوص مع خاصية التراجع (undo) باستخدام نمط الأمر (Command pattern)! ستقوم بإنشاء أوامر تغلف عمليات النص، مما يسمح للمحرر بتنفيذ الإجراءات وعكسها — وهي حالة استخدام كلاسيكية توضح سبب قوة نمط الأوامر.

ستقوم بتنظيم الكود الخاص بك عبر ثلاثة ملفات:

  • command.go: حدد واجهة الأوامر (command interface) وأوامر تحرير النص الملموسة.

    قم بإنشاء واجهة Command بطريقتين: Execute() string لأداء الإجراء و Undo() string لعكسه.

    قم بتنفيذ نوعين من الأوامر التي تعمل على *TextEditor:

    • InsertCommand — يحمل مؤشر المحرر والنص المراد إدراجه. تقوم الدالة Execute بإلحاق النص بمحتوى المحرر وتعيد Inserted: [text]. تقوم الدالة Undo بإزالة ذلك النص من النهاية وتعيد Undone insert: [text]
    • DeleteCommand — يحمل مؤشر المحرر وعدد الأحرف المراد حذفها من النهاية. تقوم الدالة Execute بحذف تلك الأحرف (مع تخزينها من أجل التراجع) وتعيد Deleted: [removed text]. تقوم الدالة Undo باستعادتها وتعيد Undone delete: [restored text]
  • editor.go: أنشئ محرر النصوص الخاص بك (المستقبل/receiver) والمستدعي (invoker) الذي يدير سجل الأوامر.

    قم ببناء هيكل TextEditor مع حقل Content (سلسلة نصية) وطرق لـ Append(text string) و DeleteLast(count int) string (تعيد النص المحذوف).

    قم ببناء هيكل EditorInvoker يحمل شريحة (slice) من الأوامر المنفذة كسجل (history). أضف هذه الطرق:

    • ExecuteCommand(cmd Command) string — ينفذ الأمر، ويضيفه إلى السجل، ويعيد النتيجة
    • UndoLast() string — يزيل الأمر الأخير من السجل، ويستدعي Undo الخاص به، ويعيد النتيجة. إذا كان السجل فارغاً، فقم بإعادة Nothing to undo
  • main.go: استعرض نظام الأوامر الخاص بك مع سلسلة من العمليات.

    اقرأ عدد العمليات. لكل عملية، اقرأ نوع العملية (insert، أو delete، أو undo). بالنسبة لـ insert، اقرأ أيضاً النص المراد إدراجه. بالنسبة لـ delete، اقرأ عدد الأحرف. نفذ كل عملية من خلال المستدعي (invoker) الخاص بك واطبع النتيجة. بعد كل العمليات، اطبع محتوى المحرر النهائي كـ Final: [content].

سيتم توفير المدخلات التالية:

  • السطر 1: عدد العمليات
  • لكل عملية: نوع العملية، ثم بيانات إضافية إذا لزم الأمر (النص لـ insert، العدد لـ delete)

على سبيل المثال، إذا أعطينا:

5
insert
Hello
insert
 World
delete
3
undo
undo

يجب أن يكون مخرجك:

Inserted: Hello
Inserted:  World
Deleted: rld
Undone delete: rld
Undone insert:  World
Final: Hello

وإذا أعطينا:

4
insert
Go
insert
Lang
undo
insert
!

يجب أن يكون مخرجك:

Inserted: Go
Inserted: Lang
Undone insert: Lang
Inserted: !
Final: Go!

وإذا أعطينا:

2
undo
insert
Test

يجب أن يكون مخرجك:

Nothing to undo
Inserted: Test
Final: Test

لاحظ كيف يغلف كل أمر كل ما هو مطلوب لتنفيذ وعكس إجراءه. لا يعرف المستدعي (invoker) ما تفعله الأوامر — هو فقط ينفذها ويحافظ على السجل لدعم التراجع!

ورقة مرجعية

يغلف نمط الأمر (Command pattern) الطلب ككائن، مما يسمح لك بتمرير العمليات كمعاملات، أو وضعها في طابور، أو دعم خاصية التراجع.

قم بتعريف واجهة Command مع طريقة Execute:

type Command interface {
    Execute() string
}

قم بإنشاء أوامر ملموسة (concrete commands) تحتوي على جميع المعلومات اللازمة لتنفيذ إجراء ما:

type Light struct {
    IsOn bool
}

type TurnOnCommand struct {
    light *Light
}

func (c *TurnOnCommand) Execute() string {
    c.light.IsOn = true
    return "Light turned on"
}

type TurnOffCommand struct {
    light *Light
}

func (c *TurnOffCommand) Execute() string {
    c.light.IsOn = false
    return "Light turned off"
}

يقوم المستدعي (invoker) بتخزين الأوامر وتنفيذها دون معرفة ما تفعله:

type RemoteControl struct {
    command Command
}

func (r *RemoteControl) SetCommand(c Command) {
    r.command = c
}

func (r *RemoteControl) PressButton() string {
    return r.command.Execute()
}

مثال على الاستخدام يوضح الفصل بين المستدعي والمستقبل:

light := &Light{}
remote := &RemoteControl{}

remote.SetCommand(&TurnOnCommand{light: light})
fmt.Println(remote.PressButton())  // Light turned on

remote.SetCommand(&TurnOffCommand{light: light})
fmt.Println(remote.PressButton())  // Light turned off

للحصول على خاصية التراجع، أضف طريقة Undo إلى واجهة Command وقم بالحفاظ على سجل الأوامر في المستدعي.

جرّب بنفسك

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)
	
	// قراءة عدد العمليات
	line, _ := reader.ReadString('\n')
	numOps, _ := strconv.Atoi(strings.TrimSpace(line))
	
	// إنشاء editor و invoker
	editor := &TextEditor{}
	invoker := &EditorInvoker{}
	
	for i := 0; i < numOps; i++ {
		// قراءة نوع العملية
		opLine, _ := reader.ReadString('\n')
		opType := strings.TrimSpace(opLine)
		
		var result string
		
		switch opType {
		case "insert":
			// قراءة النص المراد إدراجه
			textLine, _ := reader.ReadString('\n')
			text := strings.TrimSuffix(textLine, "\n")
			
			// TODO: إنشاء InsertCommand وتنفيذه من خلال invoker
			_ = text
			_ = editor
			
		case "delete":
			// قراءة عدد الأحرف المراد حذفها
			countLine, _ := reader.ReadString('\n')
			count, _ := strconv.Atoi(strings.TrimSpace(countLine))
			
			// TODO: إنشاء DeleteCommand وتنفيذه من خلال invoker
			_ = count
			
		case "undo":
			// TODO: استدعاء UndoLast على invoker
			
		}
		
		fmt.Println(result)
	}
	
	// طباعة المحتوى النهائي
	fmt.Printf("Final: %s\n", editor.Content)
}
quiz iconاختبر نفسك

يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.

جميع دروس Object Oriented Programming