5 مهر 1402
تهران، خیابان آزادی، تقاطع قریب
برنامه نویسی نرم افزار

9 دیزاین پترن در جاوا اسکریپت که دوستشان خواهید داشت

جاوا اسکریپت

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

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

بزرگترین مزایای دیزاین پترن‌ها

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

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

آن‌ها گویا هستند: دیزاین پترن‌ها می‌توانند یک راه حل پیچیده را به زیبایی توصیف کنند.

نیاز کمتری به Refactoring دارند: هنگامیکه در ذهنتان با دیزاین پترن‌ها برنامه‌ای را می‌نویسید، آسان‌تر و سریع‌تر می‌توانید به یک کد تمیز برسید. به همین ترتیب و به‌خصوص در جاوا اسکریپت، Refactor های کمتری می‌سازیم؛ زبانی که در آن برای نوشتن یک چیز روش‌های زیادی وجود دارد.

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

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

برخی از ویژگی‌های مهم جاوا اسکریپت

ما نیاز به نگاهی نزدیک‌تر به برخی از جنبه‌های این زبان داریم که ما در پیاده‌سازی پترن‌ها کمک می‌کند. ما می‌توانیم جاوا اسکریپت را این‌گونه تعریف کنیم:

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

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

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

جاوا اسکریپت از توابع درجه یک پشتیبانی می کند

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

جاوا اسکریپت بر اساس نمونه‌های اولیه است

مانند هر زبان شیء‌گرای دیگر، جاوا اسکریپت از اشیاء پشتیبانی می‌کند. زمانی که به شیء اشاره می‌کنیم، بلافاصله فکرمان به سمت کلاس‌ها و وراثت (inheritance) می‌رود. اینجاست که همه چیز عجیب می‌شود. در اصل، جاوا اسکریپت قابلیت پشتیبانی از کلاس‌ها را ندارد و همچنان از وراثت بر اساس نمونه‌های اولیه (Prototypes) استفاده می‌کند.

برنامه‌نویسی مبتنی بر Prototype سبکی است که با استفاده مجدد از اشیاء موجود که توسعه نیافته و اجرا نشده اند، ایجاد وراثت می‌کند. این را در مثال‌های دیزاین پترن خواهیم دید. این ویژگی در بسیاری از پترن‌ها واقعاً مهم است.

حلقه های رویداد (Event Loops) در جاوا اسکریپت

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

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

حالت اجرا تا تکمیل

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

				
					while (queue.waitForMessage()) {
    queue.processNextMessage();
}

				
			

queue.processNextMessage() منتظر پیام‌های جدید به شیوه‌ای هماهنگ است. هر یک از پیام‌های در حال پردازش دارای پشته (stack) خاص خود است و تا زمانی که پشته خالی نشود، پردازش نمی‌شود. هنگامی که تمامی فرآیند‌ها به پایان برسد، یک پیام جدید از صف خوانده می‌شود. این کار تا زمانی که پیام‌هایی در صف وجود دارند ادامه می‌یابد.

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

دیزاین پترن‌ها چه هستند؟

دیزاین پترن‌ها معمولاً راه حل‌هایی برای مشکلات رایج در برنامه نویسی هستند.

پروتو-پترن

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

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

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

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

آنتی-پترن

همان‌طور که یک دیزاین پترن نشان‌دهنده تمرین خوب است، یک آنتی‌پترن نیز نشان‌دهنده تمرین بد است.

تغییر نمونه اولیّه (prototype) آبجکت (شیء)، یک مثال خوب از آنتی-پترن در جاوا اسکریپت است. (به یاد داشته باشید که جاوا اسکریپت از یک وراثت مبتنی بر prototype استفاده می‌کند.) بنابراین تصور کنید که این نمونه اولیه را تغییر داده اید. تغییرات درهر شیئی که مورد وراثت قرار گرفته دیده می‌شود، که تقریباً شامل تمام آبجکت‌های جاوا اسکریپت می‌شود. یک فاجعه بزرگ در شرف وقوع است!

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

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

کتگوری‌های دیزاین پترن

