فصل 10ماژول‌ها

کدی بنویسید که راحت بتوان در صورت نیاز حذفش کرد، لازم نیست قابل توسعه باشد.

تیف, برنامه‌نویسی وحشتناک است
Picture of a building built from modular pieces

یک برنامه‌ی ایده‌آل دارای ساختاری شفاف و روشن است. به راحتی می‌توان کارکرد آن را توضیح داد و هر بخش از آن نقشی را ایفا می‌کند که به خوبی تعریف شده است.

معمولا یک برنامه‌ی واقعی به شکلی ارگانیک رشد می‌کند. قابلیت‌های جدید، همانطور که لازم می‌شوند به برنامه افزوده می‌شوند. ساختاردهی – و حفظ ساختار – کار مجزایی است. کاری است که فقط در آینده، زمانی که کسی دوباره روی برنامه قرار است کار کند، با مزایای آن روبرو می‌شوید. پس ممکن است وسوسه شوید که از آن غفلت کنید و بگذارید بخش‌های برنامه عمیقا دچار آشفتگی شوند.

در عمل این غفلت دو اشکال ایجاد می‌کند. اول اینکه درک یک سیستم بدون ساختار مشکل است. اگر همه‌ی بخش‌های برنامه در تماس با دیگر بخش‌ها باشند، سخت می‌توان بخشی از برنامه را به صورت جداگانه بررسی نمود. شما باید درکی کلی و جامع از برنامه داشته باشید. دوم اینکه، اگر بخواهید هر کدام از قابلیت‌های برنامه‌ای اینچنینی را در جایی دیگر استفاده کنید، از اول نوشتن آن قابلیت ممکن است از جداسازی آن از برنامه، آسان‌تر باشد.

اصطلاح توپ بزرگ گلی (“big ball of mud”)، اغلب برای برنامه‌های بزرگی که ساختاری ندارند استفاده می‌شود. همه چیز به هم چسبیده است و زمانی که قصد دارید یک قسمت را جدا کنید، کل آن قسمت یا برنامه متلاشی می‌شود و دستانتان را کثیف می‌کند.

ماژول‌ها

استفاده از ماژول‌ها تلاشی برای اجتناب از این گونه مشکلات است. یک ماژول بخشی از برنامه است که مشخص می‌کند به کدام بخش‌های دیگر از برنامه وابسته است (وابستگی‌های آن) و چه قابلیتی برای استفاده‌ی دیگر ماژول‌ها فراهم می‌کند (رابط آن).

رابط‌های ماژول شباهت زیادی با رابط‌های شیء دارند، همانطور که با آن‌ها در فصل 6 آشنا شدیم. رابط‌ها بخشی از ماژول را در دسترس جهان بیرون می‌گذارند و بقیه‌ی قسمت‌ها را به صورت خصوصی حفظ می‌کنند. با محدودسازی راه‌های تعامل ماژول‌ها با یکدیگر، سیستم بیشتر شبیه لگو می‌شود، جایی که قطعات توسط متصل‌کننده‌هایی که به خوبی تعریف شده‌اند با هم تعامل دارند و کمتر به توپ گلی شباهت خواهند داشت که همه چیز در آن با هم مخلوط شده است.

ارتباطات بین ماژول‌ها را وابستگی‌ها (dependency)می‌نامند. زمانی که یک ماژول به بخشی از یک ماژول دیگر نیاز دارد، گفته می‌شود که به آن ماژول وابستگی دارد. زمانی که این وابستگی در خود ماژول به صورت مشخص اعلام شود، می‌توان از آن برای شناسایی دیگر ماژول‌هایی که لازم است برای اجرای یک ماژول خاص حضور داشته باشند استفاده کرد و به صورت خودکار آن وابستگی‌ها را بارگیری کرد.

برای جداسازی ماژول‌ها به این روش، لازم است هر کدام از آن‌ها، قلمروی (scope) خصوصی خودشان را داشته باشند.

فقط قرار دادن کد‌های جاوااسکریپت در فایل‌های جداگانه این امکان را فراهم نمی‌کند. فایل‌ها همچنان فضای نام سراسری یکسانی را به صورت مشترک استفاده می‌کنند. ممکن است به صورت تصادفی یا آگاهانه بین متغیر‌های یکدیگر تداخل ایجاد کنند و ساختار وابستگی همچنان غیر شفاف خواهد ماند. می‌توان کار بهتری انجام داد که در ادامه خواهیم دید.

طراحی یک ساختار ماژول مناسب برای یک برنامه ممکن است سخت باشد. در مرحله‌ای که هنوز در حال بررسی مشکل هستید، و چیز‌های متفاوتی را آزمایش می‌کنید، ممکن است علاقه‌ای نداشته باشید که زیاد به ساختاردهی فکر کنید، چرا که می‌تواند باعث حواس‌پرتی زیادی بشود. زمانی که به نتیجه‌ای استوار و قابل اتکا رسیدید، وقت مناسب برای اقدام جهت سازماندهی برنامه فرا رسیده است.

بسته‌ها (Packages)

یکی از مزایای ساختن یک برنامه بر اساس قسمت‌های جداگانه، و داشتن قابلیت اجرای آن قسمت‌ها به صورت مستقل، این است که ممکن است بتوانید آن قسمت‌ها را در برنامه‌های مختلف به کار ببرید.

