الدليل الشامل لأنماط تصميم JavaScript
نشرت: 2022-03-11بصفتك مطور جافا سكريبت جيد ، فأنت تسعى جاهدة لكتابة كود نظيف وصحي وقابل للصيانة. أنت تحل تحديات مثيرة للاهتمام ، رغم كونها فريدة من نوعها ، إلا أنها لا تتطلب بالضرورة حلولًا فريدة. من المحتمل أنك وجدت نفسك تكتب رمزًا مشابهًا لحل مشكلة مختلفة تمامًا تعاملت معها من قبل. قد لا تعرف ذلك ، لكنك استخدمت نمط تصميم JavaScript. أنماط التصميم هي حلول قابلة لإعادة الاستخدام للمشكلات الشائعة في تصميم البرامج.
خلال عمر أي لغة ، يتم صنع واختبار العديد من هذه الحلول القابلة لإعادة الاستخدام من قبل عدد كبير من المطورين من مجتمع تلك اللغة. وبسبب هذه التجربة المشتركة للعديد من المطورين ، فإن هذه الحلول مفيدة جدًا لأنها تساعدنا في كتابة التعليمات البرمجية بطريقة محسّنة مع حل المشكلة المطروحة في نفس الوقت.
الفوائد الرئيسية التي نحصل عليها من أنماط التصميم هي كما يلي:
- إنها حلول مجربة: نظرًا لأن أنماط التصميم غالبًا ما يستخدمها العديد من المطورين ، يمكنك التأكد من أنها تعمل. وليس هذا فقط ، يمكنك أن تكون على يقين من أنه تمت مراجعتها عدة مرات وربما تم تنفيذ التحسينات.
- يمكن إعادة استخدامها بسهولة: توثق أنماط التصميم حلاً قابلاً لإعادة الاستخدام يمكن تعديله لحل مشكلات معينة متعددة ، حيث إنها غير مرتبطة بمشكلة معينة.
- إنها معبرة: يمكن لأنماط التصميم أن تشرح الحل الكبير بأناقة تامة.
- إنها تسهل الاتصال: عندما يكون المطورون على دراية بأنماط التصميم ، يمكنهم التواصل بسهولة أكبر مع بعضهم البعض حول الحلول المحتملة لمشكلة معينة.
- إنها تمنع الحاجة إلى إعادة بناء التعليمات البرمجية: إذا تمت كتابة تطبيق مع وضع أنماط التصميم في الاعتبار ، فغالبًا ما لا تحتاج إلى إعادة بناء الكود لاحقًا لأن تطبيق نمط التصميم الصحيح على مشكلة معينة يعد بالفعل أمثل المحلول.
- إنها تقلل من حجم قاعدة الكود: نظرًا لأن أنماط التصميم عادةً ما تكون حلولًا أنيقة ومثالية ، فإنها تتطلب عادةً رمزًا أقل من الحلول الأخرى.
أعلم أنك جاهز للانطلاق في هذه المرحلة ، ولكن قبل أن تتعلم كل شيء عن أنماط التصميم ، دعنا نراجع بعض أساسيات JavaScript.
تاريخ موجز لجافا سكريبت
تعد JavaScript واحدة من أكثر لغات البرمجة شيوعًا لتطوير الويب اليوم. تم إنشاؤه في البداية كنوع من "الغراء" للعديد من عناصر HTML المعروضة ، والمعروفة باسم لغة البرمجة النصية من جانب العميل ، لأحد متصفحات الويب الأولية. يسمى Netscape Navigator ، وكان بإمكانه فقط عرض HTML الثابت في ذلك الوقت. كما قد تفترض ، أدت فكرة لغة البرمجة النصية هذه إلى حروب المتصفح بين اللاعبين الكبار في صناعة تطوير المتصفحات في ذلك الوقت ، مثل Netscape Communications (حاليًا Mozilla) و Microsoft وغيرها.
أراد كل من اللاعبين الكبار المضي قدمًا في تطبيقهم الخاص للغة البرمجة النصية هذه ، لذلك قام Netscape بصنع JavaScript (في الواقع ، كما فعل Brendan Eich) ، وصنعت Microsoft JScript ، وما إلى ذلك. كما يمكنك أن تتخيل ، كانت الاختلافات بين هذه التطبيقات كبيرة ، لذلك تم تطوير متصفحات الويب لكل متصفح ، مع أفضل الملصقات التي يتم عرضها والتي تأتي مع صفحة الويب. سرعان ما أصبح واضحًا أننا بحاجة إلى معيار ، وهو حل متعدد المستعرضات من شأنه توحيد عملية التطوير وتبسيط إنشاء صفحات الويب. ما توصلوا إليه يسمى ECMAScript.
ECMAScript هو أحد مواصفات لغة البرمجة النصية القياسية التي تحاول جميع المتصفحات الحديثة دعمها ، وهناك تطبيقات متعددة (يمكنك قول اللهجات) لـ ECMAScript. الأكثر شيوعًا هو موضوع هذه المقالة ، JavaScript. منذ إصداره الأولي ، قام ECMAScript بتوحيد الكثير من الأشياء المهمة ، وبالنسبة لأولئك المهتمين أكثر بالتفاصيل ، هناك قائمة مفصلة بالعناصر الموحدة لكل إصدار من ECMAScript متاح على ويكيبيديا. لا يزال دعم المستعرض لإصدارات ECMAScript 6 (ES6) والإصدارات الأحدث غير مكتمل ويجب تحويله إلى ES5 ليتم دعمه بالكامل.
ما هو جافا سكريبت؟
لفهم محتويات هذه المقالة تمامًا ، دعنا نقدم مقدمة لبعض خصائص اللغة المهمة جدًا التي نحتاج إلى معرفتها قبل الغوص في أنماط تصميم JavaScript. إذا سألك شخص ما "ما هي JavaScript؟" قد تجيب في مكان ما في سطور:
JavaScript هي لغة برمجة خفيفة الوزن ومفسرة وموجهة للكائنات مع وظائف من الدرجة الأولى تُعرف باسم لغة البرمجة النصية لصفحات الويب.
يعني التعريف المذكور أعلاه أن شفرة JavaScript لها مساحة ذاكرة منخفضة ، وسهلة التنفيذ ، وسهلة التعلم ، مع بناء جملة مشابه للغات الشائعة مثل C ++ و Java. إنها لغة برمجة نصية ، مما يعني أن كودها يتم تفسيره بدلاً من ترجمته. يدعم أنماط البرمجة الإجرائية والموجهة للكائنات والوظيفية ، مما يجعلها مرنة جدًا للمطورين.
حتى الآن ، ألقينا نظرة على جميع الخصائص التي تبدو مثل العديد من اللغات الأخرى ، لذلك دعونا نلقي نظرة على ما هو محدد حول JavaScript فيما يتعلق باللغات الأخرى. سأقوم بإدراج بعض الخصائص وإعطاء أفضل ما لدي في شرح لماذا تستحق اهتماما خاصا.
يدعم JavaScript وظائف من الدرجة الأولى
كانت هذه الخاصية مزعجة بالنسبة لي لفهمها عندما بدأت للتو في استخدام JavaScript ، حيث أتيت من خلفية C / C ++. يعامل JavaScript الوظائف كمواطنين من الدرجة الأولى ، مما يعني أنه يمكنك تمرير الوظائف كمعلمات إلى وظائف أخرى تمامًا كما تفعل مع أي متغير آخر.
// we send in the function as an argument to be // executed from inside the calling function function performOperation(a, b, cb) { var c = a + b; cb(c); } performOperation(2, 3, function(result) { // prints out 5 console.log("The result of the operation is " + result); })
جافا سكريبت تعتمد على النموذج الأولي
كما هو الحال مع العديد من اللغات الأخرى الموجهة للكائنات ، تدعم JavaScript الكائنات ، وأحد المصطلحات الأولى التي تتبادر إلى الذهن عند التفكير في الكائنات هي الفئات والوراثة. هذا هو المكان الذي يصبح فيه الأمر صعبًا بعض الشيء ، حيث لا تدعم اللغة الفئات في شكلها البسيط بل تستخدم شيئًا يسمى الميراث المستند إلى النموذج الأولي أو القائم على المثيل.
الآن ، في ES6 ، تم تقديم فئة المصطلح الرسمي ، مما يعني أن المتصفحات لا تزال لا تدعم هذا (إذا كنت تتذكر ، حتى وقت كتابة هذا التقرير ، آخر إصدار ECMAScript مدعوم بالكامل هو 5.1). من المهم أن نلاحظ ، مع ذلك ، أنه على الرغم من إدخال مصطلح "class" في JavaScript ، إلا أنه لا يزال يستخدم الوراثة القائمة على النموذج الأولي تحت الغطاء.
البرمجة القائمة على النموذج الأولي هي نمط من البرمجة الموجهة للكائنات حيث يتم إعادة استخدام السلوك (المعروف باسم الوراثة) عبر عملية إعادة استخدام الكائنات الموجودة عبر عمليات التفويض التي تعمل كنماذج أولية. سنغوص في مزيد من التفاصيل مع هذا بمجرد وصولنا إلى قسم أنماط التصميم في المقالة ، حيث يتم استخدام هذه الخاصية في الكثير من أنماط تصميم JavaScript.
حلقات أحداث JavaScript
إذا كان لديك خبرة في العمل مع JavaScript ، فأنت بالتأكيد على دراية بوظيفة رد الاتصال . بالنسبة لأولئك الذين ليسوا على دراية بالمصطلح ، فإن وظيفة رد الاتصال هي وظيفة يتم إرسالها كمعامل (تذكر أن JavaScript يعامل الوظائف كمواطنين من الدرجة الأولى) إلى وظيفة أخرى ويتم تنفيذها بعد اندلاع حدث. يُستخدم هذا عادةً للاشتراك في أحداث مثل النقر بالماوس أو الضغط على زر لوحة المفاتيح.
في كل مرة يتم فيها إطلاق حدث مرفق به مستمع (وإلا يتم فقد الحدث) ، يتم إرسال رسالة إلى قائمة انتظار من الرسائل التي تتم معالجتها بشكل متزامن ، بطريقة ما يرد أولاً يصرف أولاً ). وهذا ما يسمى حلقة الحدث .
كل رسالة في قائمة الانتظار لها وظيفة مرتبطة بها. بمجرد إلغاء ترتيب الرسالة ، يقوم وقت التشغيل بتنفيذ الوظيفة بالكامل قبل معالجة أي رسالة أخرى. وهذا يعني أنه إذا كانت الوظيفة تحتوي على استدعاءات وظيفية أخرى ، فسيتم إجراؤها جميعًا قبل معالجة رسالة جديدة من قائمة الانتظار. وهذا ما يسمى بالتشغيل حتى الإكمال.
while (queue.waitForMessage()) { queue.processNextMessage(); }
queue.waitForMessage()
رسائل جديدة بشكل متزامن. كل رسالة تتم معالجتها لها مكدس خاص بها وتتم معالجتها حتى يصبح المكدس فارغًا. بمجرد الانتهاء ، تتم معالجة رسالة جديدة من قائمة الانتظار ، إذا كان هناك واحد.
ربما تكون قد سمعت أيضًا أن JavaScript غير محظور ، مما يعني أنه عند إجراء عملية غير متزامنة ، يكون البرنامج قادرًا على معالجة أشياء أخرى ، مثل تلقي مدخلات المستخدم ، أثناء انتظار اكتمال العملية غير المتزامنة ، وليس حظر الرئيسي موضوع التنفيذ. هذه خاصية مفيدة جدًا لجافا سكريبت ويمكن كتابة مقال كامل حول هذا الموضوع فقط ؛ ومع ذلك ، فهو خارج نطاق هذه المقالة.
ما هي أنماط التصميم؟
كما قلت من قبل ، فإن أنماط التصميم هي حلول قابلة لإعادة الاستخدام للمشاكل التي تحدث بشكل شائع في تصميم البرامج. دعنا نلقي نظرة على بعض فئات أنماط التصميم.
أنماط بدائية
كيف يمكن للمرء أن يخلق نمطا؟ لنفترض أنك تعرفت على مشكلة شائعة الحدوث ، ولديك حلك الفريد لهذه المشكلة ، وهو غير معترف به وموثق عالميًا. يمكنك استخدام هذا الحل في كل مرة تواجه هذه المشكلة ، وتعتقد أنه قابل لإعادة الاستخدام وأن مجتمع المطورين يمكن أن يستفيد منه.
هل يتحول إلى نمط على الفور؟ لحسن الحظ ، لا. في كثير من الأحيان ، قد يكون لدى المرء ممارسات جيدة في كتابة التعليمات البرمجية ويخطئ ببساطة في شيء يبدو وكأنه نمط لشخص ما ، في حين أنه في الواقع ليس نمطًا.
كيف يمكنك أن تعرف متى يكون ما تعتقد أنك تعرفه هو في الواقع نمط تصميم؟
من خلال الحصول على آراء مطورين آخرين حول هذا الموضوع ، من خلال التعرف على عملية إنشاء النموذج نفسه ، ومن خلال التعرف جيدًا على الأنماط الموجودة. هناك مرحلة يجب أن يمر بها النمط قبل أن يصبح نمطًا كاملًا ، وهذا ما يسمى بالنمط الأولي.
النموذج الأولي هو نمط يجب أن يكون إذا اجتاز فترة معينة من الاختبار بواسطة مطورين وسيناريوهات مختلفة حيث يثبت النمط أنه مفيد ويعطي نتائج صحيحة. هناك قدر كبير جدًا من العمل والوثائق - معظمها خارج نطاق هذه المقالة - يجب القيام به من أجل إنشاء نمط كامل معترف به من قبل المجتمع.
مكافحة الأنماط
نظرًا لأن نمط التصميم يمثل ممارسة جيدة ، فإن النمط المضاد يمثل ممارسة سيئة.
مثال على النمط المضاد هو تعديل النموذج الأولي لفئة Object
. ترث جميع الكائنات في JavaScript تقريبًا من Object
(تذكر أن JavaScript يستخدم الميراث المستند إلى النموذج الأولي) لذا تخيل سيناريو قمت فيه بتغيير هذا النموذج الأولي. يمكن رؤية التغييرات التي تم إجراؤها على النموذج الأولي Object
في جميع الكائنات التي ترث من هذا النموذج الأولي - والتي ستكون معظم كائنات JavaScript . هذه كارثة في انتظار ان يحدث.
مثال آخر ، مشابه للمثال المذكور أعلاه ، هو تعديل الكائنات التي لا تملكها. مثال على ذلك هو تجاوز وظيفة من كائن مستخدم في العديد من السيناريوهات في جميع أنحاء التطبيق. إذا كنت تعمل مع فريق كبير ، تخيل الارتباك الذي قد يسببه ذلك ؛ ستواجه بسرعة اصطدامات التسمية والتطبيقات غير المتوافقة وكوابيس الصيانة.
على غرار كيف أنه من المفيد معرفة جميع الممارسات والحلول الجيدة ، من المهم أيضًا معرفة السيئ منها أيضًا. بهذه الطريقة ، يمكنك التعرف عليهم وتجنب ارتكاب الخطأ مقدمًا.
تصنيف نمط التصميم
يمكن تصنيف أنماط التصميم بعدة طرق ، ولكن الأكثر شيوعًا هو ما يلي:
- أنماط التصميم الإبداعي
- أنماط التصميم الإنشائي
- أنماط التصميم السلوكية
- أنماط تصميم التزامن
- أنماط التصميم المعماري
أنماط التصميم الإبداعي
تتعامل هذه الأنماط مع آليات إنشاء الكائنات التي تعمل على تحسين إنشاء الكائن مقارنة بالنهج الأساسي. قد يؤدي الشكل الأساسي لإنشاء الكائن إلى مشاكل في التصميم أو إلى تعقيد إضافي للتصميم. تحل أنماط التصميم الإبداعي هذه المشكلة عن طريق التحكم بطريقة ما في إنشاء الكائن. بعض أنماط التصميم الشائعة في هذه الفئة هي:
- طريقة المصنع
- مصنع مجردة
- باني
- النموذج المبدئي
- سينجلتون
أنماط التصميم الإنشائي
هذه الأنماط تتعامل مع علاقات الكائن. إنهم يضمنون أنه إذا تغير جزء واحد من النظام ، فلن يحتاج النظام بأكمله إلى التغيير معه. الأنماط الأكثر شيوعًا في هذه الفئة هي:
- مشترك كهربائي
- كوبري
- مركب
- مصمم
- مظهر زائف
- وزن الذبابة
- الوكيل
أنماط التصميم السلوكي
تتعرف هذه الأنواع من الأنماط على الكائنات المتباينة في النظام وتنفذها وتحسنها. إنها تساعد في ضمان أن الأجزاء المتباينة من النظام لديها معلومات متزامنة. الأمثلة الشائعة لهذه الأنماط هي:
- سلسلة المسؤولية
- أمر
- التكرار
- وسيط
- تذكار
- مراقب
- حالة
- إستراتيجية
- زائر
أنماط تصميم التزامن
تتعامل هذه الأنواع من أنماط التصميم مع نماذج البرمجة متعددة الخيوط. ومن أشهرها:
- كائن نشط
- التفاعل النووي
- المجدول
أنماط التصميم المعماري
أنماط التصميم التي تستخدم للأغراض المعمارية. ومن أشهرها:
- MVC (نموذج متحكم في الرؤية)
- MVP (نموذج-عرض-مقدم)
- MVVM (نموذج عرض نموذج)
في القسم التالي ، سنلقي نظرة فاحصة على بعض أنماط التصميم المذكورة أعلاه مع أمثلة مقدمة من أجل فهم أفضل.
أمثلة على أنماط التصميم
يمثل كل نمط من أنماط التصميم نوعًا معينًا من الحلول لنوع معين من المشكلات. لا توجد مجموعة عالمية من الأنماط هي الأفضل دائمًا. نحتاج إلى معرفة متى يكون نمط معين مفيدًا وما إذا كان سيوفر قيمة فعلية. بمجرد أن نكون على دراية بالأنماط والسيناريوهات الأنسب لها ، يمكننا بسهولة تحديد ما إذا كان نمط معين مناسبًا لمشكلة معينة أم لا.
تذكر أن تطبيق النمط الخاطئ على مشكلة معينة قد يؤدي إلى تأثيرات غير مرغوب فيها مثل تعقيد الكود غير الضروري ، أو زيادة غير ضرورية في الأداء ، أو حتى ظهور نمط مضاد جديد.
هذه كلها أشياء مهمة يجب مراعاتها عند التفكير في تطبيق نمط تصميم على الكود الخاص بنا. سنلقي نظرة على بعض أنماط التصميم التي وجدتها مفيدة شخصيًا وأعتقد أن كل مطور جافا سكريبت كبير يجب أن يكون على دراية بها.
نمط المُنشئ
عند التفكير في اللغات الكلاسيكية الموجهة للكائنات ، فإن المُنشئ هو وظيفة خاصة في فئة تقوم بتهيئة كائن بمجموعة معينة من القيم الافتراضية و / أو القيم المرسلة.
الطرق الشائعة لإنشاء كائنات في JavaScript هي الطرق الثلاث التالية:
// either of the following ways can be used to create a new object var instance = {}; // or var instance = Object.create(Object.prototype); // or var instance = new Object();
بعد إنشاء كائن ، توجد أربع طرق (منذ ES3) لإضافة خصائص إلى هذه الكائنات. هم كالتالي:
// supported since ES3 // the dot notation instance.key = "A key's value"; // the square brackets notation instance["key"] = "A key's value"; // supported since ES5 // setting a single property using Object.defineProperty Object.defineProperty(instance, "key", { value: "A key's value", writable: true, enumerable: true, configurable: true }); // setting multiple properties using Object.defineProperties Object.defineProperties(instance, { "firstKey": { value: "First key's value", writable: true }, "secondKey": { value: "Second key's value", writable: false } });
الطريقة الأكثر شيوعًا لإنشاء كائنات هي الأقواس المتعرجة ولإضافة الخصائص ، تدوين النقطة أو الأقواس المربعة. استخدمها أي شخص لديه أي خبرة في JavaScript.
ذكرنا سابقًا أن JavaScript لا يدعم الفئات الأصلية ، ولكنه يدعم المُنشئين من خلال استخدام كلمة رئيسية "جديدة" مسبوقة باستدعاء دالة. بهذه الطريقة ، يمكننا استخدام الدالة كمنشئ وتهيئة خصائصها بنفس الطريقة التي نستخدمها مع مُنشئ اللغة الكلاسيكي.
// we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; this.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();
ومع ذلك ، لا يزال هناك مجال للتحسين هنا. إذا كنت تتذكر ، فقد ذكرت سابقًا أن JavaScript يستخدم الوراثة القائمة على النموذج الأولي. تكمن مشكلة الطريقة السابقة في إعادة تعريف الطريقة writesCode
لكل حالة من حالات مُنشئ Person
. يمكننا تجنب ذلك عن طريق ضبط التابع في النموذج الأولي للوظيفة:
// we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; } // we extend the function's prototype Person.prototype.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();
الآن ، يمكن لكلتا مثيلي مُنشئ Person
الوصول إلى مثيل مشترك writesCode()
.

