أنماط تصميم جافا سكريبت

يقول مؤلف المادة ، التي ننشر ترجمتها ، أنه عند بدء المشروع ، لا يبدأون على الفور في كتابة التعليمات البرمجية. بادئ ذي بدء ، يحددون هدف المشروع وحدوده ، ثم يحددون الفرص التي يجب أن تتاح لها. بعد ذلك ، إما أن يكتبوا الشفرة على الفور ، أو ، إذا كان مشروعًا معقدًا إلى حد ما ، فإنهم يختارون أنماط تصميم مناسبة تشكل أساسها. هذه المادة عن أنماط تصميم جافا سكريبت. تم تصميمه في المقام الأول للمطورين المبتدئين.



ما هو نمط التصميم؟


في مجال تطوير البرمجيات ، فإن نمط التصميم هو تصميم معماري قابل للتكرار وهو حل لمشكلة التصميم في سياق غالبًا ما ينشأ. أنماط التصميم هي ملخص لتجربة مطوري البرامج المحترفين. يمكن اعتبار نمط التصميم بمثابة نوع من النمط يتم بموجبه كتابة البرامج.

لماذا تحتاج أنماط التصميم؟


يعتقد العديد من المبرمجين أن أنماط التصميم مضيعة للوقت ، أو ببساطة لا يعرفون كيفية تطبيقها بشكل صحيح. ومع ذلك ، فإن استخدام نمط مناسب يمكن أن يساعد في كتابة كود أفضل وأكثر قابلية للفهم ، والذي ، نظرًا لفهمه ، سيكون من الأسهل الحفاظ عليه.

ربما الشيء الأكثر أهمية هنا هو أن استخدام الأنماط يمنح مطوري البرامج شيئًا مثل قاموس المصطلحات المعروفة المفيدة جدًا ، على سبيل المثال ، عند تحليل رمز شخص آخر. تكشف الأنماط الغرض من أجزاء معينة من البرنامج لأولئك الذين يحاولون التعامل مع جهاز المشروع.

على سبيل المثال ، إذا كنت تستخدم نمط "Decorator" ، فسيُبلغ المبرمج الجديد الذي جاء إلى المشروع على الفور بالمهام التي تحلها قطعة معينة من التعليمات البرمجية وسبب الحاجة إليها. وبفضل هذا ، سيكون هذا المبرمج قادرًا على تخصيص مزيد من الوقت للمهام العملية التي يحلها البرنامج ، بدلاً من محاولة فهم هيكله الداخلي.

الآن بعد أن اكتشفنا أنماط التصميم والغرض منها ، سننتقل إلى الأنماط نفسها ووصف تنفيذها باستخدام جافا سكريبت.

نمط "الوحدة النمطية"


الوحدة النمطية هي جزء مستقل من التعليمات البرمجية يمكن تغييره دون التأثير على رمز المشروع الآخر. بالإضافة إلى ذلك ، تسمح الوحدات بتجنب ظاهرة مثل تلوث مناطق الرؤية نظرًا لأنها تنشئ مناطق رؤية منفصلة للمتغيرات المعلنة فيها. يمكن إعادة استخدام الوحدات المكتوبة لمشروع واحد في مشاريع أخرى في حالة كون آلياتها عالمية وغير مرتبطة بخصائص مشروع معين.

تعتبر الوحدات جزءًا لا يتجزأ من أي تطبيق JavaScript حديث. فهي تساعد في الحفاظ على نظافة التعليمات البرمجية ، وتساعد على فصل التعليمات البرمجية إلى أجزاء ذات معنى ، وتساعد على تنظيمها. جافا سكريبت لديها العديد من الطرق لإنشاء وحدات ، أحدها هو نمط "الوحدة النمطية".

بخلاف لغات البرمجة الأخرى ، ليس لدى JavaScript محددات وصول. أي أنه لا يمكن الإعلان عن المتغيرات على أنها خاصة أو عامة. ونتيجة لذلك ، يستخدم نمط "الوحدة" أيضًا لمحاكاة مفهوم التغليف.

يستخدم هذا النمط IIFE (التعبير الوظيفي المستدعى فورًا) ، والإغلاق ، ونطاقات الوظائف لتقليد هذا المفهوم. على سبيل المثال:

