توسعه فرانت‌اند

JavaScript Design Patterns: الگوهای طراحی کاربردی

Factory و Strategy با نمونه کامل محاسبه محلی، مرز Module و Observer و Singleton؛ انتخاب الگو بر اساس نیاز و عمر داده.

1404/12/01 2 دقیقه مطالعه 1,005 بازدید

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

الگوهای طراحی واژگان مفیدی برای مسئله‌های تکرارشونده‌اند. در JavaScript، بعضی از آن‌ها با تابع و closure ساده بیان می‌شوند و لزوماً به classهای متعدد نیاز ندارند. استفاده از الگو زمانی قابل دفاع است که یک وابستگی، تکرار یا مرز نامعلوم را روشن کند. افزودن Factory و Observer به هر فایل فقط برای شبیه شدن به معماری مشهور، ممکن است مسیر خواندن را طولانی کند.

مسئله را قبل از نام الگو بنویسید

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

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

Factory می‌تواند یک تابع ساده باشد

Factory ساختن object را در یک مسیر مشخص نگه می‌دارد. این مسیر می‌تواند داده اولیه را بررسی کند و وابستگی لازم را دریافت کند. لازم نیست هر Factory یک class جدا داشته باشد. اگر چند نمونه باید state مستقل داشته باشند، closure می‌تواند آن جداسازی را فراهم کند. با این حال، پنهان شدن state به معنی تغییرناپذیری همه خروجی‌ها نیست؛ referenceهای بازگردانده‌شده هم باید قرارداد مشخصی داشته باشند.

برای مدل پیچیده، object ساخته‌شده باید عملیات قابل فهمی ارائه کند. فهرست بزرگی از setterهای عمومی ممکن است اعتبار داده را از بین ببرد. اگر خروجی فقط داده ساده است، تبدیل آن به یک شیء پررفتار هم لزوماً لازم نیست. تصمیم را با عمر و نیاز تغییر بگیرید. مرز ساخت، زمان مصرف و شیوه نگهداری باید برای خواننده روشن باشد تا Factory به محل جمع شدن کارهای نامرتبط تبدیل نشود.

Strategy برای قانون قابل تعویض

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

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

نمونه کامل Factory و Strategy

کد را در pricing-demo.js ذخیره و با Node.js جدید مانند نسخه 24 و فرمان node pricing-demo.js اجرا کنید. هیچ بسته، API یا فایل دیگری لازم نیست. createPricing یک نمونه مستقل با قانون هزینه مشخص می‌سازد. دو مورد با مقدارهای ۱۵۰۰ و ۳۰۰۰ اضافه می‌شوند؛ قانون نمونه برای جمع کمتر از ۵۰۰۰ هزینه ۵۰۰ می‌دهد. خروجی شامل جمع، هزینه و کل است.

function createPricing(shippingRule) {
  if (typeof shippingRule !== 'function') throw new TypeError('A shipping rule is required.');
  const items = [];
  return {
    add(item) {
      if (!item || typeof item.title !== 'string' || !item.title.trim()
        || !Number.isSafeInteger(item.amount) || item.amount < 0) throw new TypeError('Invalid item.');
      items.push({ title: item.title.trim(), amount: item.amount });
    },
    snapshot() {
      const subtotal = items.reduce((sum, item) => sum + item.amount, 0);
      if (!Number.isSafeInteger(subtotal)) throw new RangeError('Subtotal is too large.');
      const shipping = shippingRule(subtotal);
      if (!Number.isSafeInteger(shipping) || shipping < 0 || !Number.isSafeInteger(subtotal + shipping)) {
        throw new RangeError('Invalid shipping result.');
      }
      return { items: items.map(item => ({ ...item })), subtotal, shipping, total: subtotal + shipping };
    }
  };
}
const pricing = createPricing(subtotal => subtotal >= 5000 ? 0 : 500);
pricing.add({ title: 'First educational item', amount: 1500 });
pricing.add({ title: 'Second educational item', amount: 3000 });
console.log(pricing.snapshot());

داده ورودی به شکل تازه وارد فهرست داخلی می‌شود و snapshot نیز کپی موردها را برمی‌گرداند. این کار جلوی تغییر مستقیم همین فیلدهای ساده از بیرون را می‌گیرد؛ کپی سطحی به معنی deep immutability در مدل‌های تو در تو نیست. جمع و هزینه از نظر عدد صحیح امن بررسی می‌شوند. اگر strategy نتیجه نامناسب بدهد، خطای روشن ایجاد می‌شود و رقم نادرست به عنوان قیمت معتبر گزارش نمی‌شود.