اما چگونه آن را راه‌اندازی می‌کنید؟ فرض کنیم من می‌خواهم که تابع parseINI را که در فصل 9 نوشتیم در برنامه‌ی دیگری استفاده کنم. اگر روشن باشد که این تابع چه وابستگی‌هایی دارد (که در اینجا ندارد)، می‌توانم به سادگی کد‌های مورد نیاز را به پروژه‌ی جدیدم کپی کنم و از آن استفاده کنم. اما در این صورت اگر مشکلی در آن کد پیدا کنم، احتمالا آن مشکل را فقط در برنامه‌ای که در حال کار روی آن هستم رفع خواهم کرد و فراموش می‌کنم که در برنامه‌ی دیگر نیز آن را اصلاح کنم.

به محض اینکه به کپی کردن کد‌ها اقدام کنید متوجه خواهید شد که با جابجا کردن کپی‌ها بین برنامه‌ها و به روز‌رسانی آن‌ها وقت و انرژی خودتان را تلف می‌کنید.

اینجا است که بسته‌ها کاربرد خواهند داشت. یک بسته یک قطعه کد است که می‌تواند توزیع شود (کپی و نصب شود). یک بسته می‌تواند دارای یک یا چند ماژول باشد و همچنین اطلاعاتی درباره‌ی دیگر بسته‌هایی که به آن‌ها وابسته است را خواهد داشت. یک بسته همچنین همراه با مستنداتی می‌آید که کارکرد آن را شرح می‌دهد، بنابراین کسانی که بسته را ننوشته‌اند قادر خواهند بود که از آن استفاده کنند.

زمانی که مشکلی در یک بسته شناسایی شود یا ویژگی جدیدی به آن افزوده شود بسته به روز‌رسانی می‌گردد. اکنون برنامه‌هایی که به آن وابستگی داشته‌اند (که خود ممکن است بسته باشند) می‌توانند به نسخه جدیدتر به‌روز شوند.