const myModule = (function() {   const privateVariable = 'Hello World';   function privateMethod() {    console.log(privateVariable);  }  return {    publicMethod: function() {      privateMethod();    }  } })(); myModule.publicMethod(); 

نظرًا لأن لدينا IIFE ، يتم تنفيذ الشفرة على الفور ويتم تعيين الكائن الذي يتم إرجاعه بواسطة التعبير إلى myModule الثابت. نظرًا لوجود إغلاق ، فإن الكائن الذي تم إرجاعه يمكنه الوصول إلى الوظائف والمتغيرات المعلنة داخل IIFE ، حتى بعد انتهاء IIFE من عمله.

ونتيجة لذلك ، يتم إخفاء المتغيرات والوظائف المعلنة داخل IIFE عن الآليات الموجودة في نطاق الرؤية الخارجية لها. لقد تبين أنها كيانات خاصة من ثابت myModule .

بعد تنفيذ هذا الرمز ، myModule كما يلي:

 const myModule = { publicMethod: function() {   privateMethod(); }}; 

بمعنى ، بالإشارة إلى هذا الثابت ، يمكنك استدعاء الأسلوب العام publicMethod() ، والذي بدوره ، سيتم استدعاء الأسلوب الخاص privateMethod() . على سبيل المثال:

 //  'Hello World' module.publicMethod(); 

افتح نمط الوحدة


نمط الكشف عن الوحدة هو نسخة محسنة قليلاً من نمط الوحدة النمطية التي اقترحها كريستيان هيلمان. تكمن مشكلة نمط "الوحدة النمطية" في أنه يتعين علينا إنشاء وظائف عامة فقط للوصول إلى الوظائف والمتغيرات الخاصة.

في هذا النمط ، نقوم بتعيين وظائف خاصة لخصائص الكائن المرتجع الذي نريد جعله عامًا. هذا هو السبب في أن هذا النمط يسمى "الوحدة النمطية المفتوحة". فكر في مثال:

 const myRevealingModule = (function() { let privateVar = 'Peter'; const publicVar  = 'Hello World'; function privateFunction() {   console.log('Name: '+ privateVar); } function publicSetName(name) {   privateVar = name; } function publicGetName() {   privateFunction(); } /**    ,     */ return {   setName: publicSetName,   greeting: publicVar,   getName: publicGetName }; })(); myRevealingModule.setName('Mark'); //  Name: Mark myRevealingModule.getName(); 

يسهّل تطبيق هذا النمط فهم الوظائف والمتغيرات في الوحدة المتاحة للجمهور ، مما يساعد على تحسين قراءة التعليمات البرمجية.

بعد تشغيل IIFE ، يبدو myRevealingModule كما يلي:

 const myRevealingModule = { setName: publicSetName, greeting: publicVar, getName: publicGetName }; 

يمكننا ، على سبيل المثال ، استدعاء طريقة myRevealingModule.setName('Mark') ، وهي إشارة إلى الوظيفة العامة publicSetName . يشير الأسلوب myRevealingModule.getName() إلى الوظيفة العامة publicGetName . على سبيل المثال:

 myRevealingModule.setName('Mark'); //  Name: Mark myRevealingModule.getName(); 

ضع في اعتبارك مزايا نمط "Open Module" مقارنة بنموذج "Module":

  • تتيح لك "الوحدة المفتوحة" إنشاء كيانات مخفية عامة للوحدة (وإخفائها مرة أخرى إذا لزم الأمر) ، وتعديل لكل سطر منها سطر واحد فقط في الكائن الذي تم إرجاعه بعد IIFE.
  • لا يحتوي الكائن الذي تم إرجاعه على تعريف دالة. يتم تعريف كل شيء على يمين أسماء ممتلكاته في IIFE. هذا يساعد على إبقاء التعليمات البرمجية نظيفة وسهلة القراءة.

الوحدات في ES6


قبل إصدار معيار ES6 ، لم يكن لدى JavaScript أداة قياسية للعمل مع الوحدات النمطية ؛ ونتيجة لذلك ، كان على المطورين استخدام مكتبات الجهات الخارجية أو نمط "الوحدة النمطية" لتنفيذ الآليات المناسبة. ولكن مع ظهور ES6 ، ظهر نظام وحدة قياسي في JavaScript.