دیزاین پترن‌ها را می‌توان به روش‌های مختلفی دسته بندی کرد که در اینجا محبوب‌ترین آن‌ها معرفی می‌شوند:

  • خلاقانه
  • ساختاری
  • بر اساس الگوی رفتاری
  • همزمان (Concurrency)
  • به منظور معماری سیستم (Architectural)

دیزاین‌ پترن‌های خلاقانه

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

دیزاین پترن‌های ساختاری

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

دیزاین پترن‌های بر اساس الگوی رفتاری

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

دیزاین پترن‌های همزمان (Concurrency)

هنگامی که با برنامه نویسی چند نخی سر و کار دارید، این پترن‌ها برایتان مفید و قابل استفاده خواهند بود.

دیزاین پترن‌های معماری سیستم (Architectural)

شامل آن‌دسته از دیزاین‌ پترن‌ها می‌شود که در معماری سیستم مورد استفاده قرار می‌گیرند. مانند MVC یا MVVM.

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

نمونه‌هایی از 9 دیزاین پترن مورد علاقه من

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

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

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

  • کانستراکتور پترن (Constructor Pattern)

وقتی به پیاده‌سازی کلاسیک زبان‌های شیء‌گرا فکر می‌کنیم، کانستراکتور یک تابع ویژه است که مقادیر متغیر کلاس را به به اندازه پیش فرض یا اندازه اولیه، مقداردهی می‌کند.

سه روش متداول برای ایجاد یک آبجکت در جاوا اسکریپت عبارت اند از:

				
					// All of these forms can be used to create
// an object in JavaScript with the same result
let instance = {};
// or
let instance = Object.create(Object.prototype);
// or
let instance = new Object();

				
			

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

				
					// The . notation
instance.key = 'value';
 
// brackets notation
instance['key'] = 'value';
 
// defining a property with Object.defineProperty
Object.defineProperty(instance, 'key', {
    value: 'value',
    writable: true,
    enumerable: true,
    configurable: true
});
 
// defining multiple properties with Object.defineProperties
Object.defineProperties(instance, {
    'firstKey': {
        value: 'first key value',
        writable: true
    },
    'secondKey': {
        value: 'second key value',
        writable: false
    }
});

				
			

رایج ترین راه برای ایجاد آبجکت، استفاده از {} و برای افزودن properties، استفاده از . (نقطه) یا [] است. به همین دلیل است که توصیه می‌کنیم از این متدها استفاده کنید. این کار باعث می‌شود برنامه‌نویس‌های دیگر و حتی خود شما در آینده، کد را خیلی آسان‌تر بفهمند و بفهمید.

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

				
					// We define a constructor for objects of type Person
function Person (name, age, isDeveloper) {
    this.name = name;
    this.age = age;
    this.isDeveloper = isDeveloper || false;
 
    this.writesCode = () => {
        console.log(this.isDeveloper ? 'this person writes code' : 'this person does not writes code')
    }
}
 
// Create a person with: name = Ana, age = 32,
// isDeveloper = true and a method writesCode
let person1 = new Person('Ana', 32, true);
 
// Create a person with: name = Bob, age = 36,
// isDeveloper = false and a method writesCode
let person2 = new Person('Bob', 36);
 
// prints: this person writes code
person1.writesCode();
// prints: this person does not writes code
person2.writesCode();

				
			

ما هنوز هم می‌توانیم این کد را بهتر کنیم. مشکل این است که متد writesCode برای هر نمونه از Person دوباره تعریف می‌شود. از آنجا که جاوا اسکریپت مبتنی بر نمونه اولیه (prototype) است، می‌توانیم با افزودن متد به نمونه اولیه از این اتفاق جلوگیری کنیم.

				
					// We define a constructor for Person
function Person(name, age, isDeveloper) {
    this.name = name;
    this.age = age;
    this.isDeveloper = isDeveloper || false;
}
 
// Then we extend the prototype, this way we make JavaScript
// point to this function when we call it on a Person
Person.protype.writesCode = function() {
        console.log(this.isDeveloper ? 'this person writes code' : 'this person does not writes code')
    }
}
 
