الفصل 10
الوحدات
اكتب شيفرة يسهل حذفها، لا شيفرة يسهل توسيعها.
— تيف، البرمجة أمر مريع

من الناحية المثالية، يكون للبرنامج بنية واضحة ومباشرة. يسهل شرح طريقة عمله، ويؤدي كل جزء فيه دوراً محدداً بدقة.
لكن البرامج في الممارسة العملية تنمو عضوياً. تُضاف قطع من الوظائف كلما اكتشف المبرمج احتياجات جديدة. ويتطلب الحفاظ على برنامج كهذا منظماً تنظيماً جيداً انتباهاً وعملاً دائمين. وهو عمل لن يؤتي ثماره إلا في المستقبل، في المرة القادمة التي يعمل فيها أحدهم على البرنامج، لذا يغري إهماله والسماح لأجزاء البرنامج المختلفة بالتشابك العميق.
يسبب هذا مشكلتين عمليتين. أولاً، فهم نظام متشابك أمر صعب. فإذا كان كل شيء قادراً على ملامسة كل شيء آخر، يصعب النظر إلى أي قطعة معينة بمعزل عن غيرها. وتُضطر إلى بناء فهم كلي للشيء بأكمله. ثانياً، إذا أردت استخدام أي من وظائف برنامج كهذا في موقف آخر، فقد يكون إعادة كتابتها أسهل من محاولة فك تشابكها عن سياقها.
غالباً ما يُستخدم تعبير «كرة الطين الكبيرة» (big ball of mud) لبرامج كبيرة بلا بنية كهذه. فكل شيء ملتصق ببعضه، وعندما تحاول انتزاع قطعة، يتفكك الشيء كله، فلا تنجح إلا في إحداث فوضى.
البرامج المعيارية
الوحدات (modules) محاولة لتجنب هذه المشكلات. الوحدة قطعة من برنامج تحدد القطع الأخرى التي تعتمد عليها والوظائف التي توفرها لوحدات أخرى لتستخدمها (أي واجهتها).
للواجهات الوحدات قاسم مشترك كبير مع واجهات الكائنات كما رأيناها في الفصل 6. فهي تُتيح جزءاً من الوحدة للعالم الخارجي وتُبقي الباقي خاصاً.
لكن الواجهة التي توفرها وحدة ليستخدمها غيرها نصف القصة فقط. فنظام وحدات جيد يتطلب أيضاً أن تحدد الوحدات الشيفرة التي تستخدمها هي من وحدات أخرى. وتسمى هذه العلاقات التبعيات (dependencies). فإذا استخدمت الوحدة A وظائف من الوحدة B، يقال إنها تعتمد على تلك الوحدة. وعندما تُحدَّد هذه الأمور بوضوح في الوحدة نفسها، يمكن استخدامها لمعرفة الوحدات الأخرى التي يجب أن تكون موجودة لتتمكن من استخدام وحدة معينة، ولتحميل التبعيات تلقائياً.
عندما تكون طرق تفاعل الوحدات مع بعضها بعضاً صريحة، يصبح النظام أشبه بقطع LEGO، حيث تتفاعل القطع عبر وصلات محددة بدقة، وأقل شبهاً بالطين، حيث يختلط كل شيء بكل شيء آخر.
وحدات ES
لم تكن للغة JavaScript الأصلية أي مفهوم للوحدات. كانت جميع النصوص البرمجية تعمل في النطاق نفسه، وكان الوصول إلى دالة معرَّفة في نص برمجي آخر يتم بالإشارة إلى الارتباطات العامة التي أنشأها ذلك النص. وقد شجع هذا بنشاط على تشابك الشيفرة العَرَضي الذي يصعب رؤيته، ودعا إلى مشكلات مثل محاولة نصوص برمجية غير مترابطة استخدام اسم الارتباط نفسه.
منذ ECMAScript 2015، تدعم JavaScript نوعين مختلفين من البرامج. تتصرف النصوص البرمجية (scripts) بالطريقة القديمة: تُعرَّف ارتباطاتها في النطاق العام، وليس لديها طريقة للإشارة مباشرة إلى نصوص برمجية أخرى. أما الوحدات فتحصل على نطاق منفصل خاص بها، وتدعم الكلمتين المفتاحيتين import وexport، وهما غير متاحتين في النصوص البرمجية، للإعلان عن تبعياتها وواجهتها. ويسمى نظام الوحدات هذا عادة وحدات ES (حيث ES اختصار لـ ECMAScript).
يتألف البرنامج المعياري (modular) من عدد من هذه الوحدات، موصولة ببعضها عبر عمليات الاستيراد والتصدير الخاصة بها.
تحوّل الوحدة التالية في المثال بين أسماء الأيام والأعداد (كما تُرجعها طريقة getDay في Date). وهي تعرّف ثابتاً ليس جزءاً من واجهتها، ودالتين هما كذلك. وليس لها تبعيات.
const names = ["Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"];
export function dayName(number) {
return names[number];
}
export function dayNumber(name) {
return names.indexOf(name);
}
يمكن وضع الكلمة المفتاحية export قبل تعريف دالة أو صنف أو ارتباط للإشارة إلى أن ذلك الارتباط جزء من واجهة الوحدة. وهذا يجعل من الممكن للوحدات الأخرى استخدام ذلك الارتباط باستيراده.
import {dayName} from "./dayname.js";
let now = new Date();
console.log(`Today is ${dayName(now.getDay())}`);
// → Today is Monday
الكلمة المفتاحية import، متبوعة بقائمة أسماء ارتباطات بين قوسين معقوفين، تجعل ارتباطات من وحدة أخرى متاحة في الوحدة الحالية. وتُعرَّف الوحدات بنصوص مقتبسة.
تختلف طريقة تحويل اسم وحدة كهذا إلى برنامج فعلي حسب المنصة. فالمتصفح يتعامل معها كعناوين ويب، بينما يحوّلها Node.js إلى ملفات. وعندما تشغّل وحدة، تُحمَّل جميع الوحدات الأخرى التي تعتمد عليها — والوحدات التي تعتمد هي عليها — وتصبح الارتباطات المصدَّرة متاحة للوحدات التي تستوردها.
لا يمكن أن تظهر تعليمات الاستيراد والتصدير داخل الدوال أو الحلقات أو الكتل الأخرى. فهي تُحل فوراً عند تحميل الوحدة، بغض النظر عن طريقة تنفيذ الشيفرة في الوحدة. وللتعبير عن ذلك، يجب أن تظهر فقط في جسم الوحدة الخارجي.
تتألف واجهة الوحدة بذلك من مجموعة من الارتباطات المسماة، تستطيع الوحدات الأخرى التي تعتمد على الوحدة الوصول إليها. ويمكن إعادة تسمية الارتباطات المستوردة لإعطائها اسماً محلياً جديداً باستخدام as بعد اسمها.
import {dayName as nomDeJour} from "./dayname.js";
console.log(nomDeJour(3));
// → Wednesday
وقد يكون للوحدة أيضاً تصدير خاص اسمه default، يُستخدم غالباً للوحدات التي تصدّر ارتباطاً واحداً فقط. ولتعريف تصدير افتراضي، تكتب export default قبل تعبير أو تعريف دالة أو تعريف صنف.
export default ["Winter", "Spring", "Summer", "Autumn"];
ويُستورد ارتباط كهذا بحذف القوسين المعقوفين حول اسم الاستيراد.
import seasonNames from "./seasonname.js";
لاستيراد جميع الارتباطات من وحدة في الوقت نفسه، يمكنك استخدام import *. وتقدم اسماً، وسيُربط ذلك الاسم بكائن يحمل جميع صادرات الوحدة. وقد يكون هذا مفيداً عندما تستخدم عدداً كبيراً من الصادرات المختلفة.
import * as dayName from "./dayname.js";
console.log(dayName.dayName(3));
// → Wednesday
الحزم
من مزايا بناء برنامج من قطع منفصلة والقدرة على تشغيل بعض تلك القطع بمفردها أنك قد تستطيع استخدام القطعة نفسها في برامج مختلفة.
لكن كيف ترتّب ذلك؟ لنقل إنني أريد استخدام دالة parseINI من الفصل 9 في برنامج آخر. إذا كان واضحاً ما تعتمد عليه الدالة (لا شيء في هذه الحالة)، فيمكنني ببساطة نسخ تلك الوحدة إلى مشروعي الجديد واستخدامها. لكن إن وجدت لاحقاً خطأً في الشيفرة، فسأصلحه على الأرجح في البرنامج الذي أعمل عليه حينها وأنسى إصلاحه في البرنامج الآخر أيضاً.
بمجرد أن تبدأ بتكرار الشيفرة، ستجد نفسك سريعاً تهدر الوقت والطاقة في نقل النسخ وإبقائها محدَّثة. وهنا تأتي الحزم (packages). الحزمة قطعة من الشيفرة يمكن توزيعها (نسخها وتثبيتها). وقد تحتوي على وحدة أو أكثر، ولديها معلومات عن الحزم الأخرى التي تعتمد عليها. وتأتي الحزمة عادةً أيضاً مع توثيق يشرح ما تفعله حتى يتمكن الأشخاص الذين لم يكتبوها من استخدامها.
وعندما يُكتشف خطأ في حزمة أو تُضاف ميزة جديدة، تُحدَّث الحزمة. والآن يمكن للبرامج التي تعتمد عليها (وقد تكون حزماً هي أيضاً) نسخ النسخة الجديدة للحصول على التحسينات التي أُجريت على الشيفرة.
يتطلب العمل بهذه الطريقة بنية تحتية. فنحن نحتاج إلى مكان لتخزين الحزم والعثور عليها، وطريقة مريحة لتثبيتها وترقيتها. وفي عالم JavaScript، توفر NPM (https://npmjs.com) هذه البنية التحتية.
NPM شيئان: خدمة إلكترونية يمكنك منها تنزيل الحزم (ورفعها)، وبرنامج (مضمّن مع Node.js) يساعدك على تثبيتها وإدارتها.
في وقت كتابة هذه السطور، هناك أكثر من ثلاثة ملايين حزمة مختلفة متاحة على NPM. وإنصافاً، جزء كبير منها رديء. لكن كل حزمة JavaScript مفيدة ومتاحة علناً تقريباً يمكن العثور عليها على NPM. فمثلاً، محلل ملفات INI، شبيه بالذي بنيناه في الفصل 9، متاح تحت اسم الحزمة ini.
سيوضح الفصل 20 كيفية تثبيت حزم كهذه محلياً باستخدام برنامج سطر الأوامر npm.
وجود حزم عالية الجودة متاحة للتنزيل أمر بالغ القيمة. فهو يعني أننا نستطيع غالباً تجنب إعادة اختراع برنامج كتبه 100 شخص من قبل، والحصول على تنفيذ متين ومختبَر جيداً بضغطة بضع مفاتيح.
نسخ البرمجيات رخيص، لذا بمجرد أن يكتبها أحدهم، يصبح توزيعها على الآخرين عملية فعالة. لكن كتابتها في المقام الأول عمل، والاستجابة للأشخاص الذين وجدوا مشكلات في الشيفرة أو يريدون اقتراح ميزات جديدة عمل أكبر.
بحكم الأصل، تملك حقوق النشر للشيفرة التي تكتبها، ولا يجوز للآخرين استخدامها إلا بإذنك. لكن لأن بعض الناس لطفاء فحسب، ولأن نشر برمجيات جيدة يمكن أن يساعدك على اكتساب بعض الشهرة بين المبرمجين، تُنشر حزم كثيرة تحت رخصة تسمح صراحةً للآخرين باستخدامها.
معظم الشيفرة على NPM مرخَّصة بهذه الطريقة. وبعض الرخص تطلب منك أيضاً نشر الشيفرة التي تبنيها فوق الحزمة تحت الرخصة نفسها. ورخص أخرى أقل تطلباً، تكتفي بأن تبقي الرخصة مع الشيفرة عند توزيعها. ويستخدم مجتمع JavaScript في الغالب النوع الأخير من الرخص. وعند استخدام حزم الآخرين، تأكد من أنك على علم برخصها.
الآن، بدلاً من كتابة محلل ملفات INI الخاص بنا، يمكننا استخدام واحد من NPM.
import {parse} from "ini";
console.log(parse("x = 10\ny = 20"));
// → {x: "10", y: "20"}
وحدات CommonJS
قبل عام 2015، حين لم تكن للغة JavaScript نظام وحدات مدمج، كان الناس يبنون أنظمة كبيرة في JavaScript بالفعل. ولجعل ذلك قابلاً للعمل، كانوا بحاجة إلى وحدات.
صمم المجتمع أنظمة وحدات مرتجلة خاصة به فوق اللغة. وتستخدم هذه الدوال لإنشاء نطاق محلي للوحدات وكائنات عادية لتمثيل واجهات الوحدات.
في البداية، كان الناس يغلّفون وحدتهم كاملة يدوياً في «تعبير دالة يُستدعى فوراً» (immediately invoked function expression) لإنشاء نطاق الوحدة، ويسندون كائنات واجهتهم إلى متغير عام واحد.
const weekDay = function() {
const names = ["Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"];
return {
name(number) { return names[number]; },
number(name) { return names.indexOf(name); }
};
}();
console.log(weekDay.name(weekDay.number("Sunday")));
// → Sunday
يوفر هذا النمط من الوحدات عزلاً، إلى حد معين، لكنه لا يعلن التبعيات. بل يضع واجهته فقط في النطاق العام ويتوقع من تبعياته، إن وُجدت، أن تفعل الشيء نفسه. وهذا ليس مثالياً.
إذا نفّذنا محمّل وحدات خاصاً بنا، يمكننا أن نفعل أفضل. أكثر المقاربات استخداماً للوحدات الملحقة بـ JavaScript تُسمى وحدات CommonJS. استخدم Node.js نظام الوحدات هذا منذ البداية (رغم أنه يعرف الآن أيضاً كيفية تحميل وحدات ES)، وهو نظام الوحدات المستخدم في حزم كثيرة على NPM.
تبدو وحدة CommonJS كنص برمجي عادي، لكنها تستطيع الوصول إلى ارتباطين تستخدمهما للتفاعل مع وحدات أخرى. الأول دالة تسمى require. وعندما تستدعيها باسم وحدة تبعيتك، فإنها تتأكد من تحميل الوحدة وتُرجع واجهتها. والثاني كائن اسمه exports، وهو كائن واجهة الوحدة. يبدأ فارغاً وتضيف إليه خصائص لتعريف القيم المصدَّرة.
تقدم وحدة CommonJS هذه في المثال دالة لتنسيق التواريخ. وهي تستخدم حزمتين من NPM — ordinal لتحويل الأعداد إلى نصوص مثل "1st" و"2nd"، وdate-names للحصول على الأسماء الإنجليزية لأيام الأسبوع والشهور. وتصدّر دالة واحدة، formatDate، تأخذ كائن Date ونص قالب.
قد يحتوي نص القالب على رموز توجه التنسيق، مثل YYYY للسنة الكاملة وDo لليوم الترتيبي في الشهر. ويمكنك إعطاؤه نصاً مثل "MMMM Do YYYY" للحصول على إخراج مثل November 22nd 2017.
const ordinal = require("ordinal");
const {days, months} = require("date-names");
exports.formatDate = function(date, format) {
return format.replace(/YYYY|M(MMM)?|Do?|dddd/g, tag => {
if (tag == "YYYY") return date.getFullYear();
if (tag == "M") return date.getMonth();
if (tag == "MMMM") return months[date.getMonth()];
if (tag == "D") return date.getDate();
if (tag == "Do") return ordinal(date.getDate());
if (tag == "dddd") return days[date.getDay()];
});
};
واجهة ordinal دالة واحدة، بينما تصدّر date-names كائناً يحتوي على عدة أشياء — days وmonths مصفوفتان من الأسماء. والتفكيك مريح جداً عند إنشاء ارتباطات لواجهات مستوردة.
تضيف الوحدة دالة واجهتها إلى exports حتى تحصل الوحدات التي تعتمد عليها على إمكانية الوصول إليها. ويمكننا استخدام الوحدة هكذا:
const {formatDate} = require("./format-date.js");
console.log(formatDate(new Date(2017, 9, 13),
"dddd the Do"));
// → Friday the 13th
تُنفَّذ CommonJS بمحمّل وحدات يغلّف شيفرة الوحدة، عند تحميلها، في دالة (مانحاً إياها نطاقاً محلياً خاصاً بها) ويمرر ارتباطي require وexports إلى تلك الدالة كمعطيات.
إذا افترضنا أننا نستطيع الوصول إلى دالة readFile تقرأ ملفاً باسمه وتعطينا محتواه، يمكننا تعريف صيغة مبسطة من require هكذا:
function require(name) {
if (!(name in require.cache)) {
let code = readFile(name);
let exports = require.cache[name] = {};
let wrapper = Function("require, exports", code);
wrapper(require, exports);
}
return require.cache[name];
}
require.cache = Object.create(null);
Function دالة مدمجة في JavaScript تأخذ قائمة معطيات (كنص مفصول بفواصل) ونصاً يحتوي جسم الدالة، وتُرجع قيمة دالة بتلك المعطيات وذلك الجسم. هذا مفهوم مثير للاهتمام — فهو يتيح لبرنامج إنشاء قطع برمجية جديدة من بيانات نصية — لكنه خطير أيضاً، لأنه إذا استطاع أحدهم خداع برنامجك لإدخال نص يقدمه هو إلى Function، فيمكنه جعل البرنامج يفعل أي شيء يريده.
لا توفر JavaScript القياسية دالة مثل readFile، لكن بيئات JavaScript المختلفة، مثل المتصفح وNode.js، توفر طرقها الخاصة للوصول إلى الملفات. والمثال يتظاهر فقط بوجود readFile.
لتجنب تحميل الوحدة نفسها مرات متعددة، يحتفظ require بمخزن (ذاكرة تخزين مؤقت) للوحدات المحمَّلة بالفعل. وعند استدعائه، يتحقق أولاً مما إذا كانت الوحدة المطلوبة قد حُمِّلت، وإن لم تكن، يحمّلها. ويتضمن ذلك قراءة شيفرة الوحدة وتغليفها في دالة واستدعاءها.
وبتعريف require وexports كوسيطين لدالة الغلاف المولَّدة (وتمرير القيم المناسبة عند استدعائها)، يتأكد المحمّل من أن هذين الارتباطين متاحان في نطاق الوحدة.
ثمة فرق مهم بين هذا النظام ووحدات ES هو أن عمليات استيراد وحدات ES تحدث قبل أن يبدأ نص الوحدة بالعمل، بينما require دالة عادية تُستدعى عندما تكون الوحدة قيد العمل بالفعل. وخلافاً لتعليمات import، يمكن أن تظهر نداءات require داخل الدوال، ويمكن أن يكون اسم التبعية أي تعبير يُقيَّم إلى نص، بينما تسمح import بنصوص مقتبسة صريحة فقط.
كان انتقال مجتمع JavaScript من نمط CommonJS إلى وحدات ES انتقالاً بطيئاً وشائكاً بعض الشيء. لحسن الحظ، صرنا الآن في مرحلة توفر فيها معظم الحزم الشائعة على NPM شيفرتها كوحدات ES، وتسمح Node.js لوحدات ES بالاستيراد من وحدات CommonJS. ومع أن شيفرة CommonJS ما تزال شيئاً ستصادفه، فلم يبقَ سبب حقيقي لكتابة برامج جديدة بهذا النمط.
البناء والتجميع
كثير من حزم JavaScript ليست مكتوبة بلغة JavaScript من الناحية التقنية. فامتدادات اللغة مثل TypeScript، لهجة التحقق من الأنواع المذكورة في الفصل 8، مستخدمة على نطاق واسع. ويبدأ الناس أيضاً غالباً باستخدام ميزات لغوية جديدة مخطط لها قبل وقت طويل من إضافتها إلى المنصات التي تشغّل JavaScript فعلياً. ولجعل ذلك ممكناً، يصرّفون شيفرتهم، فيترجمونها من لهجة JavaScript التي اختاروها إلى JavaScript عادية قديمة — أو حتى إلى نسخة سابقة من JavaScript — حتى تستطيع المتصفحات تشغيلها.
إن تضمين برنامج معياري يتكون من 200 ملف مختلف في صفحة ويب ينتج مشكلاته الخاصة. فإذا استغرق جلب ملف واحد عبر الشبكة 50 ميلي ثانية، فسيستغرق تحميل البرنامج كله 10 ثوانٍ، أو ربما نصف ذلك إن استطعت تحميل عدة ملفات في وقت واحد. وهذا قدر كبير من الوقت المهدور. ولأن جلب ملف واحد كبير يميل إلى أن يكون أسرع من جلب كثير من الملفات الصغيرة، بدأ مبرمجو الويب باستخدام أدوات تجمع برامجهم (التي قسموها بعناء إلى وحدات) في ملف واحد كبير قبل نشرها على الويب. وتسمى أدوات كهذه المجمّعات (bundlers).
ويمكننا المضي أبعد. فإلى جانب عدد الملفات، يحدد حجم الملفات أيضاً سرعة نقلها عبر الشبكة. وهكذا اخترع مجتمع JavaScript المضغّطات (minifiers). وهي أدوات تأخذ برنامج JavaScript وتصغّره بإزالة التعليقات والمسافات البيضاء تلقائياً، وإعادة تسمية الارتباطات، واستبدال قطع من الشيفرة بشيفرة مكافئة تشغل مساحة أقل.
ليس نادراً أن تكون الشيفرة التي تجدها في حزمة NPM أو التي تعمل في صفحة ويب قد مرّت بـمراحل تحويل متعددة — التحويل من JavaScript الحديثة إلى JavaScript التاريخية، ودمج الوحدات في ملف واحد، وضغط الشيفرة. ولن ندخل في تفاصيل هذه الأدوات في هذا الكتاب، لأنها كثيرة، وأيها شائع يتغير بانتظام. كن فقط على علم بوجود أشياء كهذه، وابحث عنها عند حاجتك إليها.
تصميم الوحدات
هيكلة البرامج من ألطف جوانب البرمجة. فأي قطعة وظيفية غير بسيطة يمكن تنظيمها بطرق متنوعة.
تصميم البرنامج الجيد ذو طابع شخصي — ففيه مقايضات ومسائل ذوق. وأفضل طريقة لتعلم قيمة التصميم الجيد البنية هي قراءة كثير من البرامج أو العمل عليها وملاحظة ما ينجح وما لا ينجح. لا تفترض أن فوضى مؤلمة «هي ما هي عليه فحسب». يمكنك تحسين بنية كل شيء تقريباً بمزيد من التفكير فيه.
أحد جوانب تصميم الوحدات سهولة الاستخدام. فإذا كنت تصمم شيئاً يُقصد به أن يستخدمه عدة أشخاص — أو حتى أنت نفسك، بعد ثلاثة أشهر حين لن تتذكر تفاصيل ما فعلته — فمن المفيد أن تكون واجهتك بسيطة وقابلة للتوقع.
قد يعني ذلك اتباع الاصطلاحات القائمة. ومن الأمثلة الجيدة حزمة ini. فهذه الوحدة تحاكي كائن JSON القياسي بتوفير دالتي parse وstringify (لكتابة ملف INI)، وهي، مثل JSON، تحوّل بين النصوص والكائنات العادية. والواجهة صغيرة ومألوفة، وبعد أن تعمل بها مرة، يُرجَّح أن تتذكر كيفية استخدامها.
حتى إن لم توجد دالة قياسية أو حزمة شائعة الاستخدام لتحاكيها، يمكنك إبقاء وحداتك قابلة للتوقع باستخدام بنى بيانات بسيطة والقيام بشيء واحد محدد. فمثلاً، توفر كثير من وحدات تحليل ملفات INI على NPM دالة تقرأ ملفاً كهذا مباشرة من القرص الصلب وتحلله. وهذا يجعل استخدام وحدات كهذه في المتصفح، حيث لا نملك وصولاً مباشراً إلى نظام الملفات، أمراً مستحيلاً، ويضيف تعقيداً كان الأفضل معالجته بـتركيب الوحدة مع بعض دالة قراءة الملفات.
وهذا يشير إلى جانب مفيد آخر من تصميم الوحدات — سهولة تركيب الشيء مع شيفرة أخرى. فالوحدات المركزة التي تحسب قيماً قابلة للتطبيق في نطاق أوسع من البرامج أكثر من الوحدات الأكبر التي تنفذ أفعالاً معقدة بتأثيرات جانبية. فقارئ ملفات INI الذي يصرّ على قراءة الملف من القرص عديم الفائدة في سيناريو يأتي فيه محتوى الملف من مصدر آخر.
وبالمثل، تكون الكائنات ذات الحالة مفيدة أحياناً أو حتى ضرورية، لكن إن كان يمكن فعل شيء ما بدالة، فاستخدم دالة. وتوفر عدة من قرّاء ملفات INI على NPM نمط واجهة يتطلب منك أولاً إنشاء كائن، ثم تحميل الملف في كائنك، وأخيراً استخدام طرائق متخصصة للوصول إلى النتائج. وهذا النوع من الأشياء شائع في التقليد الكائني التوجه، وهو مريع. فبدلاً من إجراء نداء دالة واحد والمضي قدماً، عليك أداء طقس تحريك كائنك عبر حالاته المختلفة. ولأن البيانات صارت ملفوفة الآن في نوع كائن متخصص، يجب على كل شيفرة تتفاعل معه أن تعرف ذلك النوع، مما ينشئ ترابطات متبادلة غير ضرورية.
غالباً، لا يمكن تجنب تعريف بنى بيانات جديدة — فلا توفر معايير اللغة إلا عدداً قليلاً من البنى الأساسية، وكثير من أنواع البيانات يجب أن تكون أكثر تعقيداً من مصفوفة أو خريطة. لكن عندما تكفي مصفوفة، فاستخدم مصفوفة.
ومن أمثلة بنية البيانات الأكثر تعقيداً قليلاً الرسم البياني من الفصل 7. ولا توجد طريقة واحدة واضحة لتمثيل رسم بياني في JavaScript. استخدمنا في ذلك الفصل كائناً تحمل خصائصه مصفوفات من النصوص — العقد الأخرى التي يمكن الوصول إليها من تلك العقدة.
هناك عدة حزم مختلفة لإيجاد المسارات على NPM، لكن أياً منها لا يستخدم صيغة الرسم البياني هذه. فهي تسمح عادةً لحواف الرسم البياني بأن يكون لها وزن، وهو التكلفة أو المسافة المرتبطة بها. وهذا غير ممكن في تمثيلنا.
فمثلاً، هناك حزمة dijkstrajs. ثمة مقاربة معروفة لإيجاد المسارات، شبيهة جداً بدالة findRoute التي لدينا، تسمى خوارزمية دايكسترا (Dijkstra’s algorithm)، على اسم إدسخر دايكسترا الذي دوّنها أول مرة. وغالباً ما تُضاف اللاحقة js إلى أسماء الحزم للإشارة إلى أنها مكتوبة بـ JavaScript. وتستخدم حزمة dijkstrajs هذه صيغة رسم بياني شبيهة بصيغتنا، لكنها تستخدم بدلاً من المصفوفات كائنات قيم خصائصها أعداد — أوزان الحواف.
إذا أردنا استخدام تلك الحزمة، فسيتعين علينا التأكد من أن رسمنا البياني مخزَّن بالصيغة التي تتوقعها. وتحصل جميع الحواف على الوزن نفسه، لأن نموذجنا المبسط يعامل كل طريق كأن تكلفته واحدة (منعطف واحد).
const {find_path} = require("dijkstrajs");
let graph = {};
for (let node of Object.keys(roadGraph)) {
let edges = graph[node] = {};
for (let dest of roadGraph[node]) {
edges[dest] = 1;
}
}
console.log(find_path(graph, "Post Office", "Cabin"));
// → ["Post Office", "Alice's House", "Cabin"]
قد يشكل هذا عائقاً أمام التركيب — فعندما تستخدم حزم متنوعة بنى بيانات مختلفة لوصف أشياء متشابهة، يصعب دمجها. لذا إن أردت التصميم لقابلية التركيب، فاكتشف بنى البيانات التي يستخدمها الآخرون، واتبع مثالهم عندما يكون ذلك ممكناً.
قد يكون تصميم بنية وحدة مناسبة لبرنامج أمراً صعباً. وفي المرحلة التي ما زلت تستكشف فيها المشكلة وتجرب فيها أشياء مختلفة لترى ما ينجح، قد ترغب في عدم القلق بشأنها كثيراً، لأن إبقاء كل شيء منظماً قد يكون تشتيتاً كبيراً. وبمجرد أن يصبح لديك شيء يبدو متيناً، يكون ذلك وقتاً جيداً للتراجع خطوة وتنظيمه.
الملخص
توفر الوحدات بنية للبرامج الأكبر بفصل الشيفرة إلى قطع ذات واجهات وتبعيات واضحة. والواجهة هي جزء الوحدة المرئي للوحدات الأخرى، والتبعيات هي الوحدات الأخرى التي تستخدمها.
ولأن JavaScript لم توفر تاريخياً نظام وحدات، بُني نظام CommonJS فوقها. ثم في مرحلة ما حصلت فعلاً على نظام مدمج، وهو الآن يتعايش بصعوبة مع نظام CommonJS.
والحزمة قطعة من الشيفرة يمكن توزيعها بمفردها. وNPM مستودع لحزم JavaScript. يمكنك تنزيل جميع أنواع الحزم المفيدة (وغير المفيدة) منه.
التمارين
روبوت معياري
هذه هي الارتباطات التي ينشئها المشروع من الفصل 7:
roads
buildGraph
roadGraph
VillageState
runRobot
randomPick
randomRobot
mailRoute
routeRobot
findRoute
goalOrientedRobot
إذا كتبت ذلك المشروع كبرنامج معياري، فما الوحدات التي ستنشئها؟ وأي وحدة ستعتمد على أي وحدة أخرى، وكيف ستبدو واجهاتها؟
ما القطع التي يُرجَّح أن تكون متاحة مكتوبة مسبقاً على NPM؟ وهل تفضل استخدام حزمة NPM أم كتابتها بنفسك؟
إظهار التلميح
إليك ما كنت سأفعله (لكن مرة أخرى، لا توجد طريقة صحيحة وحيدة لتصميم وحدة معينة):
توجد الشيفرة المستخدمة لبناء رسم الطرق البياني في وحدة graph.js. ولأنني أفضل استخدام dijkstrajs من NPM على شيفرة إيجاد المسارات الخاصة بنا، سنجعل هذه الوحدة تبني نوع بيانات الرسم البياني الذي تتوقعه dijkstrajs. وتصدّر هذه الوحدة دالة واحدة، buildGraph. وكنت سأجعل buildGraph تقبل مصفوفة من مصفوفات من عنصرين، بدلاً من نصوص تحتوي على شرطات، لجعل الوحدة أقل اعتماداً على صيغة المدخلات.
تحتوي وحدة roads.js على بيانات الطرق الخام (مصفوفة roads) وارتباط roadGraph. وتعتمد هذه الوحدة على ./graph.js وتصدّر رسم الطرق البياني.
يوجد صنف VillageState في وحدة state.js. وهي تعتمد على وحدة ./roads.js لأنها تحتاج إلى القدرة على التحقق من وجود طريق معين. وتحتاج أيضاً إلى randomPick. ولأن تلك دالة من ثلاثة أسطر، يمكننا ببساطة وضعها في وحدة state.js كدالة مساعدة داخلية. لكن randomRobot تحتاجها هي أيضاً. لذا سيتعين علينا إما تكرارها أو وضعها في وحدتها الخاصة. ولأن هذه الدالة توجد مصادفةً على NPM في حزمة random-item، فالحل المعقول هو جعل كلتا الوحدتين تعتمدان على تلك الحزمة. ويمكننا إضافة دالة runRobot إلى هذه الوحدة أيضاً، لأنها صغيرة ووثيقة الصلة بإدارة الحالة. وتصدّر الوحدة كلاً من صنف VillageState ودالة runRobot.
وأخيراً، يمكن أن تذهب الروبوتات، مع القيم التي تعتمد عليها مثل mailRoute، إلى وحدة example-robots.js، التي تعتمد على ./roads.js وتصدّر دوال الروبوتات. ولتمكين goalOrientedRobot من إيجاد المسارات، تعتمد هذه الوحدة أيضاً على dijkstrajs.
وبإحالة بعض العمل إلى وحدات NPM، صارت الشيفرة أصغر قليلاً. وتفعل كل وحدة على حدة شيئاً بسيطاً إلى حد ما ويمكن قراءتها بمفردها. كما أن تقسيم الشيفرة إلى وحدات يوحي غالباً بتحسينات إضافية في تصميم البرنامج. وفي هذه الحالة، يبدو غريباً بعض الشيء أن تعتمد VillageState والروبوتات على رسم طرق بياني معين. وقد تكون فكرة أفضل جعل الرسم البياني معطى لبانية الحالة وجعل الروبوتات تقرأه من كائن الحالة — فهذا يقلل التبعيات (وهو أمر جيد دائماً) ويتيح تشغيل المحاكيات على خرائط مختلفة (وهو أفضل حتى).
هل من فكرة جيدة استخدام وحدات NPM لأشياء كان يمكننا كتابتها بأنفسنا؟ من حيث المبدأ، نعم — ففي الأشياء غير البسيطة مثل دالة إيجاد المسارات، يُرجَّح أن ترتكب أخطاء وتهدر وقتاً في كتابتها بنفسك. أما الدوال الصغيرة مثل random-item، فكتابتها بنفسك سهلة بما يكفي. لكن إضافتها حيثما تحتاجها يميل فعلاً إلى إثقال وحداتك.
ومع ذلك، لا ينبغي أن تبخس أيضاً العمل المتضمن في العثور على حزمة NPM مناسبة. وحتى إن وجدت واحدة، فقد لا تعمل جيداً أو قد تنقصها بعض الميزات التي تحتاجها. وفوق ذلك، فإن الاعتماد على حزم NPM يعني أنه عليك التأكد من تثبيتها، وعليك توزيعها مع برنامجك، وقد تضطر إلى ترقيتها دورياً.
لذا مرة أخرى، هذه مقايضة، ويمكنك أن تقرر في أي من الاتجاهين بناءً على مقدار ما تساعدك به حزمة معينة فعلاً.
وحدة الطرق
اكتب وحدة ES مبنية على المثال من الفصل 7 تحتوي على مصفوفة الطرق وتصدّر بنية بيانات الرسم البياني التي تمثلها باسم roadGraph. وهي تعتمد على وحدة ./graph.js تصدّر دالة buildGraph، تُستخدم لبناء الرسم البياني. وتتوقع هذه الدالة مصفوفة من مصفوفات من عنصرين (نقطتي البداية والنهاية للطرق).
// أضف التبعيات والصادرات
const roads = [
"Alice's House-Bob's House", "Alice's House-Cabin",
"Alice's House-Post Office", "Bob's House-Town Hall",
"Daria's House-Ernie's House", "Daria's House-Town Hall",
"Ernie's House-Grete's House", "Grete's House-Farm",
"Grete's House-Shop", "Marketplace-Farm",
"Marketplace-Post Office", "Marketplace-Shop",
"Marketplace-Town Hall", "Shop-Town Hall"
];
إظهار التلميح
بما أن هذه وحدة ES، عليك استخدام import للوصول إلى وحدة الرسم البياني. وقد وُصفت تلك بأنها تصدّر دالة buildGraph، ويمكنك انتزاعها من كائن واجهتها بتعريف const تفكيكي.
لتصدير roadGraph، تضع الكلمة المفتاحية export قبل تعريفه. ولأن buildGraph تأخذ بنية بيانات لا تطابق roads تطابقاً دقيقاً، يجب أن يحدث تقسيم نصوص الطرق في وحدتك.
التبعيات الدائرية
التبعية الدائرية (circular dependency) حالة تعتمد فيها الوحدة A على B، وتعتمد B أيضاً، مباشرةً أو غير مباشرة، على A. وتحظر أنظمة وحدات كثيرة هذا ببساطة، لأنه أي ترتيب تختاره لتحميل وحدات كهذه، لا يمكنك التأكد من أن تبعيات كل وحدة قد حُمِّلت قبل تشغيلها.
تسمح وحدات CommonJS بصيغة محدودة من التبعيات الدورية. فما دامت الوحدات لا تصل إلى واجهة بعضها بعضاً إلا بعد انتهاء تحميلها، فالتبعيات الدورية مقبولة.
دالة require المعروضة سابقاً في هذا الفصل تدعم هذا النوع من دورات التبعية. هل ترى كيف تتعامل مع الدورات؟
إظهار التلميح
الحيلة هي أن require تضيف كائن الواجهة الخاص بوحدة إلى ذاكرتها المؤقتة قبل أن تبدأ تحميل الوحدة. وبهذه الطريقة، إذا حاول أي نداء require يُجرى أثناء تشغيلها تحميلها، فهي معروفة بالفعل، وستُرجَع الواجهة الحالية بدلاً من البدء في تحميل الوحدة مرة أخرى (وهو ما سيؤدي في النهاية إلى تجاوز سعة المكدس).