يتم تخزين وحدات ES6 في الملفات. يمكن أن يحتوي ملف واحد على وحدة واحدة فقط. كل شيء داخل الوحدة خاص بشكل افتراضي. يمكن جعل الدوال والمتغيرات والفئات عامة باستخدام الكلمة الأساسية export . يتم تنفيذ التعليمات البرمجية داخل الوحدة النمطية دائمًا في وضع صارم.

▍ وحدة التصدير


هناك طريقتان لتصدير دالة أو متغير معلن في وحدة نمطية:

  • يتم التصدير بإضافة الكلمة الأساسية export قبل الإعلان عن وظيفة أو متغير. على سبيل المثال:

     // utils.js export const greeting = 'Hello World'; export function sum(num1, num2) { console.log('Sum:', num1, num2); return num1 + num2; } export function subtract(num1, num2) { console.log('Subtract:', num1, num2); return num1 - num2; } //  -   function privateLog() { console.log('Private Function'); } 
  • يتم التصدير عن طريق إضافة الكلمة الأساسية export إلى نهاية التعليمات البرمجية التي تسرد أسماء الوظائف والمتغيرات التي سيتم تصديرها. على سبيل المثال:

     // utils.js function multiply(num1, num2) { console.log('Multiply:', num1, num2); return num1 * num2; } function divide(num1, num2) { console.log('Divide:', num1, num2); return num1 / num2; } //    function privateLog() { console.log('Private Function'); } export {multiply, divide}; 

portاستيراد الوحدة النمطية


تمامًا كما توجد طريقتان للتصدير ، هناك طريقتان لاستيراد الوحدات. يتم ذلك باستخدام الكلمة الأساسية import :

  • استيراد عناصر متعددة محددة. على سبيل المثال:

     // main.js //     import { sum, multiply } from './utils.js'; console.log(sum(3, 7)); console.log(multiply(3, 7)); 
  • استيراد كل ما تصدر الوحدة النمطية. على سبيل المثال:

     // main.js //  ,    import * as utils from './utils.js'; console.log(utils.sum(3, 7)); console.log(utils.multiply(3, 7)); 

▍ الأسماء المستعارة للكيانات المصدرة والمستوردة


إذا كانت أسماء الوظائف أو المتغيرات التي تم تصديرها إلى التعليمات البرمجية يمكن أن تسبب تصادمًا ، فيمكن تغييرها أثناء التصدير أو أثناء الاستيراد.

لإعادة تسمية الكيانات أثناء التصدير ، يمكنك القيام بذلك:

 // utils.js function sum(num1, num2) { console.log('Sum:', num1, num2); return num1 + num2; } function multiply(num1, num2) { console.log('Multiply:', num1, num2); return num1 * num2; } export {sum as add, multiply}; 

لإعادة تسمية الكيانات أثناء الاستيراد ، يتم استخدام البناء التالي:

 // main.js import { add, multiply as mult } from './utils.js'; console.log(add(3, 7)); console.log(mult(3, 7)); 

نمط Singleton


نمط "Singleton" أو "Singleton" هو كائن يمكن أن يوجد في نسخة واحدة فقط. كجزء من تطبيق هذا النمط ، يتم إنشاء مثيل جديد للفئة إذا لم يتم إنشاؤه بعد. إذا كان مثيل الفئة موجودًا بالفعل ، فعند محاولة الوصول إلى المُنشئ ، يتم إرجاع مرجع إلى الكائن المقابل. ستعيد المكالمات اللاحقة للمنشئ دائمًا نفس الكائن.

في الواقع ، ما نسميه نمط "Singleton" كان موجودًا دائمًا في JavaScript ، لكنهم لا يسمونه "Singleton" ، ولكن "كائن حرفي". فكر في مثال:

 const user = { name: 'Peter', age: 25, job: 'Teacher', greet: function() {   console.log('Hello!'); } }; 

نظرًا لأن كل كائن في JavaScript يشغل مساحة الذاكرة الخاصة به ولا يشاركه مع كائنات أخرى ، كلما وصلنا إلى متغير user ، نحصل على ارتباط إلى نفس الكائن.