// Create a person with: name = Ana, age = 32,
// isDeveloper = true and a method writesCode
let person1 = new Person('Ana', 32, true);
 
// Create a person with: name = Bob, age = 36,
// isDeveloper = false and a method writesCode
let person2 = new Person('Bob', 36);
 
// prints this person writes code
person1.writesCode();
// prints this person does not writes code
person2.writesCode();

				
			

اکنون، هر دو مورد Personها، می‌توانند به یک نمونه مشترک از wrutesCode() دسترسی داشته باشند.

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

ماژول پترن (Module Pattern)

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

قبل از اینکه وارد جزئیات ماژول‌ پترن‌ها شویم، اجازه دهید نگاهی به کلوژرها (closures) بیاندازیم. Closure تابعی است که حتی پس از پایان کار والد،  به محدوده والد (parent’s scope) خود دسترسی دارد. این توابع به ما کمک می‌کنند تا رفتار محدودکنندگان دسترسی را تقلید کنیم. به مثال زیر توجه کنید:

				
					// We use a immediately invoked function to create
// a private variable counter
var counterIncrementer = (() => {
    let counter = 0;
    return function() {
        return ++counter;
    };
})(); // these () in the end make this a immediately invoked function

// prints: 1
console.log(counterIncrementer());
// prints: 2
console.log(counterIncrementer());
// prints: 3
console.log(counterIncrementer());

				
			

همان‌طور که می‌بینید، ما شمارنده counter را به تابعی گره زدیم که فراخوانی و بسته شده است، اما همچنان می‌توانیم از طریق تابع فرزند که counter را  توسعه و افزایش می‌دهد، به آن دسترسی داشته باشیم. توجه داشته باشید که تابع داخلی از سمت counterIncrementer() بازگشت داده می‌شود. ما نمی‌توانیم از بیرون تابع به counter دسترسی پیدا کنیم. ما اساساً یک متغیر پریوت را از طریق دستکاری در scope، در vanilla JavaScript ایجاد کرده ایم.

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

				
					// Using a closure we will expose an object
// as part of a public API that manages its
// private parts
let fruitsCollection = (() => {
    // private
    let objects = [];
 
    // public
    return {
        addObject: (object) => {
            objects.push(object);
        },
        removeObject: (object) => {
            let index = objects.indexOf(object);
            if (index >= 0) {
                objects.splice(index, 1);
            }
        },
        getObjects: () => JSON.parse(JSON.stringify(objects))
    };
})(); // notice the execution
 
fruitsCollection.addObject("apple");
fruitsCollection.addObject("orange");
fruitsCollection.addObject("banana");
 
// prints: ["apple", "orange", "banana"]
console.log(fruitsCollection.getObjects());
 
fruitsCollection.removeObject("apple");
 
// prints: ["orange", "banana"]
console.log(fruitsCollection.getObjects());

				
			

بزرگترین کاربرد این پترن ایجاد یک مرز جداکننده واضح بین بخش عمومی و پریویت یک آبجکت است.

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

ماژول پترن آشکارگر (Revealing Module Pattern)

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

				
					// We write the whole logic as private members
// and expose an anonymous object that maps the
// methods we want as their public counterparts
let fruitsCollection = (() => {
    // private
    let objects = [];
 
    const addObject = (object) => {
        object.push(object);
    }
 
    const removeObject = (object) => {
        let index = objects.indexOf(object);
        if (index >= 0) {
            objects.splice(index, 1);
        }
    }
 
    const getObjects = () => JSON.parse(JSON.stringify(objects))
 
    // public
    return {
        addName: addObject,
        removeName: removeObject,
        getNames: getObjects
    };
})();
 
fruitsCollection.addName("Bob");
fruitsCollection.addName("Alice");
fruitsCollection.addName("Frank");
 
// prints: ["Bob", "Alice", "Frank"]
console.log(namesCollection.getNames());
 
namesCollection.removeName("Alice");
 
// prints: ["Bob", "Frank"]
console.log(namesCollection.getNames());


				
			

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

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

