الفرق بين المراجعتين ل"Design Patterns/command"
أسامه-دمراني (نقاش | مساهمات) (2.9 محتوى) |
أسامه-دمراني (نقاش | مساهمات) (2.10 محتوى) |
||
سطر 478: | سطر 478: | ||
== الاستخدام في لغة #C == | == الاستخدام في لغة #C == | ||
− | المستوى: ★ ☆ ☆ | + | '''المستوى:''' ★ ☆ ☆ |
− | الانتشار: ★ ★ ★ | + | '''الانتشار:''' ★ ★ ★ |
− | أمثلة الاستخدام: يكثر استخدام نمط الأمر في لغة #C، ويستخدم عادة كبديل للاستدعاءات إلى إدخال إجراءاتٍ (Actions) كمعامِلات (Parameters) إلى عناصر الواجهة الرسومية. كما تستخدم أيضًا لصف المهام (Queuing Tasks) وتتبع سجل العمليات، إلخ. | + | '''أمثلة الاستخدام:''' يكثر استخدام نمط الأمر في لغة #C، ويستخدم عادة كبديل للاستدعاءات إلى إدخال إجراءاتٍ (Actions) كمعامِلات (Parameters) إلى عناصر الواجهة الرسومية. كما تستخدم أيضًا لصف المهام (Queuing Tasks) وتتبع سجل العمليات، إلخ. |
يمكن ملاحظة نمط الأمر من خلال الأساليب السلوكية في نوعٍ مجردٍ أو واجهةٍ (المرسل) الذي يستدعي أسلوبًا في تطبيق لنوعٍ مجردٍ أو واجهةٍ مختلف (المستقبِل) الذي اختُزِل بواسطة تطبيق الأمر أثناء إنشائه، وتكون فئات الأمر عادة محدودة بإجراءات محددة. | يمكن ملاحظة نمط الأمر من خلال الأساليب السلوكية في نوعٍ مجردٍ أو واجهةٍ (المرسل) الذي يستدعي أسلوبًا في تطبيق لنوعٍ مجردٍ أو واجهةٍ مختلف (المستقبِل) الذي اختُزِل بواسطة تطبيق الأمر أثناء إنشائه، وتكون فئات الأمر عادة محدودة بإجراءات محددة. | ||
سطر 498: | سطر 498: | ||
namespace RefactoringGuru.DesignPatterns.Command.Conceptual | namespace RefactoringGuru.DesignPatterns.Command.Conceptual | ||
{ | { | ||
− | // | + | // تصرح واجهة الأمر عن أسلوب لتنفيذ أحد الأوامر. |
public interface ICommand | public interface ICommand | ||
{ | { | ||
سطر 504: | سطر 504: | ||
} | } | ||
− | // | + | // تستطيع بعض الأوامر إجراء عمليات بأنفسها. |
class SimpleCommand : ICommand | class SimpleCommand : ICommand | ||
{ | { | ||
سطر 520: | سطر 520: | ||
} | } | ||
− | // | + | // (receivers) لكن بعض الأوامر تفوض العمليات المعقدة إلى كائنات أخرى تسمى المستقبِلات. |
− | |||
class ComplexCommand : ICommand | class ComplexCommand : ICommand | ||
{ | { | ||
private Receiver _receiver; | private Receiver _receiver; | ||
− | // | + | // البيانات السياقية مطلوبة لتشغيل أساليب المستقبِل. |
private string _a; | private string _a; | ||
private string _b; | private string _b; | ||
− | // | + | // تقبل الأوامر المعقدة كائن مستقبِل واحد أو أكثر إضافة إلى أي بيانات سياقية |
− | // | + | // (Constructor) من خلال المنشئ. |
public ComplexCommand(Receiver receiver, string a, string b) | public ComplexCommand(Receiver receiver, string a, string b) | ||
{ | { | ||
سطر 540: | سطر 539: | ||
} | } | ||
− | // | + | // تستطيع الأوامر أن تفوض إلى أي أسلوب من أساليب المستقبِل. |
public void Execute() | public void Execute() | ||
{ | { | ||
سطر 549: | سطر 548: | ||
} | } | ||
− | // | + | // على بعض منطق العمل المهم، وتعرف كيف (Receiver) تحتوي فئات المستقبِل |
− | // | + | // تنفذ كل أنواع العمليات المرتبطة بتنفيذ الطلب. |
− | // | + | // يمكن لأي فئة أن تعمل كمستقبِل. |
class Receiver | class Receiver | ||
{ | { | ||
سطر 565: | سطر 564: | ||
} | } | ||
− | // | + | // بأمر واحد أو أكثر، ويرسل الطلب إلى الأمر (Invoker) يرتبط المستدعي |
− | |||
class Invoker | class Invoker | ||
{ | { | ||
سطر 573: | سطر 571: | ||
private ICommand _onFinish; | private ICommand _onFinish; | ||
− | // | + | // شغِّل الأوامر. |
public void SetOnStart(ICommand command) | public void SetOnStart(ICommand command) | ||
{ | { | ||
سطر 584: | سطر 582: | ||
} | } | ||
− | // | + | // لا يعتمد المستدعي على فئات الأمر الحقيقي أو على المستقبِل، بل يمرر المستدعي |
− | // | + | // الطلبَ إلى المستقبِل بطريقة غير مباشرة عبر تنفيذ أحد الأوامر. |
− | + | ||
public void DoSomethingImportant() | public void DoSomethingImportant() | ||
{ | { | ||
سطر 609: | سطر 607: | ||
static void Main(string[] args) | static void Main(string[] args) | ||
{ | { | ||
− | // | + | // (Invoker) تستطيع شيفرة العميل أن تدخل أي أمر كمعامِل إلى المستدعي. |
Invoker invoker = new Invoker(); | Invoker invoker = new Invoker(); | ||
invoker.SetOnStart(new SimpleCommand("Say Hi!")); | invoker.SetOnStart(new SimpleCommand("Say Hi!")); | ||
سطر 619: | سطر 617: | ||
} | } | ||
} | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== Output.txt: نتائج التنفيذ ==== | ||
+ | <syntaxhighlight> | ||
+ | Invoker: Does anybody want something done before I begin? | ||
+ | SimpleCommand: See, I can do simple things like printing (Say Hi!) | ||
+ | Invoker: ...doing something really important... | ||
+ | Invoker: Does anybody want something done after I finish? | ||
+ | ComplexCommand: Complex stuff should be done by a receiver object. | ||
+ | Receiver: Working on (Send email.) | ||
+ | Receiver: Also working on (Save report.) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == الاستخدام في لغة PHP == | ||
+ | '''المستوى:''' ★ ☆ ☆ | ||
+ | |||
+ | '''الانتشار:''' ★ ★ ★ | ||
+ | |||
+ | '''أمثلة الاستخدام:''' يكثر استخدام نمط الأمر في لغة PHP، ويستخدم عادة كبديل للاستدعاءات إلى إدخال إجراءاتٍ (Actions) كمعامِلات (Parameters) إلى عناصر الواجهة الرسومية. كما تستخدم أيضًا لصف المهام (Queuing Tasks) وتتبع سجل العمليات المُنفَّذة وإجراء عمليات التراجع (undo). | ||
+ | |||
+ | === مثال تصوري === | ||
+ | يوضح هذا المثال بنية نمط الأمر (Command)، ويركز على إجابة الأسئلة التالية: | ||
+ | * ما الفئات التي يتكون منها؟ | ||
+ | * ما الأدوار التي تلعبها هذه الفئات؟ | ||
+ | * كيف ترتبط عناصر النمط ببعضها؟ | ||
+ | سيكون من السهل عليك استيعاب المثال التالي بعد تعلم بنية النمط، بناء على استخدام واقعي له في لغة PHP. | ||
+ | |||
+ | ==== index.php: مثال تصوري ==== | ||
+ | <syntaxhighlight lang="php"> | ||
+ | <?php | ||
+ | |||
+ | namespace RefactoringGuru\Command\Conceptual; | ||
+ | |||
+ | /** | ||
+ | * تصرح واجهة الأمر عن أسلوب لتنفيذ أحد الأوامر. | ||
+ | */ | ||
+ | interface Command | ||
+ | { | ||
+ | public function execute(): void; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * تستطيع بعض الأوامر إجراء عمليات بأنفسها. | ||
+ | */ | ||
+ | class SimpleCommand implements Command | ||
+ | { | ||
+ | private $payload; | ||
+ | |||
+ | public function __construct(string $payload) | ||
+ | { | ||
+ | $this->payload = $payload; | ||
+ | } | ||
+ | |||
+ | public function execute(): void | ||
+ | { | ||
+ | echo "SimpleCommand: See, I can do simple things like printing (" . $this->payload . ")\n"; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * (receivers) لكن بعض الأوامر تفوض العمليات المعقدة إلى كائنات أخرى تسمى المستقبِلات. | ||
+ | */ | ||
+ | class ComplexCommand implements Command | ||
+ | { | ||
+ | /** | ||
+ | * @var Receiver | ||
+ | */ | ||
+ | private $receiver; | ||
+ | |||
+ | /** | ||
+ | * البيانات السياقية مطلوبة لتشغيل أساليب المستقبِل. | ||
+ | */ | ||
+ | private $a; | ||
+ | |||
+ | private $b; | ||
+ | |||
+ | /** | ||
+ | * تقبل الأوامر المعقدة كائن مستقبِل واحد أو أكثر إضافة إلى أي بيانات سياقية | ||
+ | * (Constructor) من خلال المنشئ. | ||
+ | */ | ||
+ | public function __construct(Receiver $receiver, string $a, string $b) | ||
+ | { | ||
+ | $this->receiver = $receiver; | ||
+ | $this->a = $a; | ||
+ | $this->b = $b; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * تستطيع الأوامر أن تفوض إلى أي أسلوب من أساليب المستقبِل. | ||
+ | */ | ||
+ | public function execute(): void | ||
+ | { | ||
+ | echo "ComplexCommand: Complex stuff should be done by a receiver object.\n"; | ||
+ | $this->receiver->doSomething($this->a); | ||
+ | $this->receiver->doSomethingElse($this->b); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * على بعض منطق العمل المهم، وتعرف كيف (Receiver) تحتوي فئات المستقبِل | ||
+ | * تنفذ كل أنواع العمليات المرتبطة بتنفيذ الطلب. | ||
+ | * يمكن لأي فئة أن تعمل كمستقبِل. | ||
+ | */ | ||
+ | class Receiver | ||
+ | { | ||
+ | public function doSomething(string $a): void | ||
+ | { | ||
+ | echo "Receiver: Working on (" . $a . ".)\n"; | ||
+ | } | ||
+ | |||
+ | public function doSomethingElse(string $b): void | ||
+ | { | ||
+ | echo "Receiver: Also working on (" . $b . ".)\n"; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * بأمر واحد أو أكثر، ويرسل الطلب إلى الأمر (Invoker) يرتبط المستدعي | ||
+ | */ | ||
+ | class Invoker | ||
+ | { | ||
+ | /** | ||
+ | * @var Command | ||
+ | */ | ||
+ | private $onStart; | ||
+ | |||
+ | /** | ||
+ | * @var Command | ||
+ | */ | ||
+ | private $onFinish; | ||
+ | |||
+ | /** | ||
+ | * شغِّل الأوامر. | ||
+ | */ | ||
+ | public function setOnStart(Command $command): void | ||
+ | { | ||
+ | $this->onStart = $command; | ||
+ | } | ||
+ | |||
+ | public function setOnFinish(Command $command): void | ||
+ | { | ||
+ | $this->onFinish = $command; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * لا يعتمد المستدعي على فئات الأمر الحقيقي أو على المستقبِل، بل يمرر المستدعي | ||
+ | * الطلبَ إلى المستقبِل بطريقة غير مباشرة عبر تنفيذ أحد الأوامر. | ||
+ | |||
+ | */ | ||
+ | public function doSomethingImportant(): void | ||
+ | { | ||
+ | echo "Invoker: Does anybody want something done before I begin?\n"; | ||
+ | if ($this->onStart instanceof Command) { | ||
+ | $this->onStart->execute(); | ||
+ | } | ||
+ | |||
+ | echo "Invoker: ...doing something really important...\n"; | ||
+ | |||
+ | echo "Invoker: Does anybody want something done after I finish?\n"; | ||
+ | if ($this->onFinish instanceof Command) { | ||
+ | $this->onFinish->execute(); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * (Invoker) تستطيع شيفرة العميل أن تدخل أي أمر كمعامِل إلى المستدعي. | ||
+ | */ | ||
+ | $invoker = new Invoker; | ||
+ | $invoker->setOnStart(new SimpleCommand("Say Hi!")); | ||
+ | $receiver = new Receiver; | ||
+ | $invoker->setOnFinish(new ComplexCommand($receiver, "Send email", "Save report")); | ||
+ | |||
+ | $invoker->doSomethingImportant(); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== Output.txt: نتائج التنفيذ ==== | ||
+ | <syntaxhighlight> | ||
+ | Invoker: Does anybody want something done before I begin? | ||
+ | SimpleCommand: See, I can do simple things like printing (Say Hi!) | ||
+ | Invoker: ...doing something really important... | ||
+ | Invoker: Does anybody want something done after I finish? | ||
+ | ComplexCommand: Complex stuff should be done by a receiver object. | ||
+ | Receiver: Working on (Send email.) | ||
+ | Receiver: Also working on (Save report.) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === مثال واقعي === | ||
+ | يُستخدم نمط الأمر Command في هذا المثال لصفّ استدعاءات الاستخلاص (Web scraping calls) إلى موقع IMDB ومن ثم تنفيذها واحدًا تلو الآخر، ويُحفظ الصف نفسه في قاعدة بيانات تساهم في الاحتفاظ بالأوامر بين إطلاقات الشيفرة النصية (Script Launches). | ||
+ | |||
+ | ==== index.php: مثال واقعي ==== | ||
+ | <syntaxhighlight lang="php"> | ||
+ | <?php | ||
+ | |||
+ | namespace RefactoringGuru\Command\RealWorld; | ||
+ | |||
+ | /** | ||
+ | * تصرح واجهة الأمر عن أسلوب التنفيذ الأساسي إضافة إلى عدة أساليب مساعدة لاسترجاع | ||
+ | * البيانات الوصفية للأمر. | ||
+ | */ | ||
+ | interface Command | ||
+ | { | ||
+ | public function execute(): void; | ||
+ | |||
+ | public function getId(): int; | ||
+ | |||
+ | public function getStatus(): int; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * البنيةَ التحتيةَ الأساسية للتحميل، والمشتركة (Web Scraping) يعرِّف أمرُ استخلاص الويب الأساسي | ||
+ | * بين كل أوامر استخلاص الويب الحقيقية. | ||
+ | */ | ||
+ | abstract class WebScrapingCommand implements Command | ||
+ | { | ||
+ | public $id; | ||
+ | |||
+ | public $status = 0; | ||
+ | |||
+ | /** | ||
+ | * @var الرابط النصي للاستخلاص. | ||
+ | */ | ||
+ | public $url; | ||
+ | |||
+ | public function __construct(string $url) | ||
+ | { | ||
+ | $this->url = $url; | ||
+ | } | ||
+ | |||
+ | public function getId(): int | ||
+ | { | ||
+ | return $this->id; | ||
+ | } | ||
+ | |||
+ | public function getStatus(): int | ||
+ | { | ||
+ | return $this->status; | ||
+ | } | ||
+ | |||
+ | public function getURL(): string | ||
+ | { | ||
+ | return $this->url; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * بما أن أساليب التنفيذ لكل أوامر استخلاص الويب متشابهة، فيمكننا توفير | ||
+ | * استخدام افتراضي ونعطي للفئات الفرعية صلاحية تخطيها إن دعت الحاجة. | ||
+ | * | ||
+ | * قد تلاحظ نمطًا سلوكيًا آخر هنا إن دققت النظر! | ||
+ | */ | ||
+ | public function execute(): void | ||
+ | { | ||
+ | $html = $this->download(); | ||
+ | $this->parse($html); | ||
+ | $this->complete(); | ||
+ | } | ||
+ | |||
+ | public function download(): string | ||
+ | { | ||
+ | $html = file_get_contents($this->getURL()); | ||
+ | echo "WebScrapingCommand: Downloaded {$this->url}\n"; | ||
+ | |||
+ | return $html; | ||
+ | } | ||
+ | |||
+ | abstract public function parse(string $html): void; | ||
+ | |||
+ | public function complete(): void | ||
+ | { | ||
+ | $this->status = 1; | ||
+ | Queue::get()->completeCommand($this); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * الأمر الحقيقي لاستخلاص قائمة من تصانيف الأفلام. | ||
+ | */ | ||
+ | class IMDBGenresScrapingCommand extends WebScrapingCommand | ||
+ | { | ||
+ | public function __construct() | ||
+ | { | ||
+ | $this->url = "https://www.imdb.com/feature/genre/"; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * استخرج كل التصانيف وروابط بحثها من الصفحة التالية: | ||
+ | * https://www.imdb.com/feature/genre/ | ||
+ | */ | ||
+ | public function parse($html): void | ||
+ | { | ||
+ | preg_match_all("|href=\"(https://www.imdb.com/search/title\?genres=.*?)\"|", $html, $matches); | ||
+ | echo "IMDBGenresScrapingCommand: Discovered " . count($matches[1]) . " genres.\n"; | ||
+ | |||
+ | foreach ($matches[1] as $genre) { | ||
+ | Queue::get()->add(new IMDBGenrePageScrapingCommand($genre)); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * الأمر الحقيقي لاستخلاص قائمة من الأفلام في تصنيف معين. | ||
+ | */ | ||
+ | class IMDBGenrePageScrapingCommand extends WebScrapingCommand | ||
+ | { | ||
+ | private $page; | ||
+ | |||
+ | public function __construct(string $url, int $page = 1) | ||
+ | { | ||
+ | parent::__construct($url); | ||
+ | $this->page = $page; | ||
+ | } | ||
+ | |||
+ | public function getURL(): string | ||
+ | { | ||
+ | return $this->url . '?page=' . $this->page; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * استخرج كل الأفلام من صفحة كهذه: | ||
+ | * https://www.imdb.com/search/title?genres=sci-fi&explore=title_type,genres | ||
+ | */ | ||
+ | public function parse(string $html): void | ||
+ | { | ||
+ | preg_match_all("|href=\"(/title/.*?/)\?ref_=adv_li_tt\"|", $html, $matches); | ||
+ | echo "IMDBGenrePageScrapingCommand: Discovered " . count($matches[1]) . " movies.\n"; | ||
+ | |||
+ | foreach ($matches[1] as $moviePath) { | ||
+ | $url = "https://www.imdb.com" . $moviePath; | ||
+ | Queue::get()->add(new IMDBMovieScrapingCommand($url)); | ||
+ | } | ||
+ | |||
+ | // رابط الصفحة التالية (Parse) حلل.. | ||
+ | if (preg_match("|Next »</a>|", $html)) { | ||
+ | Queue::get()->add(new IMDBGenrePageScrapingCommand($this->url, $this->page + 1)); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * الأمر الحقيقي لاستخلاص تفاصيل الفيلم. | ||
+ | */ | ||
+ | class IMDBMovieScrapingCommand extends WebScrapingCommand | ||
+ | { | ||
+ | /** | ||
+ | * احصل على معلومات الفيلم من صفحة كهذه: | ||
+ | * https://www.imdb.com/title/tt4154756/ | ||
+ | */ | ||
+ | public function parse(string $html): void | ||
+ | { | ||
+ | if (preg_match("|<h1 itemprop=\"name\" class=\"\">(.*?)</h1>|", $html, $matches)) { | ||
+ | $title = $matches[1]; | ||
+ | } | ||
+ | echo "IMDBMovieScrapingCommand: Parsed movie $title.\n"; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * فتكدِّس كائنات الأمر وتنفذها واحدًا تلو الآخر ،(Invoker) كمستدعي Queue تتصرف فئة | ||
+ | * فإن أوُقِف تنفيذُ الشيفرة النصية فجأة فإن الصف وكل أوامره يمكن استرجاعهم جميعًا بسهولة. | ||
+ | * ولن تضطر إلى تكرار تنفيذ كل الأوامر التي سبق تنفيذها. | ||
+ | * | ||
+ | * الذي يخزن الأوامر في قاعدة (Command Queue) لاحظ أن هذا تطبيق بدائي جدًا لصف الأوامر | ||
+ | * محلية، فهناك عشرات الحلول المجدية المتاحة للاستخدام في التطبيقات الحقيقية SQLite بيانات. | ||
+ | */ | ||
+ | class Queue | ||
+ | { | ||
+ | private $db; | ||
+ | |||
+ | public function __construct() | ||
+ | { | ||
+ | $this->db = new \SQLite3(__DIR__ . '/commands.sqlite', | ||
+ | SQLITE3_OPEN_CREATE | SQLITE3_OPEN_READWRITE); | ||
+ | |||
+ | $this->db->query('CREATE TABLE IF NOT EXISTS "commands" ( | ||
+ | "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, | ||
+ | "command" TEXT, | ||
+ | "status" INTEGER | ||
+ | )'); | ||
+ | } | ||
+ | |||
+ | public function isEmpty(): bool | ||
+ | { | ||
+ | $query = 'SELECT COUNT("id") FROM "commands" WHERE status = 0'; | ||
+ | |||
+ | return $this->db->querySingle($query) === 0; | ||
+ | } | ||
+ | |||
+ | public function add(Command $command): void | ||
+ | { | ||
+ | $query = 'INSERT INTO commands (command, status) VALUES (:command, :status)'; | ||
+ | $statement = $this->db->prepare($query); | ||
+ | $statement->bindValue(':command', base64_encode(serialize($command))); | ||
+ | $statement->bindValue(':status', $command->getStatus()); | ||
+ | $statement->execute(); | ||
+ | } | ||
+ | |||
+ | public function getCommand(): Command | ||
+ | { | ||
+ | $query = 'SELECT * FROM "commands" WHERE "status" = 0 LIMIT 1'; | ||
+ | $record = $this->db->querySingle($query, true); | ||
+ | $command = unserialize(base64_decode($record["command"])); | ||
+ | $command->id = $record['id']; | ||
+ | |||
+ | return $command; | ||
+ | } | ||
+ | |||
+ | public function completeCommand(Command $command): void | ||
+ | { | ||
+ | $query = 'UPDATE commands SET status = :status WHERE id = :id'; | ||
+ | $statement = $this->db->prepare($query); | ||
+ | $statement->bindValue(':status', $command->getStatus()); | ||
+ | $statement->bindValue(':id', $command->getId()); | ||
+ | $statement->execute(); | ||
+ | } | ||
+ | |||
+ | public function work(): void | ||
+ | { | ||
+ | while (!$this->isEmpty()) { | ||
+ | $command = $this->getCommand(); | ||
+ | $command->execute(); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * من باب تسهيل المثال (Singleton) سنجعل كائن الصفِّ هنا مفردةً. | ||
+ | */ | ||
+ | public static function get(): Queue | ||
+ | { | ||
+ | static $instance; | ||
+ | if (!$instance) { | ||
+ | $instance = new Queue; | ||
+ | } | ||
+ | |||
+ | return $instance; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * شيفرة العميل. | ||
+ | */ | ||
+ | |||
+ | $queue = Queue::get(); | ||
+ | |||
+ | if ($queue->isEmpty()) { | ||
+ | $queue->add(new IMDBGenresScrapingCommand); | ||
+ | } | ||
+ | |||
+ | $queue->work(); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== Output.txt: نتائج التنفيذ ==== | ||
+ | <syntaxhighlight> | ||
+ | WebScrapingCommand: Downloaded https://www.imdb.com/feature/genre/ | ||
+ | IMDBGenresScrapingCommand: Discovered 14 genres. | ||
+ | WebScrapingCommand: Downloaded https://www.imdb.com/search/title?genres=comedy | ||
+ | IMDBGenrePageScrapingCommand: Discovered 50 movies. | ||
+ | WebScrapingCommand: Downloaded https://www.imdb.com/search/title?genres=sci-fi | ||
+ | IMDBGenrePageScrapingCommand: Discovered 50 movies. | ||
+ | WebScrapingCommand: Downloaded https://www.imdb.com/search/title?genres=horror | ||
+ | IMDBGenrePageScrapingCommand: Discovered 50 movies. | ||
+ | WebScrapingCommand: Downloaded https://www.imdb.com/search/title?genres=romance | ||
+ | IMDBGenrePageScrapingCommand: Discovered 50 movies. | ||
+ | ... | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == الاستخدام في لغة بايثون == | ||
+ | '''المستوى:''' ★ ☆ ☆ | ||
+ | |||
+ | '''الانتشار:''' ★ ★ ★ | ||
+ | |||
+ | '''أمثلة الاستخدام:''' يكثر استخدام نمط الأمر في لغة بايثون، ويستخدم عادة كبديل للاستدعاءات إلى إدخال إجراءاتٍ (Actions) كمعامِلات (Parameters) إلى عناصر الواجهة الرسومية. كما تستخدم أيضًا لصف المهام (Queuing Tasks) وتتبع سجل العمليات، إلخ. | ||
+ | |||
+ | يمكن ملاحظة نمط الأمر من خلال الأساليب السلوكية في نوعٍ مجردٍ أو واجهةٍ (المرسل) الذي يستدعي أسلوبًا في تطبيق لنوعٍ مجردٍ أو واجهةٍ مختلف (المستقبِل) الذي اختُزِل بواسطة تطبيق الأمر أثناء إنشائه، وتكون فئات الأمر عادة محدودة بإجراءات محددة. | ||
+ | |||
+ | === مثال تصوري === | ||
+ | يوضح هذا المثال بنية نمط الأمر (Command)، ويركز على إجابة الأسئلة التالية: | ||
+ | * ما الفئات التي يتكون منها؟ | ||
+ | * ما الأدوار التي تلعبها هذه الفئات؟ | ||
+ | * كيف ترتبط عناصر النمط ببعضها؟ | ||
+ | main.py: مثال تصوري<syntaxhighlight lang="python"> | ||
+ | from __future__ import annotations | ||
+ | from abc import ABC, abstractmethod | ||
+ | |||
+ | |||
+ | class Command(ABC): | ||
+ | """ | ||
+ | تصرح واجهة الأمر عن أسلوب لتنفيذ أحد الأوامر. | ||
+ | """ | ||
+ | |||
+ | @abstractmethod | ||
+ | def execute(self) -> None: | ||
+ | pass | ||
+ | |||
+ | |||
+ | class SimpleCommand(Command): | ||
+ | """ | ||
+ | تستطيع بعض الأوامر إجراء عمليات بأنفسها. | ||
+ | """ | ||
+ | |||
+ | def __init__(self, payload: str) -> None: | ||
+ | self._payload = payload | ||
+ | |||
+ | def execute(self) -> None: | ||
+ | print(f"SimpleCommand: See, I can do simple things like printing" | ||
+ | f"({self._payload})") | ||
+ | |||
+ | |||
+ | class ComplexCommand(Command): | ||
+ | """ | ||
+ | (receivers) لكن بعض الأوامر تفوض العمليات المعقدة إلى كائنات أخرى تسمى المستقبِلات. | ||
+ | """ | ||
+ | |||
+ | def __init__(self, receiver: Receiver, a: str, b: str) -> None: | ||
+ | """ | ||
+ | تقبل الأوامر المعقدة كائن مستقبِل واحد أو أكثر إضافة إلى أي | ||
+ | (Constructor) بيانات سياقية من خلال المنشئ. | ||
+ | """ | ||
+ | |||
+ | self._receiver = receiver | ||
+ | self._a = a | ||
+ | self._b = b | ||
+ | |||
+ | def execute(self) -> None: | ||
+ | """ | ||
+ | تستطيع الأوامر أن تفوض إلى أي أسلوب من أساليب المستقبِل. | ||
+ | """ | ||
+ | |||
+ | print("ComplexCommand: Complex stuff should be done by a receiver object", end="") | ||
+ | self._receiver.do_something(self._a) | ||
+ | self._receiver.do_something_else(self._b) | ||
+ | |||
+ | |||
+ | class Receiver: | ||
+ | """ | ||
+ | على بعض منطق العمل المهم، وتعرف كيف (Receiver) تحتوي فئات المستقبِل | ||
+ | تنفذ كل أنواع العمليات المرتبطة بتنفيذ الطلب. بل يمكن لأي فئة أن تعمل كمستقبِل. | ||
+ | """ | ||
+ | |||
+ | def do_something(self, a: str) -> None: | ||
+ | print(f"\nReceiver: Working on ({a}.)", end="") | ||
+ | |||
+ | def do_something_else(self, b: str) -> None: | ||
+ | print(f"\nReceiver: Also working on ({b}.)", end="") | ||
+ | |||
+ | |||
+ | class Invoker: | ||
+ | """ | ||
+ | بأمر واحد أو أكثر، ويرسل الطلب إلى الأمر (Invoker) يرتبط المستدعي. | ||
+ | """ | ||
+ | |||
+ | _on_start = None | ||
+ | _on_finish = None | ||
+ | |||
+ | """ | ||
+ | شغِّل الأوامر. | ||
+ | """ | ||
+ | |||
+ | def set_on_start(self, command: Command): | ||
+ | self._on_start = command | ||
+ | |||
+ | def set_on_finish(self, command: Command): | ||
+ | self._on_finish = command | ||
+ | |||
+ | def do_something_important(self) -> None: | ||
+ | """ | ||
+ | لا يعتمد المستدعي على فئات الأمر الحقيقي أو على المستقبِل، بل يمرر المستدعي الطلبَ | ||
+ | إلى المستقبِل بطريقة غير مباشرة عبر تنفيذ أحد الأوامر. | ||
+ | """ | ||
+ | |||
+ | print("Invoker: Does anybody want something done before I begin?") | ||
+ | if isinstance(self._on_start, Command): | ||
+ | self._on_start.execute() | ||
+ | |||
+ | print("Invoker: ...doing something really important...") | ||
+ | |||
+ | print("Invoker: Does anybody want something done after I finish?") | ||
+ | if isinstance(self._on_finish, Command): | ||
+ | self._on_finish.execute() | ||
+ | |||
+ | |||
+ | if __name__ == "__main__": | ||
+ | """ | ||
+ | (Invoker) تستطيع شيفرة العميل أن تدخل أي أمر كمعامِل إلى المستدعي. | ||
+ | """ | ||
+ | |||
+ | invoker = Invoker() | ||
+ | invoker.set_on_start(SimpleCommand("Say Hi!")) | ||
+ | receiver = Receiver() | ||
+ | invoker.set_on_finish(ComplexCommand( | ||
+ | receiver, "Send email", "Save report")) | ||
+ | |||
+ | invoker.do_something_important() | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== Output.txt: نتائج التنفيذ ==== | ||
+ | <syntaxhighlight> | ||
+ | Invoker: Does anybody want something done before I begin? | ||
+ | SimpleCommand: See, I can do simple things like printing (Say Hi!) | ||
+ | Invoker: ...doing something really important... | ||
+ | Invoker: Does anybody want something done after I finish? | ||
+ | ComplexCommand: Complex stuff should be done by a receiver object | ||
+ | Receiver: Working on (Send email.) | ||
+ | Receiver: Also working on (Save report.) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == الاستخدام في لغة روبي == | ||
+ | '''المستوى:''' ★ ☆ ☆ | ||
+ | |||
+ | '''الانتشار:''' ★ ★ ★ | ||
+ | |||
+ | '''أمثلة الاستخدام:''' يكثر استخدام نمط الأمر في لغة روبي، ويستخدم عادة كبديل للاستدعاءات إلى إدخال إجراءاتٍ (Actions) كمعامِلات (Parameters) إلى عناصر الواجهة الرسومية. كما تستخدم أيضًا لصف المهام (Queuing Tasks) وتتبع سجل العمليات، إلخ. | ||
+ | |||
+ | يمكن ملاحظة نمط الأمر من خلال الأساليب السلوكية في نوعٍ مجردٍ أو واجهةٍ (المرسل) الذي يستدعي أسلوبًا في تطبيق لنوعٍ مجردٍ أو واجهةٍ مختلف (المستقبِل) الذي اختُزِل بواسطة تطبيق الأمر أثناء إنشائه، وتكون فئات الأمر عادة محدودة بإجراءات محددة. | ||
+ | |||
+ | === مثال تصوري === | ||
+ | يوضح هذا المثال بنية نمط الأمر (Command)، ويركز على إجابة الأسئلة التالية: | ||
+ | * ما الفئات التي يتكون منها؟ | ||
+ | * ما الأدوار التي تلعبها هذه الفئات؟ | ||
+ | * كيف ترتبط عناصر النمط ببعضها؟ | ||
+ | |||
+ | ==== main.rb: مثال تصوري ==== | ||
+ | <syntaxhighlight lang="ruby"> | ||
+ | # تصرح واجهة الأمر عن أسلوب لتنفيذ أحد الأوامر. | ||
+ | class Command | ||
+ | # @abstract | ||
+ | def execute | ||
+ | raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" | ||
+ | end | ||
+ | end | ||
+ | |||
+ | # تستطيع بعض الأوامر إجراء عمليات بأنفسها. | ||
+ | class SimpleCommand < Command | ||
+ | # @param [String] payload | ||
+ | def initialize(payload) | ||
+ | @payload = payload | ||
+ | end | ||
+ | |||
+ | def execute | ||
+ | puts "SimpleCommand: See, I can do simple things like printing (#{@payload})" | ||
+ | end | ||
+ | end | ||
+ | |||
+ | # (receivers) لكن بعض الأوامر تفوض العمليات المعقدة إلى كائنات أخرى تسمى المستقبِلات. | ||
+ | class ComplexCommand < Command | ||
+ | # تقبل الأوامر المعقدة كائن مستقبِل واحد أو أكثر إضافة إلى أي بيانات سياقية | ||
+ | # (Constructor) من خلال المنشئ. | ||
+ | def initialize(receiver, a, b) | ||
+ | @receiver = receiver | ||
+ | @a = a | ||
+ | @b = b | ||
+ | end | ||
+ | |||
+ | # تستطيع الأوامر أن تفوض إلى أي أسلوب من أساليب المستقبِل. | ||
+ | def execute | ||
+ | print 'ComplexCommand: Complex stuff should be done by a receiver object' | ||
+ | @receiver.do_something(@a) | ||
+ | @receiver.do_something_else(@b) | ||
+ | end | ||
+ | end | ||
+ | |||
+ | # على بعض منطق العمل المهم، وتعرف كيف (Receiver) تحتوي فئات المستقبِل | ||
+ | # تنفذ كل أنواع العمليات المرتبطة بتنفيذ الطلب، بل يمكن لأي فئة أن تعمل كمستقبِل. | ||
+ | class Receiver | ||
+ | # @param [String] a | ||
+ | def do_something(a) | ||
+ | print "\nReceiver: Working on (#{a}.)" | ||
+ | end | ||
+ | |||
+ | # @param [String] b | ||
+ | def do_something_else(b) | ||
+ | print "\nReceiver: Also working on (#{b}.)" | ||
+ | end | ||
+ | end | ||
+ | |||
+ | # The Invoker is associated with one or several commands. It sends a request to | ||
+ | # the command. | ||
+ | class Invoker | ||
+ | # Initialize commands. | ||
+ | |||
+ | # @param [Command] command | ||
+ | def on_start=(command) | ||
+ | @on_start = command | ||
+ | end | ||
+ | |||
+ | # @param [Command] command | ||
+ | def on_finish=(command) | ||
+ | @on_finish = command | ||
+ | end | ||
+ | |||
+ | # لا يعتمد المستدعي على فئات الأمر الحقيقي أو على المستقبِل، بل يمرر المستدعي الطلبَ إلى | ||
+ | # المستقبِل بطريقة غير مباشرة عبر تنفيذ أحد الأوامر. | ||
+ | def do_something_important | ||
+ | puts 'Invoker: Does anybody want something done before I begin?' | ||
+ | @on_start.execute if @on_start.is_a? Command | ||
+ | |||
+ | puts 'Invoker: ...doing something really important...' | ||
+ | |||
+ | puts 'Invoker: Does anybody want something done after I finish?' | ||
+ | @on_finish.execute if @on_finish.is_a? Command | ||
+ | end | ||
+ | end | ||
+ | |||
+ | # (Invoker) تستطيع شيفرة العميل أن تدخل أي أمر كمعامِل إلى المستدعي. | ||
+ | invoker = Invoker.new | ||
+ | invoker.on_start = SimpleCommand.new('Say Hi!') | ||
+ | receiver = Receiver.new | ||
+ | invoker.on_finish = ComplexCommand.new(receiver, 'Send email', 'Save report') | ||
+ | |||
+ | invoker.do_something_important | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== output.txt: نتائج التنفيذ ==== | ||
+ | <syntaxhighlight> | ||
+ | Invoker: Does anybody want something done before I begin? | ||
+ | SimpleCommand: See, I can do simple things like printing (Say Hi!) | ||
+ | Invoker: ...doing something really important... | ||
+ | Invoker: Does anybody want something done after I finish? | ||
+ | ComplexCommand: Complex stuff should be done by a receiver object | ||
+ | Receiver: Working on (Send email.) | ||
+ | Receiver: Also working on (Save report.) | ||
</syntaxhighlight> | </syntaxhighlight> | ||
* | * |
مراجعة 18:45، 30 مايو 2019
نمط الأمر (Command) هو نمط تصميم سلوكي (Behavioral Design Pattern) يحول الطلب إلى كائن مستقل بذاته بداخله كل بيانات الطلب، ويسمح لك هذا التحول بإدخال طلبات مختلفة كمعامِلات (Parameters) داخل الأساليب، وتأخير تنفيذ الطلب أو وضعه في صف انتظار، ودعم العمليات غير الممكنة (Undoable).
المشكلة
تخيل أنك تعمل على إنشاء محرر نصي جديد، ومهمتك الحالية هي إنشاء شريط أدوات به بضعة أزرار لعمليات مختلفة داخل المحرر، وقد أنشأتَ فئة Button
يمكن استخدامها للأزرار التي في شريط الأدوات وكذلك في الأزرار داخل الصناديق الحوارية المختلفة.
الصورة. (ش.1) كل أزرار التطبيق تنحدر من نفس الفئة.
ورغم أن جميع تلك الأزرار تبدو متشابهة إلا أنها يُفترض أن تنفذ مهام مختلفة، ويبدو الحل الأسهل لكتابة شيفرة لمعالِجات النقرات المختلفة لتلك الأزرار هو إنشاء فئات فرعية كثيرة لكل مكان يُستخدم فيه الزر، وستحتوي تلك الفئات الشيفرة التي يجب تنفيذها عند النقر على الزر.
الصورة. (ش.2) فئات فرعية كثيرة لفئة Button، ما أسوأ ما قد يحدث؟
لكن مع الوقت سترى أن هذا المنظور به الكثير من العيوب، فلديك عدد هائل من الفئات الفرعية، ولم يكن هذا ليمثل مشكلة لو لم تكن تخاطر بتعطيل الشيفرة في هذه الفئات الفرعية في كل مرة تعدِّل فيها فئة Button
الأساسية. فكأن شيفرة الواجهة الرسومية للمحرر صارت معتمدة بشكل غريب على شيفرة متطايرة لمنطق العمل (Business Logic).
الصورة. (ش.3) عدة فئات لها نفس الوظيفة.
والمشكلة هنا أن بعض العمليات مثل النسخ واللصق ستحدث من أماكن متعددة، فمثلًا قد يضغط مستخدم على زر "نسخ" صغير على شريط الأدوات، أو ينسخ شيئًا من خلال القائمة المنسدلة، أو ربما حتى يستخدم اختصار لوحة المفاتيح المشهور Ctrl+C
.
ولم يكن ثمة بأس في وضع تطبيقات العمليات المختلفة في فئات الزر الفرعية حين لم يكن في المحرر سوى شريط الأدوات، بمعنى أن وجود شيفرة نسخ النصوص داخل فئة CopyButton
فرعية مثلًا كان لا بأس به. لكنك تضطر إلى تكرار شيفرة العملية في عدة فئات أو جعل القوائم معتمدة على الأزرار، وذلك عندما تستخدم القوائم السياقية (Context Menus) والاختصارات وغيرها، وهو خيار أسوأ من الأول.
الحل
يبنى التصميم الجيد للبرمجيات على مبدأ فصل المشاكل والمهام ومن ثم تقسيم البرنامج إلى طبقات، والمثال المشهور في هذا الباب هو أن يكون لديك طبقة لواجهة المستخدم الرسومية وأخرى لمنطق العمل (Business Logic)، وتكون طبقة الواجهة الرسومية مسؤولة عن إخراج الصورة الجميلة على الشاشة والتقاط المدخلات وعرض نتيجة ما يفعله كل من المستخدم والتطبيق. لكن عند تنفيذ شيء مهم مثل حساب مسار القمر مثلًا أو إنشاء تقرير سنوي، فإن طبقة الواجهة الرسومية تفوض العمل إلى الطبقة التي تحتها، وهي طبقة منطق العمل.
وقد تبدو الشيفرة التي تعبر عن مثالنا هذا كالتالي: يستدعي كائن GUI أسلوبًا من كائن منطق عمل، ممررًا إليه بعض الوسائط (Arguments)، وتوصف هذه العملية بأنها كائن يرسل طلبًا إلى كائن آخر.
الصورة. (ش.4) قد تتواصل كائنات الواجهة الرسومية مع كائنات منطق العمل مباشرة.
ويقترح نمط الأمر (Command) أن كائنات الواجهة الرسومية يجب ألا ترسل تلك الطلبات بشكل مباشر، بل يجب أن تستخرج كل تفاصيل الطلب مثل الكائن المستدعَى واسم الأسلوب وقائمة الوسائط (Arguments)، تستخرجها إلى فئة command مع أسلوب وحيد يشغِّل (trigger) هذا الطلب.
وتتصرف كائنات الأمر (Command Objects) هنا كروابط بين كائنات الواجهة الرسومية وكائنات منطق العمل المختلفة، ومن تلك النقطة فإن كائن الواجهة الرسومية لا يحتاج أن يعرف أي كائن منطق عمل سيستلم الطلب ولا كيف سيعالجه، بل هو -أي كائن الواجهة- يبدأ الأمر الذي يعالج كل تلك التفاصيل.
الصورة. (ش.5) الوصول إلى طبقة منطق العمل من خلال أمر.
والخطوة التالية هي جعل أوامرك تستخدم نفس الواجهة، وعادة يكون لديها أسلوب تنفيذ وحيد لا يأخذ أي معامِلات (Parameters)، وتسمح لك هذه الواجهة باستخدام أوامر مختلفة مع نفس مرسل الطلب دون ربط ذلك مع الفئات الحقيقية للأوامر. وزيادة على ذلك تستطيع الآن تبديل كائنات الأمر المرتبطة مع المرسل، مغيرًا بهذا سلوك المرسل أثناء وقت التشغيل (runtime).
ويكون الأمر (Command) مجهز مسبقًا بتفاصيل الطلب التي ستُرسَل إلى المستقبِل أو يستطيع الحصول عليها بنفسه، ذلك أننا نريد تمرير تفاصيل الطلب إلى المستقبِل لكن أسلوب تنفيذ الأمر ليس فيه أي معامِلات (Parameters).
الصورة. (ش.6) تفوض كائنات الواجهة الرسومية العمل إلى الأوامر.
وبالعودة إلى مثال المحرر النصي، فبعد تطبيق نمط الأمر فإننا لا نكون بحاجة إلى كل فئات الزر الفرعية تلك لتطبيق سلوكيات مختلفة للنقرات، فيكفي وضع حق واحد داخل فئة Button
الأساسية تخزن مرجعًا إلى كائن أمر وتجعل الزر ينفذ هذا الأمر عند النقر. ستستخدم مجموعة من فئات الأمر لكل عملية ممكنة وتربطهم جميعًا مع أزرار معينة بناءً على السلوك الذي تريده للأزرار.
أما عناصر الواجهة الرسومية الأخرى مثل القوائم والاختصارات أو الصناديق الحوارية، فيمكن تطبيقها (Implement) بنفس الطريقة، وستكون تلك العناصر مرتبطة بأمر يُنفَّذ عندما يتفاعل مستخدم مع عنصر الواجهة الرسومية، وستكون العناصر المتعلقة بنفس العمليات مرتبطة بنفس الأوامر لمنع تكرار الشيفرة. وكنتيجة لهذا تشكل الأوامر طبقة وسطى تقلل الازدواج أو الربط (Coupling) بين طبقتي الواجهة الرسومية ومنطق العمل، وهذه النتيجة جزء بسيط من منافع نمط الأمر.
مثال واقعي
الصورة. (ش.7) إعداد طلب في مطعم.
لنفرض أنك دخلت مطعمًا وجلست إلى طاولة فيه وأتاك النادل ليأخذ طلبك ويكتبه على ورقة معه، سيذهب النادل بعدها إلى المطبخ ليلصق الورقة على الحائط، ولا يمر وقت طويل حتى يصل الطلب إلى الطاهي الذي يقرؤه ويطهو الطعام الذي طلبتَه، حتى إذا انتهى من إعداده يضعه على صينية مع طلبك، ويراها النادل فيأتي ليتأكد أن كل ما طلبتَه موجود ثم يحضر إليك الطعام إلى طاولتك.
في هذا المثال السابق، تكون الورقة بمثابة الأمر، وتبقى الورقة في صف انتظار حتى يكون الطاهي جاهزًا لإعدادها، ويحتوي الطلب كل البيانات المرتبطة بالوجبة وتفاصيلها التي يحتاجها الطاهي ليضعها في حسابه أثناء إعدادها، فيشرع في طهيها مباشرة دون إضاعة وقت معك من أجل توضيح تفاصيل الوجبة الذي طلبتَها.
البنية
الصورة. (ش.8)
- تكون فئة المرسِل Sender (أو المستدعي Invoker) مسؤولة عن بدء الطلبات، ويجب أن تحتوي هذه الفئة على حقل لتخزين مرجع إلى كائنِ أمر، ويشغِّل المرسل هذا الأمر بدلًا من إرسال الطلب مباشرة إلى المستقبِل، لاحظ أن المرسل ليس مسؤولًا عن إنشاء كائن الأمر، فعادة ما يحصل على أمر منشأ مسبقًا من العميل من خلال المنشئ (Constructor).
- تصرح واجهة الأمر Command عادة عن أسلوب واحد لتنفيذ الأمر.
- تستخدم الأوامر الحقيقية Concrete Commands أنواعًا مختلفة من الطلبات، ولا يفترض بالأمر الحقيقي أن ينفذ العمل بنفسه، بل يمرر الاستدعاء إلى أحد كائنات منطق العمل. لكن يمكن دمج تلك الفئات بداعي تبسيط الشيفرة.
- تحتوي فئة المستقبِل Receiver على بعض منطق العمل، ويتصرف أي كائن تقريبًا كمستقبِل، وتعالج أغلب الأوامر تفاصيل تمرير الطلب إلى المستقبِل فقط، بينما ينفذ المستقبِل نفسه أغلب العمل الفعلي.
- ينشئ العميل Client كائنات الأمر الحقيقي ويهيؤها، ويجب أن يمرر العميل كل معامِلات الطلب بما فيها نسخة المستقبِل (Receiver Instance) إلى منشئ الأمر، ثم يكون الأمر الناتج بعدها مرتبطًا مع مرسِل واحد أو أكثر.
مثال توضيحي
في هذا المثال يساعد نمط الأمر على تتبع تاريخ العمليات المنفَّذة ويجعل عكس أي عملية ممكنًا عند الحاجة إلى ذلك.
الصورة. (ش.9) العمليات غير القابلة للعكس -لا يمكن التراجع عنها Undoable- في محرر نصي.
تنشئ الأوامر التي تُحدث تغييرات في حالة المحرر (مثل النسخ واللصق) نسخةً احتياطية من حالة المحرر قبل تنفيذ العملية المرتبطة بالأمر، ويوضع في سجل الأوامر بعد تنفيذه (سجل الأوامر Command History: مكدَّس من كائنات الأمر) مع النسخة الاحتياطية من حالة المحرر في تلك النقطة.
ثم إذا احتاج المستخدم أن يعكس عملية ما فإن التطبيق يستطيع أخذ آخر أمر من السجل ويقرأ النسخة الاحتياطية المرتبطة به من حالة المحرر ويخزنها. ولا تكون شيفرة العميل (عناصر الواجهة الرسومية، سجل الأوامر، ... إلخ) مرتبطة بفئات أمر حقيقية لأنها تعمل مع الأوامر من خلال واجهة الأمر، ويتيح لك هذا المنظور أن تدخل أوامر جديدة في التطبيق دون تعطيل الشيفرة الحالية.
// تصرح فئة الأمر الأساسية عن واجهة مشتركة لكل
// (Concrete Commands) الأوامر الحقيقية.
abstract class Command is
protected field app: Application
protected field editor: Editor
protected field backup: text
constructor Command(app: Application, editor: Editor) is
this.app = app
this.editor = editor
// أنشئ نسخة احتياطية من حالة المحرِّر.
method saveBackup() is
backup = editor.text
// استعِدْ حالة المحرِّر.
method undo() is
editor.text = backup
// يُصرح عن أسلوب التنفيذ على أنه أسلوم مجرد، من أجل إجبار كل الأوامر
// بنفسها (Implementations) الحقيقية على توفير تطبيقاتها.
// وفقًا لتغيير الأمر false أو true ويجب أن يعيد الأسلوب إما
// لحالة المحرر من عدمه.
abstract method execute()
// الأوامر الحقيقية تدخل هنا.
class CopyCommand extends Command is
// لا يحفظ أمر النسخ في السجل نظرًا لأنه لا يغير حالة المحرر
method execute() is
app.clipboard = editor.getSelection()
return false
class CutCommand extends Command is
// يغير أمر القص حالة المحرر لذا يجب أن يُحفظ في السجل، وسيظل
// true محفوظًا طالما أن الأسلوب يعيد
method execute() is
saveBackup()
app.clipboard = editor.getSelection()
editor.deleteSelection()
return true
class PasteCommand extends Command is
method execute() is
saveBackup()
editor.replaceSelection(app.clipboard)
return true
// تُعد عملية التراجع أمرًا كذلك.
class UndoCommand extends Command is
method execute() is
app.undo()
return false
// (Stack) السجل العام للأوامر ما هو إلا مكدَّس.
class CommandHistory is
private field history: array of Command
// الأخير دخولًا...
method push(c: Command) is
// أزح الأمر إلى نهاية مصفوفة السجل.
// ...الأول خروجًا
method pop():Command is
// اجلب أحدث أمر من السجل
// فئة المحرر بها عمليات تعديل فعلية على النصوص، وتلعب دور المستقبِل:
// كل الأوامر في النهاية تفوض التنفيذ إلى أساليب المحرر
class Editor is
field text: string
method getSelection() is
// أعد النص المحدَّد.
method deleteSelection() is
// احذف النص المحدد.
method replaceSelection(text) is
// في الموضع الحالي (Clipboard) أدخل محتويات الحافظة.
// تُعِدُّ فئةُ التطبيق علاقات الكائن، وتتصرف كمرسِل: حين نحتاج إلى تنفيذ شيء ما
// فإنها تنشئ كائن أمر وتنفذه.
class Application is
field clipboard: string
field editors: array of Editors
field activeEditor: Editor
field history: CommandHistory
// قد تبدو الشيفرة التي تعيِّن الأوامر إلى كائنات الواجهة كالتالي:
method createUI() is
// ...
copy = function() { executeCommand(
new CopyCommand(this, activeEditor)) }
copyButton.setCommand(copy)
shortcuts.onKeyPress("Ctrl+C", copy)
cut = function() { executeCommand(
new CutCommand(this, activeEditor)) }
cutButton.setCommand(cut)
shortcuts.onKeyPress("Ctrl+X", cut)
paste = function() { executeCommand(
new PasteCommand(this, activeEditor)) }
pasteButton.setCommand(paste)
shortcuts.onKeyPress("Ctrl+V", paste)
undo = function() { executeCommand(
new UndoCommand(this, activeEditor)) }
undoButton.setCommand(undo)
shortcuts.onKeyPress("Ctrl+Z", undo)
// نفِّذ أمرًا وتفقد إن كان يجب أن يضاف إلى السجل.
method executeCommand(command) is
if (command.execute)
history.push(command)
// خذ أحدث أمر من السجل وشغِّل أسلوب التراجع الخاص به، لاحظ أننا لا نعرف فئة
// ذلك الأمر، لكننا لا نحتاج إلى ذلك بأي حال بما أن الأمر يعرف كيف
// يتراجع عن أفعاله الخاصة.
method undo() is
command = history.pop()
if (command != null)
command.undo()
قابلية التطبيق
- استخدم نمط الأمر حين تريد أن تدخل العمليات كمعامِلات إلى الكائنات.
يستطيع نمط الأمر أن يحول أسلوب استدعاء بعينه إلى كائن مستقل، وهذا التحويل يفتح الباب لاستخدامات كثيرة، فتستطيع تمرير الأوامر كوسائط للأساليب (Arguments)، وتخزنها داخل كائنات أخرى، وتبدل الأوامر المرتبطة أثناء وقت التشغيل، إلخ. كأن تطور أحد مكونات الواجهة الرسومية مثلًا كالقائمة المنسدلة، وتريد لمستخدميك أن يستطيعوا إعداد عناصر القائمة التي تشغِّل عمليات حين ينقر المستخدم النهائي على أحد العناصر.
- استخدم نمط الأمر حين تريد أن تضع العمليات في صف أو تجدول تنفيذها أو تنفذها عن بعد.
يمكن وضع الأمر في تسلسل -كأي كائن آخر-، ما يعني تحويله إلى نص (String) يمكن كتابته بسهولة إلى ملف أو قاعدة بيانات، ويمكن استعادة النص لاحقًا على أنه كائن الأمر الأولي، ومن ثم يمكنك تأخير وجدولة تنفيذ الأمر. وبنفس الطريقة تستطيع إرسال الأوامر عبر الشبكة أو وضعها في صف (Queue) أو في سجل (Log).
- استخدم نمط الأمر حين تريد استخدام العمليات القابلة للعكس (Reversible)
برغم وجود عدة طرق لتطبيق عمليات التراجع وإعادة التنفيذ (Undo/Redo) إلا أن نمط الأمر هو أكثر تلك الطرق شهرة تقريبًا. ولكي تستطيع عكس عملية ستحتاج إلى استخدام سجل العمليات المنفَّذة، وسجل الأوامر هو مكدَّس يحتوي كل كائنات الأمر المنفَّذة مع نسخ احتياطية لحالة التطبيق مرتبطة بها.
لكن هذا الأسلوب له عيبان، أولهما أنْ ليس من السهل حفظ حالة التطبيق بسبب أن جزءًا منه قد يكون خاصًا (Private)، وهذه المشكلة يمكن حلها باستخدام نمط التذكرة (Memento). أما الثاني فإن النُسخ الاحتياطية للحالة قد تستهلك جزءًا كبيرًا من الذاكرة العشوائية، لذا فإنك تستطيع أحيانًا الرجوع إلى تطبيق بديل: ينفذ نمط الأمر عملية العكس بدلًا من استعادة الحالة السابقة، لكن عملية العكس قد يكون من الصعب تنفيذها أحيانًا، إن لم يكن مستحيلًا.
كيفية الاستخدام
- صرح عن واجهة الأمر مع أسلوب تنفيذ واحد.
- استخرج الطلبات إلى فئات أمر حقيقية تستخدم واجهة الأمر (Command Interface)، ويجب أن يكون لكل فئة مجموعة حقول لتخزين وسائط الطلب مع مرجع إلى الكائن المستقبِل الحقيقي، ويجب أن تُشغَّل كل تلك القيم من خلال منشئ الأمر (Command Constructor).
- عرِّف الفئات التي ستقوم بدور المرسل، وأضف حقول تخزين الأوامر في هذه الفئات. ينبغي للمرسِلات أن تتواصل مع أوامرها من خلال واجهة الأمر فقط، ولا تنشئ المرسلات عادة كائنات أمر بنفسها، بل تحصل عليها من شيفرة العميل.
- غيِّر المرسِلات بحيث تنفذ الأمر بدلًا من إرسال طلب إلى المستقبِل مباشرة.
- يجب أن يشغِّل العميلُ الكائنات بالترتيب التالي
- إنشاء المستقبِلات.
- إنشاء الأوامر وربطها بالمستقبِلات إن دعت الحاجة إلى ذلك.
- إنشاء المرسِلات وربطها بأوامر محددة.
المزايا والعيوب
المزايا
- مبدأ المسؤولية الواحدة. تستطيع فصل الفئات التي تستدعي عمليات من فئات تنفذ هذه العمليات.
- مبدأ المفتوح/المغلق. تستطيع إدخال أوامر جديدة في التطبيق دون تعطيل شيفرة العميل الحالية.
- تستطيع تطبيق التراجع/إعادة التنفيذ.
- تستطيع تطبيق التنفيذ المؤجل للعمليات.
- تستطيع تجميع أوامر بسيطة داخل أمر واحد أكثر تفصيلًا وتعقيدًا.
العيوب
- قد تصبح الشيفرة أكثر تعقيدًا بما أنك تُدخل طبقة جديدة بين المرسِلات والمستقبِلات.
العلاقات مع الأنماط الأخرى
- تختلف أنماط سلسلة المسؤولية والأمر والوسيط والمراقب في طرق توصيل مرسِلات الطلبات ومستقبِلاتها ببعضها، وترى ذلك الاختلاف فيما يلي:
- تمرِّر سلسلة المسؤولية الطلب بشكل تسلسلي في سلسلة مرنة من المستقبلين المحتملين إلى أن يعالج أحدهم الطلب.
- ينشئ نمط الأمر وصلات أحادية الاتجاه (Unidirectional) بين المرسلين والمستقبلين.
- يلغي نمط الوسيط الاتصالات المباشرة بين المرسلين والمستقبلين مجبرًا إياهم على التواصل بشكل غير مباشر من خلال كائن وسيط.
- يسمح نمط المراقب للمستقبلين بالاشتراك في استلام الطلبات وكذلك إلغاء الاشتراك بمرونة.
- يمكن استخدام المداوِلات في سلسلة المسؤولية كأوامر، وفي تلك الحالة تستطيع تنفيذ عمليات كثيرة مختلفة على نفس الكائن السياقي الممثل في هيئة طلب. لكن هناك طريقة أخرى يكون فيها الطلب نفسه كائن أمر (Command Object)، وفي تلك الحالة تستطيع تنفيذ نفس العملية في تسلسل من السياقات المختلفة المرتبطة في سلسلة.
- تستطيع استخدام نمطي الأمر والتذكرة معًا عند إجراء التراجع (Undo)، وفي تلك الحالة تكون الأوامر مسؤولة عن تنفيذ عمليات مختلفة على كائنٍ هدف، في الوقت الذي تحفظ فيه التذكرة حالة ذلك الكائن قبل تنفيذ الأمر.
- قد يبدو نمط الأمر مشابهًا لنمط الخطة (Strategy) إذ تستطيع استخدام كليهما لإضافة بعض الإجراءات (Actions) كمعامِلات إلى الكائن، لكن هدف كل منهما يختلف كما يلي:
- تستطيع استخدام نمط الأمر لتحويل أي عملية إلى كائن، وتصبح معامِلات العملية حقولًا لذلك الكائن. ويسمح لك هذا التحويل بتأجيل تنفيذ العملية أو وضعها في صف أو تخزين سجل الأوامر أو إرسال الأوامر إلى خدمات بعيدة (Remote Services)، إلخ
- من الناحية الأخرى، يصف نمط الخطة (Strategy) عادة طرقًا مختلفة لتنفيذ نفس الشيء مما يسمح لك بتبديل تلك الخوارزميات داخل فئة سياقية واحدة.
- يُفضل استخدام نمط النموذج الأولي حين تريد حفظ نسخ من الأوامر داخل السجل.
- تستطيع النظر إلى نمط الزائر (Visitor) على أنه نسخة معززة من نمط الأمر، ذلك أن كائناته تستطيع تنفيذ عمليات على عدة كائنات من فئات مختلفة.
الاستخدم في لغة جافا
المستوى: ★ ☆ ☆
الانتشار: ★ ★ ★
أمثلة الاستخدام: يكثر استخدام نمط الأمر في لغة جافا، ويستخدم عادة كبديل للاستدعاءات إلى إدخال إجراءاتٍ (Actions) كمعامِلات (Parameters) إلى عناصر الواجهة الرسومية. كما تستخدم أيضًا لصف المهام (Queuing Tasks) وتتبع سجل العمليات، إلخ. إليك بعض الأمثلة من نمط الأمر في مكتبات جافا:
- كل استخدامات java.lang.Runnable.
- كل استخدامات javax.swing.Action.
يمكن ملاحظة نمط الأمر من خلال الأساليب السلوكية في نوعٍ مجردٍ أو واجهةٍ (المرسل) الذي يستدعي أسلوبًا في تطبيق لنوعٍ مجردٍ أو واجهةٍ مختلف (المستقبِل) الذي اختُزِل بواسطة تطبيق الأمر أثناء إنشائه، وتكون فئات الأمر عادة محدودة بإجراءات محددة.
أوامر المحرر النصي والتراجع (undo)
ينشئ المحرر النصي في هذا المثال كائنات أمر جديدة في كل مرة يتفاعل المستخدم معه، وبعد أن ينفذ الأمر إجراءاته فإن الأمر يُدفع إلى مكدَّس السجل، ولتنفيذ عملية التراجع فإن التطبيق يأخذ آخر أمر منفَّذ من السجل ويتخذ إجراءً عكسيًا أو يسترجع الحالة السابقة للمحرر المحفوظة بذلك الأمر.
الأوامر (Commands)
commands/Command.java: أمر أساسي مجرد
package refactoring_guru.command.example.commands;
import refactoring_guru.command.example.editor.Editor;
public abstract class Command {
public Editor editor;
private String backup;
Command(Editor editor) {
this.editor = editor;
}
void backup() {
backup = editor.textField.getText();
}
public void undo() {
editor.textField.setText(backup);
}
public abstract boolean execute();
}
commands/CopyCommand.java: نسخ النص المحدد إلى الحافظة (Clipboard)
package refactoring_guru.command.example.commands;
import refactoring_guru.command.example.editor.Editor;
public class CopyCommand extends Command {
public CopyCommand(Editor editor) {
super(editor);
}
@Override
public boolean execute() {
editor.clipboard = editor.textField.getSelectedText();
return false;
}
}
commands/PasteCommand.java: لصق النص من الحافظة
package refactoring_guru.command.example.commands;
import refactoring_guru.command.example.editor.Editor;
public class PasteCommand extends Command {
public PasteCommand(Editor editor) {
super(editor);
}
@Override
public boolean execute() {
if (editor.clipboard == null || editor.clipboard.isEmpty()) return false;
backup();
editor.textField.insert(editor.clipboard, editor.textField.getCaretPosition());
return true;
}
commands/CutCommand.java: قص النص ووضعه في الحافظة
package refactoring_guru.command.example.commands;
import refactoring_guru.command.example.editor.Editor;
public class CutCommand extends Command {
public CutCommand(Editor editor) {
super(editor);
}
@Override
public boolean execute() {
if (editor.textField.getSelectedText().isEmpty()) return false;
backup();
String source = editor.textField.getText();
editor.clipboard = editor.textField.getSelectedText();
editor.textField.setText(cutString(source));
return true;
}
private String cutString(String source) {
String start = source.substring(0, editor.textField.getSelectionStart());
String end = source.substring(editor.textField.getSelectionEnd());
return start + end;
}
}
commands/CommandHistory.java: سجل الأوامر
package refactoring_guru.command.example.commands;
import java.util.Stack;
public class CommandHistory {
private Stack<Command> history = new Stack<>();
public void push(Command c) {
history.push(c);
}
public Command pop() {
return history.pop();
}
public boolean isEmpty() { return history.isEmpty(); }
}
المحرر
editor/Editor.java: الواجهة الرسومية للمحرر النصي
package refactoring_guru.command.example.editor;
import refactoring_guru.command.example.commands.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class Editor {
public JTextArea textField;
public String clipboard;
private CommandHistory history = new CommandHistory();
public void init() {
JFrame frame = new JFrame("Text editor (type & use buttons, Luke!)");
JPanel content = new JPanel();
frame.setContentPane(content);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
textField = new JTextArea();
textField.setLineWrap(true);
content.add(textField);
JPanel buttons = new JPanel(new FlowLayout(FlowLayout.CENTER));
JButton ctrlC = new JButton("Ctrl+C");
JButton ctrlX = new JButton("Ctrl+X");
JButton ctrlV = new JButton("Ctrl+V");
JButton ctrlZ = new JButton("Ctrl+Z");
Editor editor = this;
ctrlC.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
executeCommand(new CopyCommand(editor));
}
});
ctrlX.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
executeCommand(new CutCommand(editor));
}
});
ctrlV.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
executeCommand(new PasteCommand(editor));
}
});
ctrlZ.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
undo();
}
});
buttons.add(ctrlC);
buttons.add(ctrlX);
buttons.add(ctrlV);
buttons.add(ctrlZ);
content.add(buttons);
frame.setSize(450, 200);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
private void executeCommand(Command command) {
if (command.execute()) {
history.push(command);
}
}
private void undo() {
if (history.isEmpty()) return;
Command command = history.pop();
if (command != null) {
command.undo();
}
}
}
Demo.java: شيفرة العميل
package refactoring_guru.command.example;
import refactoring_guru.command.example.editor.Editor;
public class Demo {
public static void main(String[] args) {
Editor editor = new Editor();
editor.init();
}
}
OutputDemo.png: نتائج التنفيذ
الصورة.
الاستخدام في لغة #C
المستوى: ★ ☆ ☆
الانتشار: ★ ★ ★
أمثلة الاستخدام: يكثر استخدام نمط الأمر في لغة #C، ويستخدم عادة كبديل للاستدعاءات إلى إدخال إجراءاتٍ (Actions) كمعامِلات (Parameters) إلى عناصر الواجهة الرسومية. كما تستخدم أيضًا لصف المهام (Queuing Tasks) وتتبع سجل العمليات، إلخ.
يمكن ملاحظة نمط الأمر من خلال الأساليب السلوكية في نوعٍ مجردٍ أو واجهةٍ (المرسل) الذي يستدعي أسلوبًا في تطبيق لنوعٍ مجردٍ أو واجهةٍ مختلف (المستقبِل) الذي اختُزِل بواسطة تطبيق الأمر أثناء إنشائه، وتكون فئات الأمر عادة محدودة بإجراءات محددة.
مثال تصوري
يوضح هذا المثال بنية نمط الأمر (Command)، ويركز على إجابة الأسئلة التالية:
- ما الفئات التي يتكون منها؟
- ما الأدوار التي تلعبها هذه الفئات؟
- كيف ترتبط عناصر النمط ببعضها؟
Program.cs: مثال تصوري
using System;
namespace RefactoringGuru.DesignPatterns.Command.Conceptual
{
// تصرح واجهة الأمر عن أسلوب لتنفيذ أحد الأوامر.
public interface ICommand
{
void Execute();
}
// تستطيع بعض الأوامر إجراء عمليات بأنفسها.
class SimpleCommand : ICommand
{
private string _payload = string.Empty;
public SimpleCommand(string payload)
{
this._payload = payload;
}
public void Execute()
{
Console.WriteLine($"SimpleCommand: See, I can do simple things like printing ({this._payload})");
}
}
// (receivers) لكن بعض الأوامر تفوض العمليات المعقدة إلى كائنات أخرى تسمى المستقبِلات.
class ComplexCommand : ICommand
{
private Receiver _receiver;
// البيانات السياقية مطلوبة لتشغيل أساليب المستقبِل.
private string _a;
private string _b;
// تقبل الأوامر المعقدة كائن مستقبِل واحد أو أكثر إضافة إلى أي بيانات سياقية
// (Constructor) من خلال المنشئ.
public ComplexCommand(Receiver receiver, string a, string b)
{
this._receiver = receiver;
this._a = a;
this._b = b;
}
// تستطيع الأوامر أن تفوض إلى أي أسلوب من أساليب المستقبِل.
public void Execute()
{
Console.WriteLine("ComplexCommand: Complex stuff should be done by a receiver object.");
this._receiver.DoSomething(this._a);
this._receiver.DoSomethingElse(this._b);
}
}
// على بعض منطق العمل المهم، وتعرف كيف (Receiver) تحتوي فئات المستقبِل
// تنفذ كل أنواع العمليات المرتبطة بتنفيذ الطلب.
// يمكن لأي فئة أن تعمل كمستقبِل.
class Receiver
{
public void DoSomething(string a)
{
Console.WriteLine($"Receiver: Working on ({a}.)");
}
public void DoSomethingElse(string b)
{
Console.WriteLine($"Receiver: Also working on ({b}.)");
}
}
// بأمر واحد أو أكثر، ويرسل الطلب إلى الأمر (Invoker) يرتبط المستدعي
class Invoker
{
private ICommand _onStart;
private ICommand _onFinish;
// شغِّل الأوامر.
public void SetOnStart(ICommand command)
{
this._onStart = command;
}
public void SetOnFinish(ICommand command)
{
this._onFinish = command;
}
// لا يعتمد المستدعي على فئات الأمر الحقيقي أو على المستقبِل، بل يمرر المستدعي
// الطلبَ إلى المستقبِل بطريقة غير مباشرة عبر تنفيذ أحد الأوامر.
public void DoSomethingImportant()
{
Console.WriteLine("Invoker: Does anybody want something done before I begin?");
if (this._onStart is ICommand)
{
this._onStart.Execute();
}
Console.WriteLine("Invoker: ...doing something really important...");
Console.WriteLine("Invoker: Does anybody want something done after I finish?");
if (this._onFinish is ICommand)
{
this._onFinish.Execute();
}
}
}
class Program
{
static void Main(string[] args)
{
// (Invoker) تستطيع شيفرة العميل أن تدخل أي أمر كمعامِل إلى المستدعي.
Invoker invoker = new Invoker();
invoker.SetOnStart(new SimpleCommand("Say Hi!"));
Receiver receiver = new Receiver();
invoker.SetOnFinish(new ComplexCommand(receiver, "Send email", "Save report"));
invoker.DoSomethingImportant();
}
}
}
Output.txt: نتائج التنفيذ
Invoker: Does anybody want something done before I begin?
SimpleCommand: See, I can do simple things like printing (Say Hi!)
Invoker: ...doing something really important...
Invoker: Does anybody want something done after I finish?
ComplexCommand: Complex stuff should be done by a receiver object.
Receiver: Working on (Send email.)
Receiver: Also working on (Save report.)
الاستخدام في لغة PHP
المستوى: ★ ☆ ☆
الانتشار: ★ ★ ★
أمثلة الاستخدام: يكثر استخدام نمط الأمر في لغة PHP، ويستخدم عادة كبديل للاستدعاءات إلى إدخال إجراءاتٍ (Actions) كمعامِلات (Parameters) إلى عناصر الواجهة الرسومية. كما تستخدم أيضًا لصف المهام (Queuing Tasks) وتتبع سجل العمليات المُنفَّذة وإجراء عمليات التراجع (undo).
مثال تصوري
يوضح هذا المثال بنية نمط الأمر (Command)، ويركز على إجابة الأسئلة التالية:
- ما الفئات التي يتكون منها؟
- ما الأدوار التي تلعبها هذه الفئات؟
- كيف ترتبط عناصر النمط ببعضها؟
سيكون من السهل عليك استيعاب المثال التالي بعد تعلم بنية النمط، بناء على استخدام واقعي له في لغة PHP.
index.php: مثال تصوري
<?php
namespace RefactoringGuru\Command\Conceptual;
/**
* تصرح واجهة الأمر عن أسلوب لتنفيذ أحد الأوامر.
*/
interface Command
{
public function execute(): void;
}
/**
* تستطيع بعض الأوامر إجراء عمليات بأنفسها.
*/
class SimpleCommand implements Command
{
private $payload;
public function __construct(string $payload)
{
$this->payload = $payload;
}
public function execute(): void
{
echo "SimpleCommand: See, I can do simple things like printing (" . $this->payload . ")\n";
}
}
/**
* (receivers) لكن بعض الأوامر تفوض العمليات المعقدة إلى كائنات أخرى تسمى المستقبِلات.
*/
class ComplexCommand implements Command
{
/**
* @var Receiver
*/
private $receiver;
/**
* البيانات السياقية مطلوبة لتشغيل أساليب المستقبِل.
*/
private $a;
private $b;
/**
* تقبل الأوامر المعقدة كائن مستقبِل واحد أو أكثر إضافة إلى أي بيانات سياقية
* (Constructor) من خلال المنشئ.
*/
public function __construct(Receiver $receiver, string $a, string $b)
{
$this->receiver = $receiver;
$this->a = $a;
$this->b = $b;
}
/**
* تستطيع الأوامر أن تفوض إلى أي أسلوب من أساليب المستقبِل.
*/
public function execute(): void
{
echo "ComplexCommand: Complex stuff should be done by a receiver object.\n";
$this->receiver->doSomething($this->a);
$this->receiver->doSomethingElse($this->b);
}
}
/**
* على بعض منطق العمل المهم، وتعرف كيف (Receiver) تحتوي فئات المستقبِل
* تنفذ كل أنواع العمليات المرتبطة بتنفيذ الطلب.
* يمكن لأي فئة أن تعمل كمستقبِل.
*/
class Receiver
{
public function doSomething(string $a): void
{
echo "Receiver: Working on (" . $a . ".)\n";
}
public function doSomethingElse(string $b): void
{
echo "Receiver: Also working on (" . $b . ".)\n";
}
}
/**
* بأمر واحد أو أكثر، ويرسل الطلب إلى الأمر (Invoker) يرتبط المستدعي
*/
class Invoker
{
/**
* @var Command
*/
private $onStart;
/**
* @var Command
*/
private $onFinish;
/**
* شغِّل الأوامر.
*/
public function setOnStart(Command $command): void
{
$this->onStart = $command;
}
public function setOnFinish(Command $command): void
{
$this->onFinish = $command;
}
/**
* لا يعتمد المستدعي على فئات الأمر الحقيقي أو على المستقبِل، بل يمرر المستدعي
* الطلبَ إلى المستقبِل بطريقة غير مباشرة عبر تنفيذ أحد الأوامر.
*/
public function doSomethingImportant(): void
{
echo "Invoker: Does anybody want something done before I begin?\n";
if ($this->onStart instanceof Command) {
$this->onStart->execute();
}
echo "Invoker: ...doing something really important...\n";
echo "Invoker: Does anybody want something done after I finish?\n";
if ($this->onFinish instanceof Command) {
$this->onFinish->execute();
}
}
}
/**
* (Invoker) تستطيع شيفرة العميل أن تدخل أي أمر كمعامِل إلى المستدعي.
*/
$invoker = new Invoker;
$invoker->setOnStart(new SimpleCommand("Say Hi!"));
$receiver = new Receiver;
$invoker->setOnFinish(new ComplexCommand($receiver, "Send email", "Save report"));
$invoker->doSomethingImportant();
Output.txt: نتائج التنفيذ
Invoker: Does anybody want something done before I begin?
SimpleCommand: See, I can do simple things like printing (Say Hi!)
Invoker: ...doing something really important...
Invoker: Does anybody want something done after I finish?
ComplexCommand: Complex stuff should be done by a receiver object.
Receiver: Working on (Send email.)
Receiver: Also working on (Save report.)
مثال واقعي
يُستخدم نمط الأمر Command في هذا المثال لصفّ استدعاءات الاستخلاص (Web scraping calls) إلى موقع IMDB ومن ثم تنفيذها واحدًا تلو الآخر، ويُحفظ الصف نفسه في قاعدة بيانات تساهم في الاحتفاظ بالأوامر بين إطلاقات الشيفرة النصية (Script Launches).
index.php: مثال واقعي
<?php
namespace RefactoringGuru\Command\RealWorld;
/**
* تصرح واجهة الأمر عن أسلوب التنفيذ الأساسي إضافة إلى عدة أساليب مساعدة لاسترجاع
* البيانات الوصفية للأمر.
*/
interface Command
{
public function execute(): void;
public function getId(): int;
public function getStatus(): int;
}
/**
* البنيةَ التحتيةَ الأساسية للتحميل، والمشتركة (Web Scraping) يعرِّف أمرُ استخلاص الويب الأساسي
* بين كل أوامر استخلاص الويب الحقيقية.
*/
abstract class WebScrapingCommand implements Command
{
public $id;
public $status = 0;
/**
* @var الرابط النصي للاستخلاص.
*/
public $url;
public function __construct(string $url)
{
$this->url = $url;
}
public function getId(): int
{
return $this->id;
}
public function getStatus(): int
{
return $this->status;
}
public function getURL(): string
{
return $this->url;
}
/**
* بما أن أساليب التنفيذ لكل أوامر استخلاص الويب متشابهة، فيمكننا توفير
* استخدام افتراضي ونعطي للفئات الفرعية صلاحية تخطيها إن دعت الحاجة.
*
* قد تلاحظ نمطًا سلوكيًا آخر هنا إن دققت النظر!
*/
public function execute(): void
{
$html = $this->download();
$this->parse($html);
$this->complete();
}
public function download(): string
{
$html = file_get_contents($this->getURL());
echo "WebScrapingCommand: Downloaded {$this->url}\n";
return $html;
}
abstract public function parse(string $html): void;
public function complete(): void
{
$this->status = 1;
Queue::get()->completeCommand($this);
}
}
/**
* الأمر الحقيقي لاستخلاص قائمة من تصانيف الأفلام.
*/
class IMDBGenresScrapingCommand extends WebScrapingCommand
{
public function __construct()
{
$this->url = "https://www.imdb.com/feature/genre/";
}
/**
* استخرج كل التصانيف وروابط بحثها من الصفحة التالية:
* https://www.imdb.com/feature/genre/
*/
public function parse($html): void
{
preg_match_all("|href=\"(https://www.imdb.com/search/title\?genres=.*?)\"|", $html, $matches);
echo "IMDBGenresScrapingCommand: Discovered " . count($matches[1]) . " genres.\n";
foreach ($matches[1] as $genre) {
Queue::get()->add(new IMDBGenrePageScrapingCommand($genre));
}
}
}
/**
* الأمر الحقيقي لاستخلاص قائمة من الأفلام في تصنيف معين.
*/
class IMDBGenrePageScrapingCommand extends WebScrapingCommand
{
private $page;
public function __construct(string $url, int $page = 1)
{
parent::__construct($url);
$this->page = $page;
}
public function getURL(): string
{
return $this->url . '?page=' . $this->page;
}
/**
* استخرج كل الأفلام من صفحة كهذه:
* https://www.imdb.com/search/title?genres=sci-fi&explore=title_type,genres
*/
public function parse(string $html): void
{
preg_match_all("|href=\"(/title/.*?/)\?ref_=adv_li_tt\"|", $html, $matches);
echo "IMDBGenrePageScrapingCommand: Discovered " . count($matches[1]) . " movies.\n";
foreach ($matches[1] as $moviePath) {
$url = "https://www.imdb.com" . $moviePath;
Queue::get()->add(new IMDBMovieScrapingCommand($url));
}
// رابط الصفحة التالية (Parse) حلل..
if (preg_match("|Next »</a>|", $html)) {
Queue::get()->add(new IMDBGenrePageScrapingCommand($this->url, $this->page + 1));
}
}
}
/**
* الأمر الحقيقي لاستخلاص تفاصيل الفيلم.
*/
class IMDBMovieScrapingCommand extends WebScrapingCommand
{
/**
* احصل على معلومات الفيلم من صفحة كهذه:
* https://www.imdb.com/title/tt4154756/
*/
public function parse(string $html): void
{
if (preg_match("|<h1 itemprop=\"name\" class=\"\">(.*?)</h1>|", $html, $matches)) {
$title = $matches[1];
}
echo "IMDBMovieScrapingCommand: Parsed movie $title.\n";
}
}
/**
* فتكدِّس كائنات الأمر وتنفذها واحدًا تلو الآخر ،(Invoker) كمستدعي Queue تتصرف فئة
* فإن أوُقِف تنفيذُ الشيفرة النصية فجأة فإن الصف وكل أوامره يمكن استرجاعهم جميعًا بسهولة.
* ولن تضطر إلى تكرار تنفيذ كل الأوامر التي سبق تنفيذها.
*
* الذي يخزن الأوامر في قاعدة (Command Queue) لاحظ أن هذا تطبيق بدائي جدًا لصف الأوامر
* محلية، فهناك عشرات الحلول المجدية المتاحة للاستخدام في التطبيقات الحقيقية SQLite بيانات.
*/
class Queue
{
private $db;
public function __construct()
{
$this->db = new \SQLite3(__DIR__ . '/commands.sqlite',
SQLITE3_OPEN_CREATE | SQLITE3_OPEN_READWRITE);
$this->db->query('CREATE TABLE IF NOT EXISTS "commands" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"command" TEXT,
"status" INTEGER
)');
}
public function isEmpty(): bool
{
$query = 'SELECT COUNT("id") FROM "commands" WHERE status = 0';
return $this->db->querySingle($query) === 0;
}
public function add(Command $command): void
{
$query = 'INSERT INTO commands (command, status) VALUES (:command, :status)';
$statement = $this->db->prepare($query);
$statement->bindValue(':command', base64_encode(serialize($command)));
$statement->bindValue(':status', $command->getStatus());
$statement->execute();
}
public function getCommand(): Command
{
$query = 'SELECT * FROM "commands" WHERE "status" = 0 LIMIT 1';
$record = $this->db->querySingle($query, true);
$command = unserialize(base64_decode($record["command"]));
$command->id = $record['id'];
return $command;
}
public function completeCommand(Command $command): void
{
$query = 'UPDATE commands SET status = :status WHERE id = :id';
$statement = $this->db->prepare($query);
$statement->bindValue(':status', $command->getStatus());
$statement->bindValue(':id', $command->getId());
$statement->execute();
}
public function work(): void
{
while (!$this->isEmpty()) {
$command = $this->getCommand();
$command->execute();
}
}
/**
* من باب تسهيل المثال (Singleton) سنجعل كائن الصفِّ هنا مفردةً.
*/
public static function get(): Queue
{
static $instance;
if (!$instance) {
$instance = new Queue;
}
return $instance;
}
}
/**
* شيفرة العميل.
*/
$queue = Queue::get();
if ($queue->isEmpty()) {
$queue->add(new IMDBGenresScrapingCommand);
}
$queue->work();
Output.txt: نتائج التنفيذ
WebScrapingCommand: Downloaded https://www.imdb.com/feature/genre/
IMDBGenresScrapingCommand: Discovered 14 genres.
WebScrapingCommand: Downloaded https://www.imdb.com/search/title?genres=comedy
IMDBGenrePageScrapingCommand: Discovered 50 movies.
WebScrapingCommand: Downloaded https://www.imdb.com/search/title?genres=sci-fi
IMDBGenrePageScrapingCommand: Discovered 50 movies.
WebScrapingCommand: Downloaded https://www.imdb.com/search/title?genres=horror
IMDBGenrePageScrapingCommand: Discovered 50 movies.
WebScrapingCommand: Downloaded https://www.imdb.com/search/title?genres=romance
IMDBGenrePageScrapingCommand: Discovered 50 movies.
...
الاستخدام في لغة بايثون
المستوى: ★ ☆ ☆
الانتشار: ★ ★ ★
أمثلة الاستخدام: يكثر استخدام نمط الأمر في لغة بايثون، ويستخدم عادة كبديل للاستدعاءات إلى إدخال إجراءاتٍ (Actions) كمعامِلات (Parameters) إلى عناصر الواجهة الرسومية. كما تستخدم أيضًا لصف المهام (Queuing Tasks) وتتبع سجل العمليات، إلخ.
يمكن ملاحظة نمط الأمر من خلال الأساليب السلوكية في نوعٍ مجردٍ أو واجهةٍ (المرسل) الذي يستدعي أسلوبًا في تطبيق لنوعٍ مجردٍ أو واجهةٍ مختلف (المستقبِل) الذي اختُزِل بواسطة تطبيق الأمر أثناء إنشائه، وتكون فئات الأمر عادة محدودة بإجراءات محددة.
مثال تصوري
يوضح هذا المثال بنية نمط الأمر (Command)، ويركز على إجابة الأسئلة التالية:
- ما الفئات التي يتكون منها؟
- ما الأدوار التي تلعبها هذه الفئات؟
- كيف ترتبط عناصر النمط ببعضها؟
main.py: مثال تصوري
from __future__ import annotations
from abc import ABC, abstractmethod
class Command(ABC):
"""
تصرح واجهة الأمر عن أسلوب لتنفيذ أحد الأوامر.
"""
@abstractmethod
def execute(self) -> None:
pass
class SimpleCommand(Command):
"""
تستطيع بعض الأوامر إجراء عمليات بأنفسها.
"""
def __init__(self, payload: str) -> None:
self._payload = payload
def execute(self) -> None:
print(f"SimpleCommand: See, I can do simple things like printing"
f"({self._payload})")
class ComplexCommand(Command):
"""
(receivers) لكن بعض الأوامر تفوض العمليات المعقدة إلى كائنات أخرى تسمى المستقبِلات.
"""
def __init__(self, receiver: Receiver, a: str, b: str) -> None:
"""
تقبل الأوامر المعقدة كائن مستقبِل واحد أو أكثر إضافة إلى أي
(Constructor) بيانات سياقية من خلال المنشئ.
"""
self._receiver = receiver
self._a = a
self._b = b
def execute(self) -> None:
"""
تستطيع الأوامر أن تفوض إلى أي أسلوب من أساليب المستقبِل.
"""
print("ComplexCommand: Complex stuff should be done by a receiver object", end="")
self._receiver.do_something(self._a)
self._receiver.do_something_else(self._b)
class Receiver:
"""
على بعض منطق العمل المهم، وتعرف كيف (Receiver) تحتوي فئات المستقبِل
تنفذ كل أنواع العمليات المرتبطة بتنفيذ الطلب. بل يمكن لأي فئة أن تعمل كمستقبِل.
"""
def do_something(self, a: str) -> None:
print(f"\nReceiver: Working on ({a}.)", end="")
def do_something_else(self, b: str) -> None:
print(f"\nReceiver: Also working on ({b}.)", end="")
class Invoker:
"""
بأمر واحد أو أكثر، ويرسل الطلب إلى الأمر (Invoker) يرتبط المستدعي.
"""
_on_start = None
_on_finish = None
"""
شغِّل الأوامر.
"""
def set_on_start(self, command: Command):
self._on_start = command
def set_on_finish(self, command: Command):
self._on_finish = command
def do_something_important(self) -> None:
"""
لا يعتمد المستدعي على فئات الأمر الحقيقي أو على المستقبِل، بل يمرر المستدعي الطلبَ
إلى المستقبِل بطريقة غير مباشرة عبر تنفيذ أحد الأوامر.
"""
print("Invoker: Does anybody want something done before I begin?")
if isinstance(self._on_start, Command):
self._on_start.execute()
print("Invoker: ...doing something really important...")
print("Invoker: Does anybody want something done after I finish?")
if isinstance(self._on_finish, Command):
self._on_finish.execute()
if __name__ == "__main__":
"""
(Invoker) تستطيع شيفرة العميل أن تدخل أي أمر كمعامِل إلى المستدعي.
"""
invoker = Invoker()
invoker.set_on_start(SimpleCommand("Say Hi!"))
receiver = Receiver()
invoker.set_on_finish(ComplexCommand(
receiver, "Send email", "Save report"))
invoker.do_something_important()
Output.txt: نتائج التنفيذ
Invoker: Does anybody want something done before I begin?
SimpleCommand: See, I can do simple things like printing (Say Hi!)
Invoker: ...doing something really important...
Invoker: Does anybody want something done after I finish?
ComplexCommand: Complex stuff should be done by a receiver object
Receiver: Working on (Send email.)
Receiver: Also working on (Save report.)
الاستخدام في لغة روبي
المستوى: ★ ☆ ☆
الانتشار: ★ ★ ★
أمثلة الاستخدام: يكثر استخدام نمط الأمر في لغة روبي، ويستخدم عادة كبديل للاستدعاءات إلى إدخال إجراءاتٍ (Actions) كمعامِلات (Parameters) إلى عناصر الواجهة الرسومية. كما تستخدم أيضًا لصف المهام (Queuing Tasks) وتتبع سجل العمليات، إلخ.
يمكن ملاحظة نمط الأمر من خلال الأساليب السلوكية في نوعٍ مجردٍ أو واجهةٍ (المرسل) الذي يستدعي أسلوبًا في تطبيق لنوعٍ مجردٍ أو واجهةٍ مختلف (المستقبِل) الذي اختُزِل بواسطة تطبيق الأمر أثناء إنشائه، وتكون فئات الأمر عادة محدودة بإجراءات محددة.
مثال تصوري
يوضح هذا المثال بنية نمط الأمر (Command)، ويركز على إجابة الأسئلة التالية:
- ما الفئات التي يتكون منها؟
- ما الأدوار التي تلعبها هذه الفئات؟
- كيف ترتبط عناصر النمط ببعضها؟
main.rb: مثال تصوري
# تصرح واجهة الأمر عن أسلوب لتنفيذ أحد الأوامر.
class Command
# @abstract
def execute
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
end
# تستطيع بعض الأوامر إجراء عمليات بأنفسها.
class SimpleCommand < Command
# @param [String] payload
def initialize(payload)
@payload = payload
end
def execute
puts "SimpleCommand: See, I can do simple things like printing (#{@payload})"
end
end
# (receivers) لكن بعض الأوامر تفوض العمليات المعقدة إلى كائنات أخرى تسمى المستقبِلات.
class ComplexCommand < Command
# تقبل الأوامر المعقدة كائن مستقبِل واحد أو أكثر إضافة إلى أي بيانات سياقية
# (Constructor) من خلال المنشئ.
def initialize(receiver, a, b)
@receiver = receiver
@a = a
@b = b
end
# تستطيع الأوامر أن تفوض إلى أي أسلوب من أساليب المستقبِل.
def execute
print 'ComplexCommand: Complex stuff should be done by a receiver object'
@receiver.do_something(@a)
@receiver.do_something_else(@b)
end
end
# على بعض منطق العمل المهم، وتعرف كيف (Receiver) تحتوي فئات المستقبِل
# تنفذ كل أنواع العمليات المرتبطة بتنفيذ الطلب، بل يمكن لأي فئة أن تعمل كمستقبِل.
class Receiver
# @param [String] a
def do_something(a)
print "\nReceiver: Working on (#{a}.)"
end
# @param [String] b
def do_something_else(b)
print "\nReceiver: Also working on (#{b}.)"
end
end
# The Invoker is associated with one or several commands. It sends a request to
# the command.
class Invoker
# Initialize commands.
# @param [Command] command
def on_start=(command)
@on_start = command
end
# @param [Command] command
def on_finish=(command)
@on_finish = command
end
# لا يعتمد المستدعي على فئات الأمر الحقيقي أو على المستقبِل، بل يمرر المستدعي الطلبَ إلى
# المستقبِل بطريقة غير مباشرة عبر تنفيذ أحد الأوامر.
def do_something_important
puts 'Invoker: Does anybody want something done before I begin?'
@on_start.execute if @on_start.is_a? Command
puts 'Invoker: ...doing something really important...'
puts 'Invoker: Does anybody want something done after I finish?'
@on_finish.execute if @on_finish.is_a? Command
end
end
# (Invoker) تستطيع شيفرة العميل أن تدخل أي أمر كمعامِل إلى المستدعي.
invoker = Invoker.new
invoker.on_start = SimpleCommand.new('Say Hi!')
receiver = Receiver.new
invoker.on_finish = ComplexCommand.new(receiver, 'Send email', 'Save report')
invoker.do_something_important
output.txt: نتائج التنفيذ
Invoker: Does anybody want something done before I begin?
SimpleCommand: See, I can do simple things like printing (Say Hi!)
Invoker: ...doing something really important...
Invoker: Does anybody want something done after I finish?
ComplexCommand: Complex stuff should be done by a receiver object
Receiver: Working on (Send email.)
Receiver: Also working on (Save report.)