يمكن تنفيذ نمط Singleton باستخدام وظيفة المُنشئ. يبدو هذا:

 let instance = null; function User(name, age) { if(instance) {   return instance; } instance = this; this.name = name; this.age = age; return instance; } const user1 = new User('Peter', 25); const user2 = new User('Mark', 24); //  true console.log(user1 === user2); 

عندما يتم استدعاء وظيفة المُنشئ ، فإنه يتحقق أولاً لمعرفة ما إذا كان كائن instance موجودًا. إذا لم تتم تهيئة المتغير المقابل ، فسيتم كتابة this على instance . إذا كان المتغير يحتوي بالفعل على مرجع إلى كائن ، فإن المُنشئ يقوم ببساطة بإرجاع instance ، أي مرجع إلى كائن موجود.

يمكن تنفيذ نمط Singleton باستخدام نمط الوحدة النمطية. على سبيل المثال:

 const singleton = (function() { let instance; function User(name, age) {   this.name = name;   this.age = age; } return {   getInstance: function(name, age) {     if(!instance) {       instance = new User(name, age);     }     return instance;   } } })(); const user1 = singleton.getInstance('Peter', 24); const user2 = singleton.getInstance('Mark', 26); // prints true console.log(user1 === user2); 

ننشئ هنا مثيلًا جديدًا user عن طريق استدعاء طريقة singleton.getInstance() . إذا كان مثيل الكائن موجودًا بالفعل ، فستقوم هذه الطريقة ببساطة بإعادته. إذا لم يكن هناك مثل هذا الكائن حتى الآن ، فإن الطريقة تنشئ نسخة جديدة منه عن طريق استدعاء وظيفة مُنشئ User .

نمط المصنع


يستخدم نمط المصنع ما يسمى أساليب المصنع لإنشاء الكائنات. لا تحتاج إلى تحديد فئات أو وظائف المُنشئ التي يتم استخدامها لإنشاء كائنات.

يستخدم هذا النمط لإنشاء كائنات في الحالات التي لا يكون فيها من الضروري جعل منطق إنشائها عامًا. يمكن استخدام نمط المصنع إذا كنت بحاجة إلى إنشاء كائنات مختلفة وفقًا لظروف معينة. على سبيل المثال:

 class Car{ constructor(options) {   this.doors = options.doors || 4;   this.state = options.state || 'brand new';   this.color = options.color || 'white'; } } class Truck { constructor(options) {   this.doors = options.doors || 4;   this.state = options.state || 'used';   this.color = options.color || 'black'; } } class VehicleFactory { createVehicle(options) {   if(options.vehicleType === 'car') {     return new Car(options);   } else if(options.vehicleType === 'truck') {     return new Truck(options);     } } } 

يتم إنشاء فئات Car Truck هنا ، والتي تنص على استخدام قيم قياسية معينة. يتم استخدامها لإنشاء كائنات car truck . يتم أيضًا الإعلان عن فئة VehicleFactory هنا ، والتي يتم استخدامها لإنشاء كائنات جديدة استنادًا إلى تحليل خاصية vehicleType ، والتي تنتقل إلى الطريقة المقابلة للكائن الذي vehicleType في الكائن مع options . إليك كيفية العمل مع كل هذا:

 const factory = new VehicleFactory(); const car = factory.createVehicle({ vehicleType: 'car', doors: 4, color: 'silver', state: 'Brand New' }); const truck= factory.createVehicle({ vehicleType: 'truck', doors: 2, color: 'white', state: 'used' }); //  Car {doors: 4, state: "Brand New", color: "silver"} console.log(car); //  Truck {doors: 2, state: "used", color: "white"} console.log(truck); 

يتم إنشاء كائن factory من فئة VehicleFactory . بعد ذلك ، يمكنك إنشاء كائنات من فئات Car أو Truck عن طريق استدعاء الأسلوب factory.createVehicle() وتمرير كائن options مع تعيين خاصية propertyType إلى car أو truck .

نمط الديكور


يتم استخدام نمط الديكور لتوسيع وظائف الكائنات دون تعديل الفئات الموجودة أو وظائف المنشئ. يمكن استخدام هذا النمط لإضافة إمكانات معينة للكائنات دون تعديل الكود المسؤول عن إنشائها.