کارکردن به این روش نیاز به زیرساخت دارد. جایی را نیاز داریم که بسته‌ها را ذخیره و جستجو کنیم و راهی سرراست برای نصب و به روز‌رسانی آن‌ها باید وجود داشته باشد. در دنیای جاوااسکریپت این زیرساخت توسط NPM در (https://npmjs.org) فراهم شده است.

NPM دارای دو بخش است: یک سرویس آنلاین که افراد می‌توانند به بارگیری و بارگذاری بسته‌ها اقدام کنند و یک برنامه (که به همراه Node.js می‌آید) که به شما کمک می‌کند که آن‌ها را مدیریت کنید.

در زمان نوشتن این کتاب، بیش از نیم میلیون بسته در NPM وجود دارد. باید اشاره کنم که بخش زیادی از این بسته‌ها دیگر کاربردی ندارند اما تقریبا هر بسته‌ی مفیدی که عموما در دسترس باشد را می‌توان آنجا پیدا کرد. به عنوان مثال، یک تجزیه‌گر فایل ini، شبیه به چیزی که خودمان در فصل 9 ساختیم، در بسته‌ای به نام ini وجود دارد.

فصل 20 شما را با نحوه‌ی نصب بسته‌ها به صورت محلی و با استفاده از برنامه‌ی خط فرمان npm آشنا خواهد کرد.

در دسترس داشتن بسته‌های باکیفیت و قابل دانلود بسیار ارزشمند است. این بدین معنا است که می‌توان از اختراع دوباره‌ی یک برنامه در بیشتر مواقع جلوگیری کنیم، برنامه‌ای که صد‌ها نفر پیش‌تر نوشته‌اند و می‌توان آن را با چند حرکت به صورت استوار و آزموده شده پیاده‌سازی کرد.

کپی کردن یک نرم‌افزار آسان است، بنابراین اگر کسی آن را نوشته باشد، انتشار آن بین دیگر افراد روند کارآمدی محسوب می‌شود. اما به عنوان فرد اول، نوشتن آن کار می‌برد و پاسخ دادن به افرادی که مشکلاتی را در کد پیدا می‌کنند یا درخواست ویژگی جدیدی را دارند، کار بیشتری می‌طلبد.

به صورت پیش فرض، شما مالک کپی‌رایت کدی هستید که می‌نویسید و دیگر افراد فقط با اجازه‌ی شما می‌توانند از آن استفاده کنند. اما به دلیل اینکه بعضی‌ها سخاوتمند هستند و اینکه انتشار یک نرم‌افزار خوب باعث کمی شهرت بین برنامه‌نویسان می‌شود، خیلی از بسته‌ها تحت مجوزی منتشر می‌شوند که به صراحت اجازه استفاده دیگران از برنامه را می‌دهد.

بیشتر کد‌هایی که در NPM وجود دارند دارای چنین مجوزی هستند. بعضی از مجوز‌ها از شما می‌خواهند که کدی که با استفاده از بسته نوشته‌اید را با همان مجوز منتشر کنید. بعضی دیگر کمتر درخواستی دارند و فقط لازم است که مجوزی که همراه بسته وجود دارد را در هنگام توزیع کنار آن نگه دارید. بیشتر جامعه‌ی برنامه‌نویسی جاوااسکریپت از این نوع مجوز استفاده می‌کنند. زمانی که از بسته‌های دیگر افراد استفاده می‌کنید، مطمئن شوید که از مجوز آن آگاهی دارید.

فراهم ساختن ماژول‌ها

قبل از 2015 در جاوااسکریپت سیستم ماژول داخلی وجود نداشت. با این وجود برنامه‌نویسان برای بیشتر از یک دهه، سیستم‌های بزرگی را برنامه‌نویسی می‌کردند درحالیکه نیاز به ماژول‌ها وجود داشت.

بنابراین آن‌ها سیستم ماژول خودشان را با استفاده از خود زبان طراحی کردند. می‌توان از توابع جاوااسکریپت برای ایجاد قلمرو‌های محلی و از اشیاء به عنوان رابط‌های ماژول استفاده کرد.

مثال زیر یک ماژول برای انتخاب بین نام روز‌ها و عددشان است (که از متد getDay مربوط به Date استفاده می‌کند). رابط آن از weekDay.name و weekDay.number تشکیل شده است و متغیر محلی names را در قلمروی یک تابع که بلادرنگ فراخوانی می‌شود پنهان می‌کند.

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

این سبک از ماژول‌ها، تا حدی ایزوله کردن را فراهم می‌کند اما وابستگی را پشتیبانی نمی‌کند. به جای آن، رابطش را در قلمروی سراسری قرار می‌دهد و انتظار دارد که در صورت وجود، وابستگی‌هایش تعریف شده باشند تا بتواند کاری مشابه سیستم وابستگی‌ها انجام دهد. برای مدتی طولانی این روش در برنامه‌نویسی وب استفاده می‌شد اما الان تقریبا از رده خارج شده است.

اگر قصد دارید ارتباطات مربوط به وابستگی را به عنوان بخشی از کد داشته باشید، باید مدیریت بارگیری وابستگی‌ها را به عهده بگیرید. برای این کار لازم است بتوانیم رشته‌ها را به عنوان کد اجرا کنیم. این کار در جاوااسکریپت قابل اجرا است.

ارزیابی داده‌ها به عنوان کد‌های اجرایی

راه‌های متعددی برای گرفتن داده‌ها (یک رشته از کد) و اجرای آن به عنوان بخشی از برنامه کنونی وجود دارد.

روشن‌ترین راه، استفاده از عملگر eval است که رشته‌ای را در قلمروی (scope) فعلی اجرا می‌کند. این ایده معمولا ایده‌ی بدی محسوب می‌شود چرا که باعث از بین رفتن بعضی از خاصیت‌هایی می‌شود که در قلمرو‌ها به صورت نرمال وجود دارند، مثل حدس زدن آسان متغیری که یک نام مشخص به آن ارجاع می‌دهد.

const x = 1;
function evalAndReturnX(code) {
  eval(code);
  return x;
}

console.log(evalAndReturnX("var x = 2"));
// → 2
console.log(x);
// → 1

یک راه کم‌خطرتر تفسیر داده به عنوان کد، استفاده از سازنده Function است. این تابع دو آرگومان دریافت می‌کند: یک رشته که حاوی یک لیست جدا شده با ویرگول از نام آرگومان‌ها است و یک رشته که حاوی بدنه تابع است. این تابع کد را درون یک مقدار تابع می‌پوشاند بنابراین قلمروی مختص به خودش را خواهد داشت و تداخلی با دیگر قلمرو‌ها نخواهد داشت.

let plusOne = Function("n", "return n + 1;");
console.log(plusOne(4));
// → 5

این دقیقا همان چیزی است که برای یک سیستم ماژول نیاز داریم. می‌توانیم کد‌های ماژول را درون یک تابع قرار دهیم و از قلمروی مربوط به تابع به عنوان قلمروی ماژول استفاده نماییم.

CommonJS

رایج‌ترین شیوه‌ای که برای اضافه کردن ماژول‌ها به جاوااسکریپت استفاده می‌شود، ماژول‌های CommonJS می‌باشد. Node.js از آن استفاده می‌کند و سیستمی است که بیشتر بسته‌های NPM از آن استفاده می‌کنند.

مفهوم اصلی در ماژول‌های CommonJS تابعی به نام ‌require است. زمانی که از این تابع به همراه نام ماژول یک وابستگی استفاده می‌کنید، این تابع اطمینان حاصل می‌کند که ماژول مورد نظر بارگیری شده است و رابط آن را برمی گرداند.

به دلیل اینکه بارگیرنده (loader)، ماژول مورد نظر را درون یک تابع قرار می‌دهد، ماژول‌ها به صورت خودکار قلمروی محلی خودشان را می‌گیرند. تنها کاری که باید بکنند این است که تابع require را فراخوانی کنند تا به وابستگی‌های آن‌ها دسترسی داشته باشند و رابطشان را در شیئی که به exports تخصیص یافته قرار دهند.

این ماژول به عنوان مثال، یک تابع ویرایش تاریخ را فراهم می‌سازد. اینجا از دو بسته از NPM استفاده می‌شود – بسته‌ی ordinal برای تبدیل اعداد به رشته‌هایی شبیه "1 st" و "2nd" و بسته‌ی date-names برای گرفتن نام‌های انگلیسی روز‌های هفته و ماه‌ها. این ماژول یک تابع به نام fomatDate را صادر (export) می‌کند که یک شیء 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 یک شیء را که حاوی چند چیز متفاوت است صادر می‌کند – دو مقداری که در نام‌های آرایه‌ها استفاده کردیم. استفاده از روش تجزیه (Destructuring) برای ایجاد متغیر برای رابط‌های وارد شده بسیار مناسب است.

ماژول تابع رابط خودش را به exports اضافه می‌کند در نتیجه ماژول‌های وابسته، به آن دسترسی خواهند داشت. می‌توانیم از این ماژول به این صورت استفاده کنیم:

const {formatDate} = require("./format-date");

console.log(formatDate(new Date(2017, 9, 13),
                       "dddd the Do"));
// → Friday the 13th

می‌توان require را در کوتاهترین شکل خودش تعریف کرد:

require.cache = Object.create(null);

function require(name) {
  if (!(name in require.cache)) {
    let code = readFile(name);
    let module = {exports: {}};
    require.cache[name] = module;
    let wrapper = Function("require, exports, module", code);
    wrapper(require, module.exports, module);
  }
  return require.cache[name].exports;
}

در این کد، readFile یک تابع ساختگی است که یک فایل را خوانده و محتوای آن را به صورت رشته برمی‌گرداند. جاوااسکریپت استاندارد، این قابلیت را فراهم نمی‌کند – اما محیط‌های متفاوت جاوااسکریپت مثل مرورگر و Node.js، راه‌های خودشان را برای دسترسی به فایل‌ها فراهم می‌سازند. مثال بالا فقط وانمود می‌کند که readFile وجود دارد.

برای جلوگیری از بارگیری چندباره‌ی یک ماژول، require ماژول‌هایی که تاکنون بارگیری شده‌اند را جایی (در حافظه‌ی نهان) نگه داری می‌کند. در زمان فراخوانی، ابتدا بارگیری ماژول خواسته شده را در گذشته بررسی می‌کند و در صورت نبود، آن را بارگیری می‌کند. این روند شامل خواندن کد ماژول، قرار دادن آن درون یک تابع و فراخوانی آن می‌شود.

رابط بسته‌ی ordinal که قبل‌تر دیدیم یک شیء نیست بلکه یک تابع است. یک ایراد وارده به ماژول‌های CommonJS این است که اگرچه سیستم ماژول، یک رابط به صورت شیئی خالی برای شما ایجاد می‌کند (که در exports قرار می‌گیرد)، می‌توانید آن را با هر مقداری که بخواهید به وسیله‌ی بازنویسی خاصیت module.exports جایگزین کنید. این کار را خیلی از ماژول‌ها انجام می‌دهند تا بتوانند یک مقدار ساده را به جای یک شیء رابط صادر کنند.

با تعریف require، exports و module به عنوان پارامتر‌های تابع wrapper تولید شده (و ارسال مقدار‌های مناسب در هنگام فراخوانی)، بارگیرنده اطمینان حاصل می‌کند که این متغیر‌ها در قلمروی مربوط به ماژول در دسترس خواهند بود.

روشی که در آن، رشته‌ی داده شده به require به یک نام فایل واقعی یا یک آدرس وب تفسیر می‌شود، در سیستم‌های مختلف متفاوت است. زمانی که این رشته با "./" یا "../" شروع می‌شود عموما نسبت به نام فایل ماژول فعلی در نظر گرفته می‌شود. بنابراین "./format-date" فایلی به نام format-date.js که در همان پوشه قرار دارد در نظر گرفته می‌شود.

زمانی که نام نسبی نیست، Node.js به جستجوی بسته‌ای با همان نام اقدام می‌کند. در کد مثال این فصل، ما این گونه نام‌ها را به عنوان بسته‌های NPM تفسیر می‌کنیم. در فصل 20 به جزئیات نصب و استفاده از ماژول‌های NPM خواهیم پرداخت.

اکنون به جای نوشتن تجزیه‌گر فایل INI خودمان، می‌توانیم از یکی از بسته‌های موجود در NPM استفاده کنیم.

const {parse} = require("ini");

console.log(parse("x = 10\ny = 20"));
// → {x: "10", y: "20"}

ماژول‌های ECMASCRIPT

ماژول‌های CommonJS به خوبی کار می‌کنند و ترکیب آن‌ها با NPM به جامعه‌ی برنامه‌نویسان جاوااسکریپت اجازه داده است که کد‌ها را در مقیاس بزرگ به اشتراک بگذارند.

اما کمی لازم است تا دستی به سر و روی آن‌ها بکشیم. ظاهر نوشتاری آن اندکی مشکل دارد – به عنوان مثال چیز‌هایی را که به exports اضافه می‌کنید در قلمروی محلی در دسترس نیستند. و به دلیل اینکه require یک فراخوانی به یک تابع معمولی است که هر نوع آرگومانی را قبول می‌کند، نه فقط مقادیر رشته‌ای، تشخیص وابستگی‌های یک ماژول بدون اجرای کد‌های آن می‌تواند سخت شود.

به همین علت است که جاوااسکریپت استاندارد از نسخه‌ی 2015، سیستم ماژول متفاوت خودش را معرفی کرد که معمولا ES modules نامیده می‌شود. ES مخفف ECMAScript است. مفاهیم اصلی وابستگی‌ها و رابط‌ها به همان صورت باقی می‌ماند اما جزئیات آن‌ها متفاوت است. درنتیجه، نشانه‌گذاری آن اکنون درون زبان یکپارچه شده است. به جای فراخوانی یک تابع برای دسترسی به یک وابستگی، از کلید‌واژه‌ی مخصوصی به نام import استفاده می‌کنید.

import ordinal from "ordinal";
import {days, months} from "date-names";

export function formatDate(date, format) { /* ... */ }

به طور مشابه، کلیدواژه‌ی export برای صدور استفاده می‌شود. می‌توان از آن در ابتدای یک تابع، کلاس، یا تعریف متغیر استفاده کرد (let، const، یا var)

یک رابط ماژول ES، یک مقدار واحد نیست بلکه مجموعه‌ای از متغیر‌های نام‌گذاری شده است. ماژولی که در بالا آمده است formatDate را به یک تابع تخصیص می‌دهد. زمانی که از یک ماژول دیگر اقدام به وارد کردن می‌کنید شما متغیر را وارد می‌کنید نه مقدار آن را که معنای آن این است که یک صدور ماژول ممکن است مقدار یک متغیر را در هر زمانی تغییر دهد و ماژول‌هایی که آن را وارد کرده‌اند، مقدار جدیدش را خواهند دید.

زمانی که انتسابی را به صورت default مشخص می‌کنید، آن انتساب یا متغیر به عنوان مقدار صادر شده‌ی اصلی ماژول در نظر گرفته می‌شود. اگر ماژولی مانند ordinal را که در مثال آمد، بدون استفاده از کروشه‌های دور نام متغیر، import کنید، متغیر default آن ماژول را دریافت می‌کنید. این گونه ماژول‌ها می‌توانند در کنار صدور default، متغیر‌های دیگری را هم با نام‌های مختلف صادر کنند.

برای ایجاد یک صدور پیش فرض (default) می‌توانید از export default قبل از یک عبارت، تعریف تابع یا کلاس استفاده کنید.

export default ["Winter", "Spring", "Summer", "Autumn"];

می‌توان نام متغیر‌هایی که وارد شده‌اند را با استفاده از as تغییر داد.

import {days as dayNames} from "date-names";

console.log(dayNames.length);
// → 7

یک تفاوت مهم دیگر این است که عمل import ماژول ES قبل از این که اجرای اسکریپت ماژول شروع شود اتفاق می‌افتد. یعنی import را نمی‌توان درون تابع یا بلاک‌ها قرار داد و نام وابستگی‌ها باید به صورت رشته محصور در نقل قول باشد نه عبارت‌های قابل ارزیابی جاوااسکریپت.

در زمان نوشتن این کتاب، جامعه‌ی برنامه‌نویسان جاوااسکریپت در مسیر استفاده از این سبک ماژول‌ها هستند. اما این روند به آهستگی اتفاق می‌افتد. چند سالی بعد از مشخص شدن فرمت جدید طول کشید تا مرورگر‌ها و Node.js پشتیبانی از آن را شروع کنند. و اگرچه آن‌ها تقریبا از آن پشتیبانی کامل می‌کنند اما این پشتیبانی هنوز با ایراداتی روبرو است و بحث‌هایی پیرامون شیوه‌ی توزیع این گونه ماژول‌ها در NPM هنوز در جریان است.

خیلی از برنامه‌ها را با ماژول‌های ES می‌نویسند و به صورت خودکار به دیگر فرمت‌ها در هنگام انتشار تبدیل می‌کنند. ما در دوره‌ی ‌گذار به سر می‌بریم که در آن دو سیستم مدیریت ماژول همزمان استفاده می‌شوند و خوب است که قادر باشیم کد‌ها را در هر دو سیستم بنویسیم و بخوانیم.

ساخت و بسته‌بندی

در واقع خیلی از پروژه‌های جاوااسکریپت، از لحاظ فنی، در خود زبان جاوااسکریپت نوشته نمی‌شوند. افزونه‌هایی وجود دارند، مانند همان گویشی از جاوااسکریپت که به انواع داده حساس بود و در فصل 8 ذکر شد، که بسیار محبوب می‌باشند. برنامه نویسان اغلب خیلی زودتر به سراغ استفاده از افزونه‌های برنامه‌ریزی شده برای زبان ‌می‌روند قبل از آنکه به محیط‌های مجری جاوااسکریپت اضافه شوند.

ببرای اجرایی کردن این کار، کد‌هایشان را کامپایل می‌کنند، کد را از گویش جاوااسکریپت مورد نظرشان به جاوااسکریپت ساده ترجمه می‌کنند – یا حتی به یک نسخه‌ی قدیمی از جاوااسکریپت ترجمه می‌کنند که مرورگر‌های قدیمی بتوانند آن را اجرا کنند.

قرار دادن یک برنامه‌ی ماژولار که از 200 فایل متفاوت تشکیل شده است در یک صفحه‌ی وب مشکلات خودش را خواهد داشت. اگر دریافت و استفاده از یک فایل در شبکه 50 هزارم ثانیه زمان بگیرد، کل برنامه ده ثانیه زمان خواهد گرفت یا شاید نیمی از آن زمان اگر بتوانیم چندین فایل را همزمان بارگیری کنیم. این زمان زیادی را تلف خواهد کرد. به دلیل اینکه بارگیری یک فایل بزرگ واحد، از تعداد زیادی فایل کوچک، عموما سریع‌تر اتفاق می‌افتد. برنامه‌نویسان وب به سراغ ابزار‌هایی رفته‌اند که برنامه‌هایشان را قبل از اینکه بخواهند در وب منتشر کنند (که با زحمت به ماژول‌ها تقسیم کرده‌اند) گرفته و تبدیل به یک فایل بزرگ کند. این ابزار را بسته‌ساز می‌نامند (bundlers).

می‌توانیم پا را فراتر بگذاریم. جدا از تعداد فایل‌ها، حجم این فایل‌ها هم در سرعت انتقالشان در شبکه تعیین‌کننده است. بنابراین، جامعه‌ی برنامه‌نویسان جاوااسکریپت، ابزار‌های فشرده ساز را اختراع کردند (minifier). این ابزار‌ها یک برنامه‌ی جاوااسکریپت را گرفته و با حذف توضیحات، فضا‌های خالی، تغییر نام متغیر‌ها و جایگزینی بعضی کد‌ها با معادل‌های کوچک‌تر، حجم آن را کاهش می‌دهند.

پس اصلا دور از انتظار نیست که کدی که در یک بسته‌ی NPM یافته‌اید یا در یک صفحه‌ی وب اجرا می‌شود پیش از آن مراحل مختلفی از تبدیل را طی کرده باشد – تبدیل از جاوااسکریپت مدرن به جاوااسکریپت قدیمی‌تر، از ماژول‌های ES به CommonJS، بسته‌بندی و فشرده شده باشد. در این کتاب به جزئیات مربوط به این ابزار نمی‌پردازیم، چون هم کسل‌کننده خواهد بود هم به سرعت تغییر می‌کنند. فقط حواستان باشد که کدی که شما معمولا اجرا می‌کنید اغلب همانی نیست که نوشته شده است.

طراحی ماژول

ساختاردهی به یک برنامه یکی از جنبه‌های حساس و ظریف از برنامه‌نویسی محسوب می‌شود. هر قطعه‌ی معناداری از قابلیت‌ها را می‌توان به اشکال مختلفی مدلسازی کرد.

طراحی یک برنامه‌ی خوب، سلیقه‌ای است – مصالحه‌هایی پیش می‌آید که سلیقه در آن دخیل است. بهترین راه برای یادگیری ارزش یک طراحی دارای ساختار خوب، این است که برنامه‌های زیادی را مورد مطالعه قرار دهید یا با آن‌ها کار کنید تا متوجه شوید که چه چیزی مفید و چه چیز نادرست است. تصور نکنید که درهم‌ریختگی و آشفتگی به هر حال پیش خواهد آمد. تقریبا هر چیزی را با صرف کمی تفکر بیشتر می‌توان با سازماندهی بهتر کرد.

یکی از جنبه‌های طراحی ماژول که باید رعایت شود، آسانی استفاده است. اگر چیزی را می‌سازید که قرار است توسط کاربران متعددی استفاده شود (یا حتی توسط خودتان، طی مدت سه ماه، زمانی که دیگر جزئیات کاری که انجام داده‌اید را به خاطر نمی‌آورید)، اگر رابط برنامه‌ی شما ساده و قابل تشخیص باشد استفاده از آن را راحت‌تر می‌کند.

ممکن است معنای آن این باشد که از قرارداد‌های موجود پیروی کنید. یک مثال خوب می‌تواند بسته‌ی ini باشد. این ماژول، کار بسته‌ی استاندارد JSON را با فراهم نمودن parse و stringify (برای نوشتن یک فایل INI) تقلید می‌کند و شبیه به JSON عمل تبدیل بین رشته‌ها و اشیاء را انجام می‌دهد. بنابراین رابط در اینجا کوچک و آشنا است و بعد از اینکه برای اولین بار از آن استفاده کنید، احتمالا روش استفاده از آن را به خاطر خواهید داشت.

حتی اگر هیچ تابع استاندارد یا بسته‌ای که به طور گسترده استفاده می‌شود برای تقلید وجود نداشت، می‌توانید با استفاده از ساختار‌های داده‌ی ساده و تمرکز بر انجام یک کار، ماژول‌های خودتان را قابل پیشبینی کنید. به عنوان مثال، خیلی از بسته‌های تجزیه‌ی فایل INI در NPM تابعی دارند که مستقیما یک فایل ini را از دیسک سخت خوانده و تجزیه می‌کند. این کار باعث می‌شود که نتوان از این ماژول‌ها در مرورگر استفاده کرد، به دلیل اینکه در مرورگر به طور مستقیم به سیستم فایل دسترسی نداریم و پیچیدگی ای اضافه می‌کند که بهتر بود با ترکیب ماژول با یک تابع خواندن فایل دیگر، حل می‌شد.

این موضوع به یکی دیگر از جنبه‌های مفیدی که در طراحی ماژول وجود دارد اشاره می‌کند – آسانی ترکیب با کد‌های دیگر. ماژول‌هایی که تمرکز روی کار خاصی دارند و مقدار‌ها را محاسبه می‌کنند در طیف گسترده‌تری از برنامه‌ها کاربرد دارند نسبت به ماژول‌های بزرگتری که کار‌های پیچیده‌ای انجام می‌دهند و دارای اثرات جانبی هستند. خواننده‌ی فایل INIای که اصرار به خواندن فایل از دیسک سخت را دارد در مواردی که محتوای فایل از دیگر منابع می‌آید کاربردی نخواهد داشت.

همچنین، اشیاء دارای وضعیت (stateful) گاهی اوقات مفید هستند یا حتی لازم‌اند اما اگر چیزی را بتوان با یک تابع انجام داد، از تابع استفاده کنید. تعدادی از بسته‌های خواننده‌ی فایل INI در NPM، سبکی را برای رابط خود استفاده کرده‌اند که ابتدا از شما می‌خواهند که یک شیء ایجاد کنید، بعد فایل مورد نظر را درون شیء بارگیری کنید و در نهایت از متد‌های خاصی برای گرفتن نتایج استفاده کنید. این کار در سنت شیء گرایی رایج است و باید گفت بسیار بد است. به جای ساختن یک فراخوانی تابع و ادامه حرکت، باید تشریفات حرکت شیء‌تان بین وضعیت‌های مختلف را انجام دهید. و به دلیل اینکه داده‌ها اکنون در یک نوع خاصی از شیء قرار گرفته‌اند، تمام کدی که با آن در ارتباط است باید آن نوع را بداند که باعث ایجاد وابستگی‌های درونی بی‌فایده می‌شود.

اغلب نمی‌توان از تعریف ساختار‌های داده‌ی جدید صرف نظر کرد – فقط تعداد کمی از موارد اساسی و پایه‌ای توسط زبان استاندارد فراهم شده است و خیلی از انواع داده می‌بایست پیچیده‌تر از یک آرایه یا یک نگاشت باشد. اما زمانی که یک آرایه کافی است، از همان استفاده کنید.

یک نمونه از ساختار داده‌های کمی پیچیده‌تر، گراف است که در فصل 7 ذکر شد. یک راه یکسان و مشخص برای نمایش یک گراف در جاوااسکریپت وجود ندارد. در آن فصل، ما از یک شیء که خاصیت‌هایش آرایه‌هایی از رشته‌ها را نگه داری می‌کردند استفاده کردیم –‌گره‌هایی که از یک‌گره قابل دستیابی بودند.

بسته‌های متعددی درباره‌ی مسیریابی در NPM وجود دارند اما هیچ کدامشان از این فرمت گراف استفاده نمی‌کنند. معمولا این بسته‌ها به یال‌های گراف امکان داشتن وزن را می‌دهند، که همان فاصله یا هزینه‌ای است که به آن‌ها اختصاص داده شده است. در نمایش ما این‌کار ممکن نیست.

به عنوان مثال، بسته‌ای به نام dijkstrajs وجود دارد. یک راه حل شناخته شده برای مسیریابی، که نسبتا شبیه به تابع findRoute ما است، الگوریتم دیکسترا است که ادسخر دیکسترا آن را نوشت. پسوند js معمولا به انتهای بسته‌ها اضافه می‌شود تا نشان دهد که آن‌ها به زبان جاوااسکریپت نوشته شده‌اند. بسته‌ی 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"]

این خود مانعی برای ترکیب‌پذیری محسوب می‌شود – زمانی که بسته‌های متنوع از ساختار‌های متفاوتی برای توصیف چیز‌های یکسان استفاده می‌کنند، ترکیب آن‌ها سخت خواهد شد. بنابراین، اگر قصد دارید ترکیب‌پذیری را هم در طراحی لحاظ کنید، توجه کنید که چه ساختار‌های داده‌ای دیگر برنامه‌نویسان استفاده می‌کنند و هر وقت ممکن شد از روش آن‌ها استفاده کنید.

خلاصه

ماژول‌ها برای برنامه‌های بزرگ‌تر، ساختار ایجاد می‌کنند و این کار را با جداسازی کد‌ها به بخش‌هایی با رابط‌ها و وابستگی‌های شفاف انجام می‌دهند. رابط بخشی از ماژول است که برای دیگر ماژول‌ها، قابل رویت است و وابستگی‌ها، ماژول‌های دیگری هستند که ماژول از آن‌ها استفاده می‌کند.

به دلیل اینکه جاوااسکریپت از ابتدا سیستم ماژول نداشت، سیستم CommonJS با استفاده از خود زبان ایجاد شد. اما بعد از مدتی، جاوااسکریپت سیستم داخلی خودش را ایجاد کرد؛ سیستمی که در کنار سیستم CommonJS وجود دارد.

یک بسته، قطعه کدی است که می‌توان آن را مستقلا توزیع کرد. NPM مخزن بسته‌های جاوااسکریپت است. می‌توانید انواع بسته‌های کاربردی (و بدون کاربرد) را از آن دانلود کنید.

تمرین‌ها

یک ربات ماژولار

موارد زیر، متغیرهایی هستند که پروژه‌ی فصل 7 ایجاد می‌کند:

roads
buildGraph
roadGraph
VillageState
runRobot
randomPick
randomRobot
mailRoute
routeRobot
findRoute
goalOrientedRobot

اگر بخواهید آن پروژه را به صورت برنامه‌ای ماژولار بنویسید، چه ماژول‌هایی ایجاد می‌کنید. کدام ماژول به یک ماژول دیگر وابستگی خواهد داشد؟ و رابط آن‌ها چگونه خواهد بود؟

کدام قسمت‌ها احتمالا قبلا نوشته شده‌اند و از NPM در دسترس هستند؟ ترجیح می‌دهید تا از بسته‌های NPM استفاده کنید یا خودتان آن‌ها را بنویسید؟

من اگر بودم این کار را می‌کردم (البته فقط یک راه درست برای طراحی یک ماژول وجود ندارد):

کدی که برای ساخت گراف مسیر استفاده می‌شود در درون ماژول graph وجود دارد. به دلیل اینکه من ترجیح می‌دهم از بسته‌ی dijkstrajs از NPM استفاده کنم تا اینکه خودم بنویسم، گراف را طوری خواهیم ساخت که داده‌هایی مناسب بسته‌ی dijkstajs تولید کند. این ماژول یک تابع صدور می‌کند، تابع buildGraph. من تابع buildGraph را طوری می‌سازم که آرایه‌ای از آرایه‌های دو عنصری را به جای رشته‌هایی که خط تیره دارند بپذیرد تا ماژول کمتر به فرمت ورودی وابسته باشد.

ماژول roads حاوی داده‌های خام مربوط به راه‌ها (آرایه‌ی roads) به همراه متغیر roadGraph می‌باشد. این ماژول به ./graph وابستگی دارد و گراف راه را صادر می‌کند.

کلاس VillageState در ماژول state قرار دارد. این کلاس به ماژول ./roads وابستگی دارد به این خاطر که نیاز دارد تا وجود راه داده شده را اعتبارسنجی کند. همچنین به randomPick نیاز دارد. چون این تابع سه خط کد بیشتر ندارد، می‌توانیم آن را درون ماژول state به عنوان یک تابع کمکی قرار دهیم. اما randomRobot هم به آن نیاز دارد. پس باید یا کد را تکرار کنیم یا یک ماژول برایش در نظر بگیریم. این تابع در NPM به نام random-item وجود دارد پس بهتر است که هر دو ماژول را به آن وابسته کنیم. می‌توانیم تابع runRobot را به این ماژول نیز اضافه کنیم چراکه هم کوچک است و هم به مدیریت وضعیت مرتبط می‌باشد. ماژول هر دوی کلاس VillageState و تابع runRobot را صادر می‌نماید.

در انتها، ربات‌ها به همراه مقادیری که به آن‌ها وابسته‌ اند مانند mailRoute را می‌توان درون یک ماژول example-robots قرار داد که خود این ماژول وابسته به ./roads می‌باشد و تابع robot را صادر می‌کند. برای اینکه امکان مسیریابی را برای goalOrientedRobot فراهم کنیم، این ماژول همچنین به dijkstrajs وابسته خواهد بود.

با سپردن بخشی از کار‌ها به ماژول‌های NPM، کد ما اندکی کوچک‌تر می‌شود. هر ماژول مجزا کاری نسبتا ساده انجام می‌دهد و مستقل عمل می‌کند. تقسیم کد به ماژول‌ها اغلب موجب بهبود‌های بیشتری در طراحی برنامه می‌شود. در این مثال، کمی غیر طبیعی به نظر می‌رسد که VillageState و ربات‌ها به یک گراف مسیر خاص وابسته هستند. احتمالا بهتر است که گراف را به عنوان یک آرگومان برای سازنده‌ی وضعیت استفاده کنیم و ربات‌ها آن را از شیء وضعیت بخوانند - که این کار وابستگی‌ها را کاهش می‌دهد (که همیشه خوب است) و امکان اجرای شبیه‌سازی روی نقشه‌های متفاوت را هم فراهم می‌کند (که عالی است).

آیا ایده‌ی خوبی است که از ماژول‌های NPM به جای چیز‌هایی که خودمان می‌توانستیم بنویسیم استفاده کنیم؟ قاعدتا بله. برای موارد مهم مثل همین مسیریابی که در صورت کدنویسی توسط خودتان، احتمال اشتباه و اتلاف وقت، زیاد است. برای تابع‌های کوچک مثل random-item، که نوشتن آن خیلی ساده است، استفاده از آن‌ها در جا‌های مختلف می‌تواند ماژول شما را شلوغ و بی‌نظم کند.

به هر حال، نباید کاری که صرف پیدا کردن یک بسته‌ی مناسب در NPM می‌شود را هم دست کم بگیرید. و حتی اگر بسته‌ی مورد نظر را پیدا کردید، ممکن است به خوبی کار نکند یا ویژگی‌های مورد نظر شما را نداشته باشد. علاوه بر آن، وابسته بودن به بسته‌های NPM به این معنا است که باید اطمینان حاصل کنید که آن‌ها نصب شده‌اند، و باید به همراه برنامه‌تان توزیع شوند، و هر از چند گاهی باید به روز‌رسانی شوند.

بنابراین یک بار دیگر، این تصمیم نوعی مصالحه و بده بستان است و شما باید میزان کمکی که این بسته‌ها به برنامه شما می‌کنند را در نظر بگیرید.

ماژول راه‌ها

ماژول CommonJS ای بنویسید که بر اساس مثال فصل 7 آرایه‌ای از راه‌ها را داشته باشد و ساختار داده‌ی گراف را به عنوان roadGraph صادر کند. باید به ماژولی به نام ./graph وابسته باشد که تابعی به نام buildGraph را صادر می‌کند که برای ساخت گراف استفاده می‌شود. این تابع نیاز به آرایه‌ای از آرایه‌های دو عنصری دارد (نقاط شروع و پایان راه‌ها).

// Add dependencies and exports

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"
];