سینگلتون پترن (Singleton Pattern)

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

				
					let configurationSingleton = (() => {
    // private value of the singleton initialized only once
    let config;
 
    const initializeConfiguration = (values) => {
        this.randomNumber = Math.random();
        values = values || {};
        this.number = values.number || 5;
        this.size = values.size || 10;
    }
 
    // We export the centralized method to return
    // the singleton's value
    return {
        getConfig: (values) => {
            // initialize the singleton only once
            if (config === undefined) {
                config = new initializeConfiguration(values);
            }
 
            // and always return the same value
            return config;
        }
    };
})();
 
const configObject = configurationSingleton.getConfig({ "size": 8 });
// prints number: 5, size: 8, randomNumber: someRandomDecimalValue
console.log(configObject);
 
const configObject1 = configurationSingleton.getConfig({ "number": 8 });
// prints number: 5, size: 8, randomNumber: same randomDecimalValue // como no primeiro config
console.log(configObject1);

				
			

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

آبسرور پترن (پترن ناظر یا Observer Pattern)

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

چندین راه مختلف برای پیاده سازی این پترن وجود دارد، اما حالت ساده‌تر زمانی است که ما یک فرستنده emitter)) و تعداد زیادی ناظر (observer) داریم.

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

الگوی منتشرکننده/سابسکرایبر

 یکی از انواع مختلف این پترن، الگوی منتشرکننده/سابسکرایبر است، که در ادامه متن آن را خواهیم دید.

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

مثال کوچک زیر درباره پترن منتشرکننده/سابسکرایبر را ببینید:

				
					let publisherSubscriber = {};
 
// We pass an object to the container to manage subscriptions
((container) => {
    // the id represents a subscription to the topic
    let id = 0;
 
    // the objects will subscribe to a topic by
    // sending a callback to be executed when
    // the event is fired
    container.subscribe = (topic, f) => {
        if (!(topic in container)) {
            container[topic] = [];
        }
 
        container[topic].push({
            'id': ++id,
            'callback': f
        });
 
        return id;
    }
 
    // Every subscription has it's own id, we will
    // use it to remove the subscription
    container.unsubscribe = (topic, id) => {
        let subscribers = [];
        for (car subscriber of container[topic]) {
             if (subscriber.id !== id) {
                subscribers.push(subscriber);
            }
        }
        container[topic] = subscribers;
    }
    container.publish = (topic, data) => {
        for (var subscriber of container[topic]) {
            // when we execute a callback it is always
            // good to read the documentation to know which
            // arguments are passed by the object firing
            // the event
            subscriber.callback(data);
        }
    }
})(publisherSubscriber);
 
let subscriptionID1 = publisherSubscriber.subscribe("mouseClicked", (data) => {
    console.log("mouseClicked, data: " + JSON.stringify(data));
});
let subscriptionID2 = publisherSubscriber.subscribe("mouseHovered", function(data) {
    console.log("mouseHovered, data: " + JSON.stringify(data));
});
let subscriptionID1 = publisherSubscriber.subscribe("mouseClicked", function(data) {
    console.log("second mouseClicked, data: " + JSON.stringify(data));
});
 
// When we publish an event, all callbacks should
// be called and you will see three logs
publisherSubscriber.publish("mouseClicked", {"data": "data1"});
publisherSubscriber.publish("mouseHovered", {"data": "data2"});
 
// We unsubscribe an event
publisherSubscriber.unsubscribe("mouseClicked", subscriptionID3);
 
// now we have 2 logs
publisherSubscriber.publish("mouseClicked", {"data": "data1"});
publisherSubscriber.publish("mouseHovered", {"data": "data2"});

				
			

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

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

مدیاتور پترن (الگوی میانجی یا Mediator Pattern)

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

در زندگی واقعی نیز، میانجی شیئی است که نقطه کانونی ارتباط میان اشیاء دیگر خواهد بود.

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

ویزارد کردن

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

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

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

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

پروتوتایپ پترن (الگوی نمونه اولیه یاPrototype Pattern)

