۷. معماری ماژول؛ جداکردن مدل، ذخیرهسازی و رابط
وقتی همهٔ منطق داخل یک رویداد کلیک قرار دارد، تغییر ظاهر صفحه میتواند محاسبهٔ مبلغ را هم خراب کند. در سطح متوسط، مرز فایلها باید به مسئولیتها کمک کند. هدف، زیادکردن تعداد فایلها نیست؛ باید بتوانیم محاسبه را بدون مرورگر آزمایش کنیم و ذخیرهسازی را بدون دستکاری مدل تغییر دهیم.
در فایل همراه، پروژههای دفتر هزینه، فروشگاه و تیکت هرکدام مدل و کد رابط دارند. ابتدا projects/expenses/model.mjs را بخوانید: اعتبارسنجی و عملیات داده در آن قرار دارد و برای اجرا به دکمه یا ورودی HTML وابسته نیست. سپس app.mjs همان پروژه را ببینید که رویدادها، نمایش و هماهنگی با ذخیرهسازی را انجام میدهد. این جداسازی کامل و بینقص همهٔ پروژههای جهان نیست، ولی مرزهای قابل مشاهدهای برای تمرین فراهم میکند.
یک مدل کوچک بدون وابستگی به صفحه
سه فایل مثال زیر را در یک پوشهٔ مستقل بسازید. کد اول در فایل summary.mjs قرار میگیرد:
// summary.mjs
export function summarize(items) {
if (!Array.isArray(items)) throw new TypeError('Expected array');
let amount = 0;
for (const item of items) {
if (!Number.isSafeInteger(item.amount) || item.amount < 1) {
throw new RangeError('Invalid amount');
}
amount += item.amount;
if (!Number.isSafeInteger(amount)) throw new RangeError('Unsafe sum');
}
return { count: items.length, amount };
}اکنون فایل دوم را با نام check.mjs بسازید و با node check.mjs اجرا کنید:
// check.mjs
import assert from 'node:assert/strict';
import { summarize } from './summary.mjs';
assert.deepEqual(summarize([{ amount: 120000 }, { amount: 80000 }]),
{ count: 2, amount: 200000 });
assert.deepEqual(summarize([]), { count: 0, amount: 0 });
assert.throws(() => summarize([{ amount: '120000' }]), RangeError);
console.log('summary checks passed');خروجی باید summary checks passed باشد. مدل نمیداند نتیجه کجا نمایش داده میشود و آزمون هم به ساختن DOM نیاز ندارد. این استقلال باعث نمیشود تابع از هر جهت خالص باشد؛ باید خود کد را بررسی کنید. برای نمونه، خواندن زمان فعلی یا تغییر آرایهٔ ورودی میتواند پیشبینیپذیری را کم کند، حتی اگر تابع در فایل جدا باشد.
همان مدل در مرورگر
فایل سوم، index.html، کنار دو فایل قبلی قرار میگیرد. آن را با یک سرور محلی قابل اعتماد اجرا کنید. ماژول مرورگر را با مسیر نسبی و type="module" بارگذاری میکنیم:
<!doctype html>
<html lang="fa" dir="rtl">
<meta charset="utf-8">
<title>آزمایش گزارش</title>
<p id="summary"></p>
<script type="module">
import { summarize } from './summary.mjs';
const result = summarize([{ amount: 120000 }, { amount: 80000 }]);
document.querySelector('#summary').textContent =
`${result.count} ردیف، مجموع ${result.amount} تومان`;
</script>
</html>باید «2 ردیف، مجموع 200000 تومان» دیده شود. فایل check.mjs را در مرورگر وارد نکنید، چون node:assert/strict مربوط به محیط Node.js است. استقلال مدل زمانی مفید است که وابستگی به محیط را در لایههای مناسب نگه داریم. برای این مثال کوچک، ابزار build لازم نیست؛ در پروژهٔ بزرگتر، انتخاب ابزار باید نیاز واقعی را پاسخ دهد.
تزریق وابستگی با یک تابع ساده
آزمون تابعی که مستقیماً localStorage را میخواند، به وضعیت مرورگر وابسته میشود. میتوان در مرز بیرونی، شیء ذخیرهساز را به آن داد. این مثال مستقل در Node.js هم اجرا میشود:
function writeDraft(storage, title) {
if (typeof title !== 'string') throw new TypeError('Expected title');
storage.setItem('draft-title', title);
}
const memory = new Map();
const fakeStorage = {
setItem(key, value) { memory.set(key, value); }
};
writeDraft(fakeStorage, 'پرسش دربارهٔ تمرین');
console.log(memory.get('draft-title')); // پرسش دربارهٔ تمریناین «ذخیرهساز آزمایشی» همهٔ رفتار مرورگر را شبیهسازی نمیکند، اما برای بررسی این قرارداد کوچک کافی است. در آزمون خطا، setItem را طوری تعریف کنید که خطا پرتاب کند و انتظار برنامه را بنویسید. تزریق وابستگی الزاماً به چارچوب، container یا تنظیمات پیچیده نیاز ندارد؛ یک پارامتر روشن میتواند کافی باشد.
یک قرارداد مفید برای مدل این است که ورودی و خروجی آن از دادههای ساده ساخته شود. اگر مدل نیاز به تولید شناسه یا خواندن زمان دارد، میتوان این مقدارها یا تابع تولیدکننده را از بیرون به آن داد. در نتیجه آزمون با یک زمان و شناسهٔ ثابت تکرار میشود. البته لازم نیست همهٔ وابستگیها را برای برنامهای کوچک انتزاعی کنید؛ وابستگیای را جدا کنید که واقعاً آزمون یا تغییر مستقل را دشوار کرده است.
هنگام import، مسیر نسبی را از محل فایل واردکننده حساب کنید، نه از صفحهای که در ذهن دارید. اگر انتقال فایل باعث خطای پیدا نشدن ماژول شد، نخست همین مسیر و پسوند را بررسی کنید. خطای مسیر با خطای منطق جمع یکی نیست و نباید با تغییر تابع محاسبه برطرفش کرد. به همین دلیل اجرای آزمون پس از هر جابهجایی کوچک، بازآرایی را قابل کنترلتر میکند.
تمرین و پاسخ تشریحی
از برنامهٔ خود تابع مجموع، اعتبارسنجی ردیف و قالببندی متن را به فایلهای دارای مسئولیت روشن منتقل کنید. مراقب باشید برای هر تابع یک فایل بیدلیل نسازید. سپس نمودار سادهای روی کاغذ بکشید: رابط از مدل استفاده میکند، ولی مدل نباید برای انجام جمع، رابط را import کند. یک چرخهٔ وابستگی میتواند فهم ترتیب راهاندازی را سخت کند.
پاسخ راهنما: نخست آزمون رفتار فعلی را ثبت کنید، سپس جابهجایی فایل را انجام دهید و دوباره آزمون بگیرید. منطق محاسبه را هنگام جابهجایی همزمان تغییر ندهید تا علت شکست روشن بماند. معیار تحویل، اجرای آزمون مدل در Node.js، اجرای رابط در مرورگر و نداشتن دسترسی DOM در مدل است. در بازبینی، توضیح دهید کدام وابستگی به محیط عمداً در لایهٔ بیرونی باقی مانده و چرا.
منابع رسمی برای مطالعهٔ تکمیلی
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
https://nodejs.org/api/assert.html
تألیف و مثالهای اختصاصی: تحریریه درسآوند.