چون این یک ماژول CommonJS است، باید از require برای ورود ماژول گراف استفاده کنید. این ماژول به صورت تابع buildGraph مشخص شده است که می‌توانید آن را از رابط شیءاش به وسیله اعلان const و روش تجزیه (destruction) مورد ارجاع قرار دهید.

برای صدور roadGraph، یک خاصیت به شیء exports اضافه می‌کنید. به دلیل اینکه buildGraph ساختار داده‌ای دریافت می‌کند که دقیقا با roads مطابقت ندارد، عمل جداسازی رشته‌ی راه‌ها باید در درون ماژول شما انجام شود.

وابستگی‌های دایره‌ای

یک وابستگی دایره‌ای در شرایطی رخ می‌دهد که ماژول A به ماژول B و ماژول B نیز مستقیم یا غیر مستقیم، به A وابسته باشد. خیلی از سیستم‌های ماژول، این کار را ممنوع کرده‌اند به دلیل اینکه هر ترتیبی که شما انتخاب کنید برای بارگیری این گونه ماژول‌ها، نمی‌توان قبل از اجرا مطمئن شد وابستگی‌های هر ماژول بارگیری شده‌اند یا خیر.

ماژول‌های CommonJS شکل محدودی از وابستگی‌های دایره‌ای را اجازه می‌دهد. تا زمانی که ماژول‌ها، اشیاء export پیشفرض‌شان را جایگزین نکنند و به رابط‌های یکدیگر اجازه‌ی دسترسی قبل از پایان بارگیری ندهند، وابستگی‌های دایره‌ای مجاز هستند.

تابع require که پیش‌تر در این فصل آمد، از این نوع از چرخه‌ی وابستگی پشتیبانی می‌کند. آیا می‌توانید توضیح دهید چگونه این کار را انجام می‌دهد؟ چه مشکلی پیش می‌آید اگر یک ماژول در یک چرخه‌، شیء export پیشفرض خود را جایگزین کند؟

نکته این است که require ماژول‌ها را پیش از آنکه شروع به بارگیری ماژول کند به حافظه‌ی نهانش اضافه می‌کند. در این صورت، اگر در حین اجرای یک require فراخوانی دیگری به ماژول صورت گیرد، تابع از پیش نسبت به آن باخبر است و رابط فعلی برگردانده می‌شود و از بارگیری مجدد و اضافی ماژول جلوگیری می‌کند ( که این بارگیری مجدد باعث رخ دادن سرریز خواهد شد).

اگر یک ماژول مقدار module.exports خود را بازنویسی کند، هر ماژول دیگری که مقدار رابط آن ماژول را قبل از پایان بارگیری‌اش دریافت کرده است، شیء رابط پیش‌فرض را نگه خواهد داشت (که احتمالا تهی است) نه مقدار رابطی که انتظارش را دارد.