۱۶. سبد خرید؛ حفظ موجودی، جمع مبلغ و مرز اعتماد
سبد خرید نمونهٔ خوبی برای یادگیری invariant است: شرطی که پس از هر تغییر باید همچنان برقرار باشد. تعداد باید عدد صحیح مثبت و در محدودهٔ موجودی باشد، هر محصول فقط یک ردیف داشته باشد و جمع مبلغ از دادهٔ معتبر محاسبه شود. در پروژهٔ محلی این قواعد را مدل اجرا میکند؛ در فروشگاه واقعی، مرجع قیمت و مجوز خرید سرور است.
چه چیزی در سبد ذخیره میشود؟
مدل همراه، برای هر ردیف id و quantity نگه میدارد. نام، قیمت و موجودی از کاتالوگ فعلی خوانده میشوند. اگر قیمت را در localStorage ذخیره و بعد همان را قطعی حساب کنیم، دادهٔ قدیمی یا دستکاریشده میتواند نتیجهٔ غلط بسازد. حتی کاتالوگ مرورگر هم در خرید واقعی مرجع قابل اعتماد نیست؛ این تصمیم فقط ساختار مثال محلی را روشن میکند.
مثالهای زیر را در فایل .mjs ریشهٔ پروژهٔ دانلودی با Node اجرا کنید. دادهها ساختگی و قیمتها توماناند. هیچ پرداخت، رزرو موجودی یا سفارش واقعی انجام نمیشود. ابتدا یک کاتالوگ معتبر میسازیم تا تفاوت دادهٔ محصول و دادهٔ سبد واضح باشد.
مثال اول؛ تغییر تعداد از مسیر واحد
import {
validateCatalog, setQuantity, cartLines, cartTotal, restoreCart
} from './projects/shop/model.mjs';
const catalog = validateCatalog([
{ id: 'book', name: 'کتاب', category: 'آموزش', price: 180000, stock: 2 },
{ id: 'pen', name: 'قلم', category: 'نوشتافزار', price: 30000, stock: 10 }
]);
let cart = [];
cart = setQuantity(cart, catalog, 'book', 2);
cart = setQuantity(cart, catalog, 'pen', 3);
console.log(cartTotal(cart, catalog)); // 450000
try {
cart = setQuantity(cart, catalog, 'book', 3);
} catch (error) {
console.log(error.message); // تعداد از موجودی بیشتر است یا معتبر نیست.
}
console.log(cart.find(row => row.id === 'book').quantity); // هنوز 2setQuantity تابعی خالص در سطح آرایهٔ خروجی است و پیش از ساخت نتیجه قواعد را بررسی میکند. سقف این مدل، حداقل موجودی محصول و ۹۹ عدد است. صفر به معنی حذف ردیف تعریف شده؛ عدد منفی، اعشاری یا بیش از سقف خطاست. همهٔ دکمههای افزایش، کاهش و حذف باید از همین قانون عبور کنند. اگر هر handler قاعدهٔ خودش را داشته باشد، رفتار رابط بهمرور ناسازگار میشود.
مقدار cart تنها پس از بازگشت موفق تابع جایگزین میشود. در نمونهٔ خطادار، سمت راست throw میکند و انتساب انجام نمیشود؛ بنابراین سبد قبلی باقی میماند. این الگو از نصفه اعمال شدن تغییر جلوگیری میکند. پیام رابط باید شکست را واضح بگوید، نه اینکه عدد روی دکمه را زیاد کند و بعداً مشکل را کشف کند.
مثال دوم؛ بازیابی با کاتالوگ تازه
const saved = { version: 1, items: cart };
console.log(restoreCart(saved, catalog).length); // 2
const changedCatalog = validateCatalog([
{ ...catalog[0], price: 200000, stock: 2 }, catalog[1]
]);
console.log(cartTotal(cart, changedCatalog)); // 490000
try {
restoreCart({ version: 1, items: [
{ id: 'book', quantity: 1 }, { id: 'book', quantity: 1 }
] }, catalog);
} catch (error) {
console.log(error.message); // ردیف سبد تکراری یا نامعتبر است.
}
console.table(cartLines(cart, changedCatalog));قیمت جدید در نمایش مجموع دیده میشود، چون سبد قیمت جدا ذخیره نکرده است. restoreCart ورودی را با کاتالوگ فعلی بررسی و شناسهٔ تکراری را رد میکند. اگر موجودی کاهش پیدا کند و تعداد قبلی نامعتبر شود، مدل خطا میدهد؛ آن را بیصدا به سفارش موفق تبدیل نمیکند. انتخاب محصول دربارهٔ اصلاح خودکار یا درخواست تأیید کاربر باید صریح باشد.
جمع امن و رابط سازگار
در مدل آماده قیمت و موجودی محدود شدهاند و cartTotal از امن بودن عدد صحیح مجموع مطمئن میشود. نمایش money فقط برای خوانایی است؛ رشتهٔ فارسی نمایشدادهشده ورودی محاسبه نیست. برای واحدهای دارای اعشار، قرارداد واحد کوچکتر پول یا محاسبهٔ دقیقتری لازم است؛ افزودن toFixed به آخر همهٔ عملیات جای طراحی مدل پول را نمیگیرد.
در app.mjs، پس از هر تغییر، renderCart جمع و ردیفها را از مدل میسازد. اگر دکمهٔ فعال با رندر حذف شود، کد تلاش میکند فوکوس را روی دکمهٔ متناظر یا عنوان سبد برگرداند. این جزئیات باعث میشوند پروژه فقط با ماوس قابل استفاده نباشد. دکمهٔ بررسی آموزشی هم صرفاً مجموع را نمایش میدهد و صریح میگوید پرداختی انجام نشده است.
تمرین مستقل و پاسخ تشریحی
تخفیف آموزشی بسازید که فقط برای مجموع حداقل ۵۰۰ هزار تومان، ده درصد با سقف ۱۰۰ هزار تومان اعمال شود. قرارداد گرد کردن را «به پایین تا تومان صحیح» قرار دهید و مبلغ نهایی را منفی نکنید. پاسخ راهنما: subtotal را از cartTotal بگیرید، شرط را بررسی کنید، discount را با Math.floor و Math.min محدود و payable را محاسبه کنید. برای ۴۹۰ هزار تخفیف صفر و برای یک میلیون تخفیف ۱۰۰ هزار است. این محاسبهٔ مرورگری صرفاً پیشنمایش است؛ در پرداخت واقعی سرور باید دوباره آن را محاسبه کند.
معیار تحویل: صفر ردیف را حذف کند؛ موجودی صفر قابل افزودن نباشد؛ تعداد اعشاری رد شود؛ سبد ذخیرهشده با شناسهٔ تکراری رد شود؛ قیمت کاتالوگ تازه در جمع دیده شود و خطا سبد معتبر قبلی را خراب نکند. هر شرط را با یک نمونهٔ کوچک ثابت کنید.
منابع انگلیسی برای مطالعهٔ تکمیلی
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isSafeInteger
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce
https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html
تألیف و مثالهای مستقل: تحریریه درسآوند.