الگوهای طراحی نرم افزار در جاوا اسکریپت — از صفر تا صد
فرض کنید که در زمان کار روی یک پروژه نرمافزاری بارها و بارها با مسئله مشخصی مواجه میشوید و هر بار نیز یک راهحل برای آن توسعه میدهید. این وضعیت در نهایت ملالآور میشود و حتی ممکن است برای اطرافیان شما سردرگمکننده شود، زیرا امکان بهینهسازی آن وجود نخواهد داشت. به جای این کار میتوانیم از الگوهایی برای حل مسئله استفاده کنیم. در زندگی روزمره این موضوع را «میانبر ذهنی» (Heuristics) و در صنعت نرمافزار آن را «الگوهای طراحی» مینامیم. در این مقاله به بررسی الگوهای طراحی نرم افزار در جاوا اسکریپت میپردازیم.
منظور از الگوی طراحی در صنعت نرمافزار، راهحلی برای یک مسئله است که به طور مکرر رخ میدهد و به طور کلی توافق وجود دارد که بهترین راهحل است. درک الگوهای طراحی موجب میشود که از مزیتهای زیر بهرهمند شویم:
- حل مسائل با رخداد متواتر، آسانتر و سریعتر میشود.
- امکان حل مسائل با راهحلهای ازپیشبهینهسازیشده فراهم میآید.
- میتوان کدی نوشت که برای افرادی که آن الگوی طراحی را درک میکنند، آشنا باشد.
الگوهای آفرینشی
در این بخش الگوهای «آفرینشی» (Creational) را معرفی میکنیم.
الگوی سازنده
مسئله: به روشی برای ایجاد یک وهله از یک شیء نیاز داریم تا بتوانیم کارکردش را بسط دهیم.
الگوی «سازنده» (Constructor) در جاوا اسکریپت از کلیدواژه new برای ساخت یک شیء جدید از یک تابع استفاده میکند. این شیء ساخته شده جدید به پروتوتایپ شیء لینک میشود، کلیدواژه this به دامنه این شیء جدید متصل میشود و به صورت صریح this را بازگشت میدهد.
1function Person({ firstName, lastName, email }) {
2 this.firstName = firstName;
3 this.lastName = lastName;
4 this.email = email;
5 this.name = this.firstName + ' ' + this.lastName;
6
7 this.sayHello = function() {
8 return console.log(`Hello my name is ${this.firstName}`);
9 };
10
11 // return this; - The constructor pattern returns this implicitly. There is no need to return it explicitly here.
12}
13
14const Bob = new Person({
15 firstName: 'Bob',
16 lastName: 'doe',
17 email: 'test@example.com'
18});
19
20console.log(`Bob's full name is ${Bob.name}`);
21
22Bob.sayHello();
جاوا اسکریپت برای بسط بیشتر کارکرد شیء از وراثت مبتنی بر پروتوتایپ نیز بهره میگیرد.
1Bob.sayHello();
2
3// Extend Bob's functionality with JavaScript's prototype-based inheritance
4Bob.__proto__.sayEmail = function() {
5 return console.log(`My email address is ${this.email}`);
6};
در نسخه ECMAScript 2015 از جاوا اسکریپت کلاسهایی برای ایجاد نسخههای متفاوت از وراثت مبتنی بر پروتوتایپ موجود اضافه شدهاند.
الگوی ماژول
الگوی «ماژول» (Module) برای گروهبندی تعدادی متد که به یک خانواده تعلق دارند، مورد استفاده قرار میگیرد.
مزیت عمده الگوی ماژول در افزایش قابلیت نگهداری، کاهش آلودگی فضای نام و همچنین در صورت داشتن طراحی خوب، در قابلیت بالای استفاده مجدد از کد نهفته است. رایجترین و سرراستترین پیادهسازی از الگوی ماژول در جاوا اسکریپت این است که نتیجه یک IIFE را به یک متغیر نسبت داده و آن متغیر را اکسپورت کنیم.
1const MyModule = (function() {
2 // Private methods are not accessable outside of the module
3 const _privateMethod = function() {
4 console.log("Hello! I'm a private method");
5 return _private * 2;
6 };
7
8 // locally scoped variable
9 let _private = 0;
10
11 return {
12 get: function() {
13 return private;
14 },
15 getDouble: function() {
16 return _privateMethod();
17 },
18 set: function(input) {
19 private = input;
20 }
21 };
22})();
23
24// Private methods is not available outside the module
25// MyModule.privateMethod();
26
27console.log(`The private variable is ${MyModule.get()}`);
28
29MyModule.set(20);
30
31console.log(`The private variable has been set to ${MyModule.get()}`);
32
33console.log(
34 `Use the private method with a getter and you'll get back ${MyModule.getDouble()}`
35);
الگوی فکتوری
یک الگوی «فکتوری» (Factory) موجب تسهیل ایجاد شیء میشود. الگوی فکتوری زمانی مفید است که بخواهیم شیئی ایجاد کنیم که پیچیده خواهد بود و نسخههای مختلفی از شیء بسته به کانتکست ایجاد خواهد شد.
1// A factory returns an object without using the new keyword
2export const myFactory = ({ firstName, lastName, email }) => {
3 return {
4 name: `${firstName} ${lastName}`,
5 introduction: `Hello! My name is ${firstName} and my email address is ${email}`
6 };
7};
الگوی سینگلتون
الگوی «سینگلتون» (Singelton) برای محدودسازی یک شیء به یک وهله در سراسر اپلیکیشن طراحی شده است. به این ترتیب زمانی که این وهله فراخوانی شود، آخرین باری که فراخوانی شده بود را به خاطر میآورد و همان وهله شیء را بازگشت میدهد.
1/* The singleton is the only instance of this object in the application */
2const ConfigDetails = (function() {
3 const _username = 'test@example.com';
4 const _password = 'password123';
5
6 return {
7 get: function() {
8 return { username: _username, password: _password };
9 }
10 };
11})();
12
13const details = ConfigDetails.get();
14
15console.log(
16 `The configuration details for the application are ${JSON.stringify(details)}`
17);
الگوهای ساختاری
در این بخش الگوهای ساختاری رایج در زبان جاوا اسکریپت را بررسی میکنیم.
الگوی دکوراتور
الگوی «دکوراتور» (Decorator) ریشه در برنامهنویسی شیءگرا دارد و در مواردی که امکان استفاده از وراثت نباشد، برای ارائه کارکردهای اضافی برای یک شیء منفرد در زمان اجرا مورد استفاده قرار میگیرد. برای ایجاد یک دکوراتور در جاوا اسکریپت، ابتدا باید یک وهله جدید از سازنده بسازیم و این وهله را به فراخوانی وهلهسازی دکوراتور ارسال کنیم. در نتیجه یک شیء جدید ایجاد میشود که دارای کارکردهای شیء اصلی است و سپس کارکردهای اضافی دکوراتور نیز به آن افزوده میشود.
1const Person = function(name) {
2 this.name = name;
3
4 this.greet = function() {
5 return console.log(`Hi! my name is ${this.name}`);
6 };
7};
8
9// Create a decorator that gives the Persn object some extra functionality
10const DecoratedPerson = function(person, email) {
11 this.person = person;
12 this.email = email;
13
14 this.whatsYourEmail = function() {
15 return console.log(`My email address is ${this.email}.`);
16 };
17};
18
19// Initalise the first instance of the Person object
20const peter = new Person('Peter');
21
22peter.greet();
23
24// Create a new object that wrapps the Person in the extra functionality
25const decoratedPeter = new DecoratedPerson(peter, 'text@example.com');
26
27decoratedPeter.whatsYourEmail();
الگوی نما
الگوی «نما» (Facade) برای ارائه یک اینترفیس یا API ساده برای یک زیرسیستم پیچیده استفاده میشود. زمانی که به نمای مقابل یک ساختمان مینگرید، با «نما» ی آن مواجه میشوید. این نمای ساختمان همه پیچیدگیهای بالقوه درون آن را پنهان میسازد.
React نمونه خوبی برای یک الگوی Facade است. پیچیدگی مدیریت یک DOM مجازی در پشت استفاده آسان از یک اینترفیس در کلاس کامپوننتها پنهان شده است.
1const SalesforceData = {
2 Contacts: [
3 { FirstName: 'Tom', id: 1, LastName: 'example', account: 1 },
4 { FirstName: 'Jared', id: 2, LastName: 'example', account: 2 }
5 ],
6 Accounts: [{ Name: 'Example ltd', id: 1 }, { Name: 'Test ltd', id: 2 }]
7};
8
9class SalesforceFacade {
10 constructor() {
11 this._getContact = () => {};
12
13 this._getAccounts = () => {
14 return SalesforceData.Accounts.map(item => {
15 return { name: item.Name, id: item.id };
16 });
17 };
18
19 this._get = () => {};
20 }
21
22 get Accounts() {
23 return this._getAccounts();
24 }
25
26 set Accounts(){
27
28 }
29}
30
31const data = new SalesforceFacade();
32
33data._getAccounts();
34
35console.log(`Accounts: ${JSON.stringify(data.Accounts)}`);
الگوی Flyweight
الگوی Flyweight برای کار با مجموعههای بزرگی از اشیا استفاده میشود. این الگو با اشتراک بخشهایی از اشیا با اشیای دیگر در مجموعه موجب صرفهجویی در حافظه میشود.
الگوی Flyweight مشخصههای تکراری مییابد و ارجاع یکتایی به هر مشخصه یکتا ایجاد میکند و مشخصهها را روی مجموعه دادهها برای دستیابی به ارجاع یکتا با هم عوض میکند. این کار موجب ایجاد منحصر به فرد شدن مشخصهها میشود و سربار مجموعه دادهها را کاهش میدهد.
الگوهای رفتاری
در این بخش الگوهای رفتاری را در زبان جاوا اسکریپت بررسی میکنیم.
الگوی Observer
الگوی Observer به صورت یک رابطه یک به چند بین یک Observer و مشترکانش تعریف میشود. مشترکان مسئول ثبت نام و لغو ثبت نام در Observer هستند و خود Observer نیز مسئول ارسال بهروزرسانیها به مشترکانش است.
Observer دارای سه متد اصلی به صورت ثبت نام (Subscribe)، لغو ثبت نام (Unsubscribe) و انتشار (Broadcast) است. متد ثبت به صورت یک پارامتر با مشترک فراخوانی میشود و آن را به لیست مشترکان اضافه میکند. متد لغو ثبت نام مشترک را از لیست مشترکان Observer حذف میکند.
1export class Observerable {
2 constructor() {
3 this.subscribers = [];
4 }
5
6 subscribe(fn) {
7 this.subscribers = [...this.subscribers, fn];
8 }
9 unsubscribe(fn) {
10 this.subscribers = [...this.subscribers.filter(item => item !== fn)];
11 }
12
13 broadcast(data) {
14 for (let i = 0; i < this.subscribers.length; i += 1) {
15 this.subscribers[i](data);
16 }
17 }
18}
19
20const observer = new Observerable();
21
22const fn = data => {
23 console.log(`Received data`, { data });
24};
25
26observer.subscribe(fn);
27
28// Broadcast a message to the subscribers
29observer.broadcast('A broadcasted message');
الگوی Mediator
زمانی که تعداد زیادی سرویس داخلی داشته باشید که بر دادهها و سرویسهای دیگری متکی باشند، ممکن است با مشکل در ارتباط بین سرویسها مواجه شوید. یک Mediator به صورت مرجعیت مرکزی برای سرویسهای داخلی عمل میکند تا مطمئن شویم که همه سرویسها از تغییرهای حالت آگاه هستند.
1export class Mediator {
2 constructor() {
3 this.colleagues = [];
4 }
5
6 join(colleague) {
7 this.colleagues = [...this.colleagues, colleague];
8 colleague.chatroom = this;
9 }
10
11 sendMessage(message, from) {
12 for (let i = 0; i < this.colleagues.length; i += 1) {
13 this.colleagues[i].receiveMessage(message, from);
14 }
15 }
16}
17
18export class Colleague {
19 constructor(name) {
20 this.name = name;
21 this.chatroom = null;
22 }
23 sendMessage(message) {
24 this.chatroom.sendMessage(message, this.name);
25 }
26
27 receiveMessage(message, from) {
28 console.log(`${from}: ${message}`);
29 }
30}
31
32const chatRoom = new Mediator();
33
34const tom = new Colleague('Tom');
35const jared = new Colleague('Jared');
36
37chatRoom.join(tom);
38chatRoom.join(jared);
39
40tom.sendMessage('Hello!');
41jared.sendMessage(`I'm a Cyborg!`);
به این ترتیب به پایان این مقاله با موضوع معرفی الگوهای طراحی نرمافزار در جاوا اسکریپت میرسیم.