Chapter 10ماژول‌ها

Write code that is easy to delete, not easy to extend.

Tef, Programming is Terrible
Picture of a building built from modular pieces

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

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

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

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

ماژول‌ها

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

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

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

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

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

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

بسته‌ها

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

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

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

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

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

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

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

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

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

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

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

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

بیشتر کدهایی که در 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 تخصیص یافته قرار دهند.

این ماژول به عنوان مثال، یک تابع ویرایش تاریخ فراهم می سازد (date-formatting). اینجا از دو بسته از NMP استفاده می شود – بسته‌ی ordinal برای تبدیل اعداد به رشته‌های شبیه "1st" و "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 به عنوان پارامترهای تابع پوشش دهنده‌ی تولید شده (و ارسال مقدارهای مناسب در هنگام فراخوانی)، بارگیرنده‌ اطمینان حاصل می کند که این متغیرها در قلمروی مربوط به ماژول در دسترس خواهند بود.

روشی که در آن، رشته‌ی داده شده به 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 را که در مثال آمد، بدون استفاده از براکت‌ها دور نام متغیر وارد کنید، متغیر default آن ماژول را دریافت می کنید. این گونه ماژول‌ها می توانند در کنار صدور default، متغیرهای دیگری را هم با نام‌های مختلف صادر کنند.

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

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

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

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

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

یک تفاوت مهم دیگر این است که عمل وارد کردن ماژول 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، سبکی را برای رابط خود استفاده کرده اند که ابتدا از شما می خواهند که یک شیء ایجاد کنید، بعد فایل مورد نظر را درون شیء بارگیری کنید و درنهایت از متدهای خاصی برای گرفتن نتایج استفاده کنید. این کار در سنت شیء گرایی رایج است و باید گفت بسیار بد است. به جای ساختن یک فراخوانی تابع و ادامه حرکت، باید تشریفات حرکت شیء تان بین وضعیت‌های مختلف را انجام دهید. و به دلیل اینکه داده‌ها اکنون در یک نوع خاصی از شیء قرار گرفته اند، تمام کدی که با آن در ارتباط است باید آن نوع را بداند که باعث ایجاد وابستگی‌های درونی بی‌فایده می شود.

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

یک نمونه از ساختار داده‌های کمی پیچیده تر، گراف است که در فصل 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 اش را بازنویسی کند، هر ماژول دیگری که مقدار رابط آن ماژول را قبل از پایان بارگیری‌اش دریافت کرده است، شیء رابط پیش‌فرض را نگه خواهد داشت (که احتمالا تهی است) نه مقدار رابطی که انتظارش را دارد.