حتماً از خواندن این مطلب خسته شده اید، اما جاوا اسکریپت از کلاس‌ها در فرم Nativeشان پشتیبانی نمی‌کند. وراثت از طریق نمونه‌های اولیه (Prototypes) ساخته می‌شود. پس بخوانید!

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

بیایید نمونه‌ای از این پترن را ببینیم:

				
					let personPrototype = {
    sayHi: () => {
        console.log("Hi, I am " + this.name + ", and I have " + this.age);
    },
    sayBye: () => {
        console.log("Bye!");
    }
};
const Person = (name, age) => {
    let name = name || "John Doe";
    let age = age || 26;
 
    const constructorFunction = (name, age) => {
        this.name = name;
        this.age = age;
    };
 
    constructorFunction.prototype = personPrototype;
 
    let instance = new constructorFunction(name, age);
    return instance;
}
 
const person1 = Person();
const person2 = Person("Jane Doe", 38);
 
// prints Hi, I am John Doe, and I have 26
person1.sayHi();
// prints Hi, I am Jane Doe, and I have 38
person2.sayHi();

				
			

توجه داشته باشید که وراثت از نمونه‌های اولیه، منجر به بهبود عملکرد نیز می‌شود، زیرا هر دو آبجکت، به جای اینکه جداگانه پیاده سازی شوند، به یک متد یکسان که بر روی نمونه اولیه پیاده سازی شده است، ارجاع داده می‌شوند.

کاماند پترن (پترن فرمان یا Command Pattern)

از الگوی فرمان زمانی استفاده می‌شود که بخواهیم یک فراخوانی (call) را به عنوان یک آبجکت کپسول‌گذاری کنیم. این راهی برای جدا نگه داشتن مفاد فراخوانی (caller’s context) از خود فراخوانی (call) است.

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

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

مشکلی که این الگو ایجاد می‌کند این است که یک لایه انتزاعی اضافی ایجاد می‌کند و این ممکن است بر عملکرد یک برنامه تأثیر بگذارد. مهم است که نحوه تعادل بخشی به عملکرد و قابلیت خوانش کد را بدانید.

				
					// The object that knows how to execute the command
const invoker = {
    add: (x, y) => {
        return x + y;
    },
    subtract: (x, y) => {
        return x - y;
    }
}
 
// the object to be used as abstraction layer when
// we execute commands; it represents a interface
// to the caller object
let manager = {
    execute: (name, args) => {
        if (name in invoker) {
            return invoker[name].apply(invoker, [].slice.call(arguments, 1));
        }
        return false;
    }
}
// prints 8
console.log(manager.execute("add", 3, 5));
// prints 2
console.log(manager.execute("subtract", 5, 3));

				
			

پترن نَما (Facade Pattern)

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

به عنوان مثال، از این پترن در انتخابگرهای کتابخانه‌ای DOM به عنوان JQuery، Dojo و D3 استفاده می شود. این فریم ورک‌ها دارای انتخابگرهای قدرتمندی هستند که به ما امکان می‌دهند querieهای پیچیده را به روشی بسیار ساده بنویسیم. چیزی شبیه jQuery(.”parent .child div.span”) . ساده به نظر می رسد، اما یک منطق پیچیده querie را در زیرلایه‌های خود پنهان می کند.

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

نتیجه گیری

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

این پست می‌تواند طولانی‌تر هم بشود. اگر می‌خواهید درباره دیزاین پترن‌ها بیشتر بدانید، مطالعه‌ کتاب کلاسیک Design Patterns: Elements of Reusable Object-Oriented Software از گروه چهار (Gang of four) و همچنین کتاب مدرن‌تر Learning JavaScript Design Patterns نوشته شده توسط Addy Osmani را توصیه می‌کنم. هر دو کتاب واقعاً خوب هستند و ارزش خواندن را دارند.

ممنون که این پست طولانی را تا اینجا خواندید.

جاوا اسکریپت

Leave feedback about this

  • کیفیت
  • قیمت
  • خدمات

PROS

+
Add Field

CONS

+
Add Field
Choose Image
Choose Video
X