إليك مثال بسيط لاستخدام هذا النمط:

 function Car(name) { this.name = name; //    this.color = 'White'; } //   ,    const tesla= new Car('Tesla Model 3'); //   -    tesla.setColor = function(color) { this.color = color; } tesla.setPrice = function(price) { this.price = price; } tesla.setColor('black'); tesla.setPrice(49000); //  black console.log(tesla.color); 

دعونا نفكر الآن في مثال عملي لتطبيق هذا النمط. لنفترض أن تكلفة السيارات تعتمد على ميزاتها ، على الوظائف الإضافية المتاحة لها. بدون استخدام نمط الديكور ، لوصف هذه السيارات ، سيتعين علينا إنشاء فئات مختلفة لمجموعات مختلفة من هذه الوظائف الإضافية ، والتي سيكون لكل منها طريقة للعثور على تكلفة السيارة. على سبيل المثال ، قد يبدو مثل هذا:

 class Car() { } class CarWithAC() { } class CarWithAutoTransmission { } class CarWithPowerLocks { } class CarWithACandPowerLocks { } 

بفضل النمط المعني ، يمكنك إنشاء Car الدرجة الأساسية ، تصف ، على سبيل المثال ، سيارة في التكوين الأساسي ، يتم التعبير عن تكلفتها ببعض المبلغ الثابت. بعد ذلك ، يمكن توسيع الكائن القياسي الذي تم إنشاؤه على أساس هذه الفئة باستخدام وظائف الديكور. تحصل "السيارة" القياسية التي تتم معالجتها بواسطة هذه الوظيفة على فرص جديدة ، والتي تؤثر ، بالإضافة إلى ذلك ، على سعرها. على سبيل المثال ، يمكن تنفيذ هذا المخطط على النحو التالي:

 class Car { constructor() { //   this.cost = function() { return 20000; } } } // - function carWithAC(car) { car.hasAC = true; const prevCost = car.cost(); car.cost = function() {   return prevCost + 500; } } // - function carWithAutoTransmission(car) { car.hasAutoTransmission = true;  const prevCost = car.cost(); car.cost = function() {   return prevCost + 2000; } } // - function carWithPowerLocks(car) { car.hasPowerLocks = true; const prevCost = car.cost(); car.cost = function() {   return prevCost + 500; } } 

هنا ننشئ أولاً Car الفئة الأساسية ، والتي تُستخدم لإنشاء كائنات تمثل السيارات كمعيار قياسي. ثم نقوم بإنشاء العديد من وظائف الديكور التي تسمح لنا بتوسيع كائنات فئة Car الأساسية بخصائص إضافية. تأخذ هذه الوظائف الكائنات المقابلة كمعلمات. بعد ذلك ، نضيف خاصية جديدة إلى الكائن ، مع الإشارة إلى الميزة الجديدة التي سيتم تجهيز السيارة بها ، وإعادة تعريف وظيفة cost الكائن ، والتي تعيد الآن التكلفة الجديدة للسيارة. ونتيجة لذلك ، من أجل "تجهيز" سيارة التكوين القياسية بشيء جديد ، يمكننا استخدام التصميم التالي:

 const car = new Car(); console.log(car.cost()); carWithAC(car); carWithAutoTransmission(car); carWithPowerLocks(car); 

بعد ذلك ، يمكنك معرفة تكلفة السيارة في تكوين محسّن:

 //       console.log(car.cost()); 

الملخص


في هذه المقالة ، فحصنا العديد من أنماط التصميم المستخدمة في JavaScript ، ولكن في الواقع ، لا يزال هناك الكثير من الأنماط التي يمكن استخدامها لحل مجموعة واسعة من المشاكل.

في حين أن المعرفة بأنماط التصميم المختلفة مهمة للمبرمج ، فإن استخدامها المناسب مهم بنفس القدر. بمعرفة الأنماط ونطاق تطبيقها ، يمكن للمبرمج ، الذي يحلل المهمة التي أمامه ، أن يفهم أي نوع من الأنماط قادر على المساعدة في حلها.

أعزائي القراء! ما هي أنماط التصميم التي تستخدمها في أغلب الأحيان؟

Source: https://habr.com/ru/post/ar427293/


All Articles