نمط الوحدة
فيما يتعلق بالخصائص المميزة ، فإن JavaScript لا يتوقف أبدًا عن الدهشة. شيء غريب آخر لـ JavaScript (على الأقل فيما يتعلق باللغات الموجهة للكائنات) هو أن JavaScript لا يدعم معدِّلات الوصول. في لغة OOP الكلاسيكية ، يحدد المستخدم فئة ويحدد حقوق الوصول لأعضائها. نظرًا لأن JavaScript في شكله العادي لا يدعم الفئات ولا معدِّلات الوصول ، فقد اكتشف مطورو JavaScript طريقة لتقليد هذا السلوك عند الحاجة.
قبل أن ندخل في تفاصيل نمط الوحدة ، دعنا نتحدث عن مفهوم الإغلاق. الإغلاق هو وظيفة لها حق الوصول إلى النطاق الأصلي ، حتى بعد إغلاق الوظيفة الأصلية. إنها تساعدنا على محاكاة سلوك معدِّلات الوصول من خلال تحديد النطاق. دعنا نظهر هذا عبر مثال:
// we used an immediately invoked function expression // to create a private variable, counter var counterIncrementer = (function() { var counter = 0; return function() { return ++counter; }; })(); // prints out 1 console.log(counterIncrementer()); // prints out 2 console.log(counterIncrementer()); // prints out 3 console.log(counterIncrementer());
كما ترى ، باستخدام IIFE ، قمنا بربط متغير العداد بوظيفة تم استدعاؤها وإغلاقها ولكن لا يزال من الممكن الوصول إليها بواسطة الوظيفة الفرعية التي تزيدها. نظرًا لأنه لا يمكننا الوصول إلى متغير العداد من خارج تعبير الوظيفة ، فقد جعلناه خاصًا من خلال معالجة النطاق.
باستخدام الإغلاق ، يمكننا إنشاء كائنات بأجزاء خاصة وعامة. تسمى هذه الوحدات النمطية وهي مفيدة جدًا عندما نريد إخفاء أجزاء معينة من كائن وكشف واجهة فقط لمستخدم الوحدة. دعنا نظهر هذا في مثال:
// through the use of a closure we expose an object // as a public API which manages the private objects array var collection = (function() { // private members var objects = []; // public members return { addObject: function(object) { objects.push(object); }, removeObject: function(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } }, getObjects: function() { return JSON.parse(JSON.stringify(objects)); } }; })(); collection.addObject("Bob"); collection.addObject("Alice"); collection.addObject("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(collection.getObjects()); collection.removeObject("Alice"); // prints ["Bob", "Franck"] console.log(collection.getObjects());
الشيء الأكثر فائدة الذي يقدمه هذا النمط هو الفصل الواضح بين الأجزاء الخاصة والعامة من كائن ، وهو مفهوم مشابه جدًا للمطورين القادمين من خلفية كلاسيكية موجهة للكائنات.
ومع ذلك ، ليس كل شيء على ما يرام. عندما ترغب في تغيير رؤية العضو ، فأنت بحاجة إلى تعديل الكود أينما استخدمت هذا العضو بسبب الطبيعة المختلفة للوصول إلى الأجزاء العامة والخاصة. أيضًا ، لا يمكن للأساليب المضافة إلى الكائن بعد إنشائها الوصول إلى الأعضاء الخاصين في الكائن.
كشف نمط الوحدة
هذا النمط هو تحسين تم إجراؤه على نمط الوحدة كما هو موضح أعلاه. يتمثل الاختلاف الرئيسي في أننا نكتب منطق الكائن بالكامل في النطاق الخاص للوحدة النمطية ثم نكشف ببساطة الأجزاء التي نريد أن تكون عامة عن طريق إعادة كائن مجهول. يمكننا أيضًا تغيير تسمية الأعضاء الخاصين عند تعيين الأعضاء الخاصين إلى الأعضاء العموميين المطابقين لهم.
// we write the entire object logic as private members and // expose an anonymous object which maps members we wish to reveal // to their corresponding public members var namesCollection = (function() { // private members var objects = []; function addObject(object) { objects.push(object); } function removeObject(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } } function getObjects() { return JSON.parse(JSON.stringify(objects)); } // public members return { addName: addObject, removeName: removeObject, getNames: getObjects }; })(); namesCollection.addName("Bob"); namesCollection.addName("Alice"); namesCollection.addName("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(namesCollection.getNames()); namesCollection.removeName("Alice"); // prints ["Bob", "Franck"] console.log(namesCollection.getNames());
نمط الوحدة الكاشفة هو واحد من ثلاث طرق على الأقل يمكننا من خلالها تنفيذ نمط الوحدة. تكمن الاختلافات بين نموذج الوحدة النمطية المتغيرة والمتغيرات الأخرى في نمط الوحدة في المقام الأول في كيفية الإشارة إلى أعضاء الجمهور. نتيجة لذلك ، فإن نمط الوحدة الكاشفة أسهل بكثير في الاستخدام والتعديل ؛ ومع ذلك ، قد يكون هشًا في بعض السيناريوهات ، مثل استخدام كائنات RMP كنماذج أولية في سلسلة الوراثة. المواقف الإشكالية هي كما يلي:
- إذا كانت لدينا وظيفة خاصة تشير إلى وظيفة عامة ، فلا يمكننا تجاوز الوظيفة العامة ، حيث ستستمر الوظيفة الخاصة في الإشارة إلى التنفيذ الخاص للوظيفة ، وبالتالي إدخال خطأ في نظامنا.
- إذا كان لدينا عضو عام يشير إلى متغير خاص ، وحاولنا تجاوز العضو العام من خارج الوحدة النمطية ، فستظل الوظائف الأخرى تشير إلى القيمة الخاصة للمتغير ، مما يؤدي إلى إدخال خطأ في نظامنا.
نمط سينجلتون
يتم استخدام النمط الفردي في السيناريوهات عندما نحتاج إلى مثيل واحد بالضبط من الفصل. على سبيل المثال ، نحتاج إلى كائن يحتوي على بعض الضبط لشيء ما. في هذه الحالات ، ليس من الضروري إنشاء كائن جديد كلما كان كائن التكوين مطلوبًا في مكان ما في النظام.
var singleton = (function() { // private singleton value which gets initialized only once var config; function initializeConfiguration(values){ this.randomNumber = Math.random(); values = values || {}; this.number = values.number || 5; this.size = values.size || 10; } // we export the centralized method for retrieving the singleton value return { getConfig: function(values) { // we initialize the singleton value only once if (config === undefined) { config = new initializeConfiguration(values); } // and return the same config value wherever it is asked for return config; } }; })(); var configObject = singleton.getConfig({ "size": 8 }); // prints number: 5, size: 8, randomNumber: someRandomDecimalValue console.log(configObject); var configObject1 = singleton.getConfig({ "number": 8 }); // prints number: 5, size: 8, randomNumber: same randomDecimalValue as in first config console.log(configObject1);
كما ترى في المثال ، فإن الرقم العشوائي الذي يتم إنشاؤه هو نفسه دائمًا ، بالإضافة إلى قيم التكوين المرسلة.
من المهم ملاحظة أن نقطة الوصول لاسترداد القيمة المفردة يجب أن تكون واحدة فقط ومعروفة جيدًا. الجانب السلبي لاستخدام هذا النمط هو أنه من الصعب اختباره.
نمط المراقب
يعد نمط المراقب أداة مفيدة للغاية عندما يكون لدينا سيناريو نحتاج فيه إلى تحسين الاتصال بين الأجزاء المتباينة من نظامنا بطريقة محسّنة. يعزز الاقتران الفضفاض بين الأشياء.
توجد إصدارات مختلفة من هذا النمط ، ولكن في أبسط أشكاله ، لدينا جزأين رئيسيين من النمط. الأول موضوع والثاني هو المراقبين.
يعالج الموضوع جميع العمليات المتعلقة بموضوع معين يشترك فيه المراقبون. تقوم هذه العمليات بإشراك مراقب لموضوع معين ، وإلغاء اشتراك مراقب من موضوع معين ، وإخطار المراقبين بموضوع معين عند نشر حدث ما.
ومع ذلك ، هناك نوع مختلف من هذا النمط يسمى نمط الناشر / المشترك ، والذي سأستخدمه كمثال في هذا القسم. يتمثل الاختلاف الرئيسي بين نمط المراقب الكلاسيكي ونمط الناشر / المشترك في أن الناشر / المشترك يروج لمزيد من الاقتران الفضفاض مقارنة بنمط المراقب.
في نمط المراقب ، يحمل الموضوع الإشارات إلى المراقبين المشتركين ويستدعي الأساليب مباشرة من الكائنات نفسها ، بينما في نمط الناشر / المشترك ، لدينا قنوات تعمل كجسر اتصال بين المشترك والناشر. يقوم الناشر بإطلاق حدث ويقوم ببساطة بتنفيذ وظيفة رد الاتصال المرسلة لهذا الحدث.
سأقوم بعرض مثال قصير لنمط الناشر / المشترك ، ولكن بالنسبة للمهتمين ، يمكن العثور بسهولة على مثال نمط المراقب الكلاسيكي عبر الإنترنت.
var publisherSubscriber = {}; // we send in a container object which will handle the subscriptions and publishings (function(container) { // the id represents a unique subscription id to a topic var id = 0; // we subscribe to a specific topic by sending in // a callback function to be executed on event firing container.subscribe = function(topic, f) { if (!(topic in container)) { container[topic] = []; } container[topic].push({ "id": ++id, "callback": f }); return id; } // each subscription has its own unique ID, which we use // to remove a subscriber from a certain topic container.unsubscribe = function(topic, id) { var subscribers = []; for (var subscriber of container[topic]) { if (subscriber.id !== id) { subscribers.push(subscriber); } } container[topic] = subscribers; } container.publish = function(topic, data) { for (var subscriber of container[topic]) { // when executing a callback, it is usually helpful to read // the documentation to know which arguments will be // passed to our callbacks by the object firing the event subscriber.callback(data); } } })(publisherSubscriber); var subscriptionID1 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Bob's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID2 = publisherSubscriber.subscribe("mouseHovered", function(data) { console.log("I am Bob's callback function for a hovered mouse event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID3 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Alice's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); // NOTE: after publishing an event with its data, all of the // subscribed callbacks will execute and will receive // a data object from the object firing the event // there are 3 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"}); // we unsubscribe from an event by removing the subscription ID publisherSubscriber.unsubscribe("mouseClicked", subscriptionID3); // there are 2 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"});
يعتبر نمط التصميم هذا مفيدًا في المواقف التي نحتاج فيها إلى إجراء عمليات متعددة على حدث واحد يتم إطلاقه. تخيل أن لديك سيناريو نحتاج فيه إلى إجراء مكالمات AJAX متعددة إلى خدمة خلفية ثم إجراء مكالمات AJAX أخرى بناءً على النتيجة. سيتعين عليك تداخل مكالمات AJAX مع بعضها البعض ، وربما الدخول في موقف يُعرف باسم جحيم رد الاتصال. يعد استخدام نمط الناشر / المشترك حلاً أكثر أناقة.
الجانب السلبي لاستخدام هذا النمط هو الاختبار الصعب لأجزاء مختلفة من نظامنا. لا توجد طريقة رائعة لنا لمعرفة ما إذا كانت أجزاء الاشتراك في النظام تتصرف بالشكل المتوقع أم لا.
نمط الوسيط
سنغطي بإيجاز نمطًا مفيدًا جدًا أيضًا عند الحديث عن الأنظمة المنفصلة. عندما يكون لدينا سيناريو تحتاج فيه أجزاء متعددة من النظام إلى التواصل والتنسيق ، ربما يكون الحل الجيد هو تقديم وسيط.
الوسيط هو كائن يستخدم كنقطة مركزية للتواصل بين الأجزاء المتباينة من النظام ويتعامل مع سير العمل بينها. الآن ، من المهم التأكيد على أنه يتعامل مع سير العمل. لماذا هذا مهم؟
لوجود تشابه كبير مع نمط الناشر / المشترك. قد تسأل نفسك ، حسنًا ، يساعد هذان النموذجان في تنفيذ اتصال أفضل بين الكائنات ... ما الفرق؟
الفرق هو أن الوسيط يتعامل مع سير العمل ، بينما يستخدم الناشر / المشترك شيئًا يسمى نوع الاتصال "أطلق وانسى". الناشر / المشترك هو ببساطة مجمع للأحداث ، مما يعني أنه يعتني ببساطة بإطلاق الأحداث وإعلام المشتركين المناسبين بالأحداث التي تم إطلاقها. لا يهتم مُجمِّع الأحداث بما يحدث بمجرد إطلاق الحدث ، وهذا ليس هو الحال مع الوسيط.
من الأمثلة الجيدة على الوسيط نوع واجهة المعالج. لنفترض أن لديك عملية تسجيل كبيرة لنظام عملت عليه. في كثير من الأحيان ، عندما تكون هناك حاجة إلى الكثير من المعلومات من المستخدم ، فمن الجيد تقسيم ذلك إلى خطوات متعددة.
بهذه الطريقة ، سيكون الرمز أكثر وضوحًا (أسهل في الصيانة) ولن يكون المستخدم غارقًا في كمية المعلومات المطلوبة فقط من أجل إنهاء التسجيل. الوسيط هو كائن من شأنه أن يتعامل مع خطوات التسجيل ، مع مراعاة تدفقات العمل المختلفة المحتملة التي قد تحدث بسبب حقيقة أن كل مستخدم يمكن أن يكون لديه عملية تسجيل فريدة.
تتمثل الفائدة الواضحة من نمط التصميم هذا في تحسين الاتصال بين أجزاء مختلفة من النظام ، والتي تتواصل جميعها الآن من خلال وسيط وقاعدة كود أكثر نظافة.
سيكون الجانب السلبي هو أننا أدخلنا الآن نقطة فشل واحدة في نظامنا ، مما يعني أنه إذا فشل الوسيط لدينا ، فقد يتوقف النظام بأكمله عن العمل.
نمط النموذج الأولي
كما ذكرنا سابقًا في المقالة ، لا تدعم JavaScript الفئات في شكلها الأصلي. يتم تنفيذ الوراثة بين الكائنات باستخدام البرمجة القائمة على النموذج الأولي.
إنها تمكننا من إنشاء كائنات يمكن أن تكون بمثابة نموذج أولي للكائنات الأخرى التي يتم إنشاؤها. يتم استخدام كائن النموذج الأولي كمخطط لكل كائن ينشئه المنشئ.
نظرًا لأننا تحدثنا بالفعل عن هذا في الأقسام السابقة ، فلنعرض مثالًا بسيطًا لكيفية استخدام هذا النمط.
var personPrototype = { sayHi: function() { console.log("Hello, my name is " + this.name + ", and I am " + this.age); }, sayBye: function() { console.log("Bye Bye!"); } }; function Person(name, age) { name = name || "John Doe"; age = age || 26; function constructorFunction(name, age) { this.name = name; this.age = age; }; constructorFunction.prototype = personPrototype; var instance = new constructorFunction(name, age); return instance; } var person1 = Person(); var person2 = Person("Bob", 38); // prints out Hello, my name is John Doe, and I am 26 person1.sayHi(); // prints out Hello, my name is Bob, and I am 38 person2.sayHi();
Take notice how prototype inheritance makes a performance boost as well because both objects contain a reference to the functions which are implemented in the prototype itself, instead of in each of the objects.
Command Pattern
The command pattern is useful in cases when we want to decouple objects executing the commands from objects issuing the commands. For example, imagine a scenario where our application is using a large number of API service calls. Then, let's say that the API services change. We would have to modify the code wherever the APIs that changed are called.
This would be a great place to implement an abstraction layer, which would separate the objects calling an API service from the objects which are telling them when to call the API service. This way, we avoid modification in all of the places where we have a need to call the service, but rather have to change only the objects which are making the call itself, which is only one place.
As with any other pattern, we have to know when exactly is there a real need for such a pattern. We need to be aware of the tradeoff we are making, as we are adding an additional abstraction layer over the API calls, which will reduce performance but potentially save a lot of time when we need to modify objects executing the commands.
// the object which knows how to execute the command var invoker = { add: function(x, y) { return x + y; }, subtract: function(x, y) { return x - y; } } // the object which is used as an abstraction layer when // executing commands; it represents an interface // toward the invoker object var manager = { execute: function(name, args) { if (name in invoker) { return invoker[name].apply(invoker, [].slice.call(arguments, 1)); } return false; } } // prints 8 console.log(manager.execute("add", 3, 5)); // prints 2 console.log(manager.execute("subtract", 5, 3));
Facade Pattern
The facade pattern is used when we want to create an abstraction layer between what is shown publicly and what is implemented behind the curtain. It is used when an easier or simpler interface to an underlying object is desired.
A great example of this pattern would be selectors from DOM manipulation libraries such as jQuery, Dojo, or D3. You might have noticed using these libraries that they have very powerful selector features; you can write in complex queries such as:
jQuery(".parent .child div.span")
It simplifies the selection features a lot, and even though it seems simple on the surface, there is an entire complex logic implemented under the hood in order for this to work.
We also need to be aware of the performance-simplicity tradeoff. It is desirable to avoid extra complexity if it isn't beneficial enough. In the case of the aforementioned libraries, the tradeoff was worth it, as they are all very successful libraries.
الخطوات التالية
Design patterns are a very useful tool which any senior JavaScript developer should be aware of. Knowing the specifics regarding design patterns could prove incredibly useful and save you a lot of time in any project's lifecycle, especially the maintenance part. Modifying and maintaining systems written with the help of design patterns which are a good fit for the system's needs could prove invaluable.
In order to keep the article relatively brief, we will not be displaying any more examples. For those interested, a great inspiration for this article came from the Gang of Four book Design Patterns: Elements of Reusable Object-Oriented Software and Addy Osmani's Learning JavaScript Design Patterns . I highly recommend both books.