Module مرز import و export می‌دهد

راهنمای Modules در MDN قرارداد import و export را توضیح می‌دهد. Module می‌تواند مرز فایل و API عمومی بسازد، اما صرف تقسیم کد به فایل‌ها مسئولیت‌ها را درست جدا نمی‌کند. یک فایل که همه چیز را export می‌کند هنوز وابستگی‌های گسترده دارد. بخش عمومی باید آن چیزی باشد که مصرف‌کننده برای کار نیاز دارد و جزئیات داخلی بی‌دلیل به قرارداد تبدیل نشوند.

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

Observer باید مسیر خروج داشته باشد

Observer یا سازوکار event برای خبر دادن تغییر به مصرف‌کنندگان مفید است. ناشر نمی‌خواهد همه شنونده‌ها را مستقیم بشناسد، ولی نام event و شکل داده باید قرارداد باشند. در مرورگر، مرجع addEventListener نحوه ثبت و بعضی راه‌های کنترل عمر را توضیح می‌دهد. ثبت listener بدون توجه به حذف آن می‌تواند واکنش تکراری یا نگهداری reference ناخواسته ایجاد کند.

اگر هر بار باز شدن یک پنل listener تازه اضافه شود، ممکن است یک کلیک چند بار عمل کند. مسیر unsubscribe یا signal باید متناسب با عمر مؤلفه باشد. خطای یکی از شنونده‌ها و ترتیب خبر دادن هم نیاز به تصمیم دارند. event را برای پنهان کردن همه وابستگی‌ها استفاده نکنید؛ اگر نتیجه فوری یک عمل لازم است، فراخوانی تابع مستقیم شاید قابل فهم‌تر باشد. ارتباط غیرمستقیم هزینه پیگیری دارد.

Singleton دامنه دارد

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

Singleton می‌تواند آزمون و reset وضعیت را سخت کند چون وابستگی از امضای تابع دیده نمی‌شود. دریافت dependency به‌صورت پارامتر یا ساخت نمونه جدا ممکن است روشن‌تر باشد. در نمونه REST API داده حافظه‌ای عمداً محدود معرفی شده است؛ تبدیل آن به singleton مشکل ذخیره پایدار را حل نمی‌کند. الگو باید دامنه واقعی محیط را بیان کند و ادعای گسترده‌تری نسازد.

الگوهای React را با رفتار مؤلفه بسنجید

Container و Presentational می‌توانند دریافت داده را از نمایش جدا کنند، اما در هر پروژه بهترین مرز نیستند. HOC، Render Props و Compound Components قراردادهای ترکیب متفاوت دارند. اگر یک hook یا prop ساده نیاز را برآورده می‌کند، ساخت چند wrapper ممکن است خواندن و نام‌گذاری را دشوار کند. انتخاب باید با استفاده واقعی، تعداد حالت‌ها و مسئولیت state هماهنگ باشد.

در Compound Components، رابطه فرزندها و context باید روشن باشد. آزادی ترکیب نباید باعث شود نبودن یک جزء ضروری بی‌خبر رفتار را خراب کند. برای قرارداد قابل مشاهده، راهنمای Testing Library نمونه‌ای از بررسی رفتار مؤلفه دارد. آزمون باید نتیجه برای مصرف‌کننده را بسنجد، نه اینکه نام یک pattern در فایل وجود دارد یا تعداد لایه‌ها به یک الگوی از پیش تعیین‌شده رسیده است.

Adapter و قرارداد داده

اگر دو سرویس شکل داده متفاوت دارند، Adapter می‌تواند مدل مورد نیاز مصرف‌کننده را ارائه کند. این تبدیل باید خطا، داده خالی و تفاوت معنی را نیز در نظر بگیرد. یک rename ساده وقتی واحد یا زمان متفاوت است کافی نیست. قرارداد داخلی را از شکل اتفاقی یک API بیرونی جدا کنید، اما اطلاعات ضروری را در تبدیل از دست ندهید. توضیح mapping به نگهداری بعدی کمک می‌کند.

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

الگوی لازم را مستند کنید

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

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

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!