الگوهای طراحی مختلف در جاوا اسکریپت — به زبان ساده

آخرین به‌روزرسانی: ۲۰ آبان ۱۳۹۷
زمان مطالعه: ۹ دقیقه

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

الگوی طراحی چیست؟

منظور از الگوی طراحی در مهندسی نرم‌افزار راه‌حلی با قابلیت استفاده مجدد برای مسائل با رویداد مکرر در هنگام طراحی نرم‌افزار است. الگوهای طراحی نماینده بهترین رویه‌های مورد استفاده از سوی توسعه‌دهنده‌های نرم‌افزار باتجربه است. یک الگوی طراحی را می‌توان نوعی قالب برنامه‌نویسی در نظر گرفت.

چرا باید از الگوهای طراحی استفاده کنیم؟

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

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

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

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

الگوی ماژول (Module Pattern)

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

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

پلتفرم‌هایی مانند bit می‌توانند به تبدیل ماژول‌ها و کامپوننت‌ها به قطعه‌های سازنده مشترک کمک کنند. بدین ترتیب ماژول‌ها را می‌توان به اشتراک گذاشت، کشف کرد و یا از هر پروژه‌ای توسعه داد. ماژول‌ها بدون نیاز به هیچ‌گونه بازسازی (refactoring) روشی سریع و مقیاس‌پذیر برای اشتراک و استفاده مجدد از کد محسوب می‌شوند.

جاوا اسکریپت برخلاف زبان‌های دیگر برنامه‌نویسی به modifier ها دسترسی ندارد و از این رو نمی‌توان یک متغیر را به صورت عمومی یا خصوصی تعریف کرد. بدین ترتیب الگوی طراحی برای ارزیابی مفهوم کپسوله‌سازی (encapsulation) نیز استفاده می‌شود.

این الگو از IIFE یعنی «عبارت تابعی با فراخوانی بی‌درنگ» (immediately-invoked function expression)، کلوژر (Closure) و دامنه تابع برای شبیه‌سازی این مفهوم استفاده می‌کند. برای مثال:

const myModule = (function() {
  
  const privateVariable = 'Hello World';
  
  function privateMethod() {
    console.log(privateVariable);
  }
  return {
    publicMethod: function() {
      privateMethod();
    }
  }
})();
myModule.publicMethod();

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

بنابراین متغیرها و تابع‌های تعریف شده درون IIFE بنا به ضرورت از دامنه بیرونی پنهان شده‌اند و از این رو آن را برای متغیر myModule به صورت خصوصی تعریف کرده‌اند.

پس از این که کد اجرا شد، متغیر myModule به صورت زیر خواهد بود:

const myModule = {
  publicMethod: function() {
    privateMethod();
  }};

بنابراین می‌توانیم متد publicMethod را فراخوانی کنیم که به نوبه خود متد privateMethod را فراخوانی می‌کند. برای نمونه:

// Prints 'Hello World'
module.publicMethod();

الگوی Revealing Module

الگوی Revealing Module نسخه اندکی بهبود یافته از الگوی ماژول است که از سوی کریستین هیلمان (Christian Heilmann) ارائه شده است. مشکل الگوی ماژول این است که در آن باید تابع‌های عمومی جدیدی را صرفاً برای فراخوانی تابع‌ها و متغیرهای خصوصی ایجاد کنیم.

در این الگو مشخصات شیء بازگشتی را به تابع‌های خصوصی که می‌خواهیم به صورت عمومی افشا کنیم، نگاشت می‌نماییم. به همین دلیل آن را الگوی ماژول افشایی (Revealing Module) می‌نامیم. برای مثال:

const myRevealingModule = (function() {
  
  let privateVar = 'Peter';
  const publicVar  = 'Hello World';
  function privateFunction() {
    console.log('Name: '+ privateVar);
  }
  
  function publicSetName(name) {
    privateVar = name;
  }
  function publicGetName() {
    privateFunction();
  }
  /** reveal methods and variables by assigning them to object     properties */
return {
    setName: publicSetName,
    greeting: publicVar,
    getName: publicGetName
  };
})();
myRevealingModule.setName('Mark');
// prints Name: Mark
myRevealingModule.getName();

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

پس از این که کد اجرا شد، myRevealingModule به صورت زیر خواهد بود:

const myRevealingModule = {
  setName: publicSetName,
  greeting: publicVar,
  getName: publicGetName
};

می‌توان (myRevealingModule.setName(‘Mark’ را که ارجاعی به متد درونی publicSetName و ()myRevealingModule.getName را که ارجاعی به متد درونی publicGetName است، فراخوانی کرد. برای مثال:

myRevealingModule.setName('Mark');
// prints Name: Mark
myRevealingModule.getName();

مزیت‌های الگوی Revealing Module نسبت به الگوی Module را می‌توان به صورت زیر جمع‌بندی کرد:

  • در این الگو می‌توان اعضا را با تغییر دادن یک خط کد در عبارت بازگشتی از حالت عمومی به خصوصی و برعکس تبدیل کرد.
  • شیء بازگشتی شامل هیچ تعریف تابعی نیست و همه بخش راست عبارت درون IIFE تعریف شده است. بدین ترتیب خوانایی کد افزایش می‌یابد.

ماژول‌های ES6

جاوا اسکریپت تا پیش از ES6 ماژول‌های درونی نداشت و از این رو توسعه‌دهندگان باید روی کتابخانه‌های شخص ثالث یا الگوی module برای پیاده‌سازی ماژول‌ها تکیه می‌کردند. ماژول‌های ES6 در فایل‌ها ذخیره می‌شوند و در هر فایل تنها یک ماژول می‌توان داشت. هر چیزی درون یک ماژول به طور پیش‌فرض خصوصی است. تابع‌ها، متغیرها و کلاس‌ها با استفاده از کلیدواژه export افشا می‌شوند. کد درون یک ماژول همواره در حالت strict mode اجرا می‌شود.

اکسپورت کردن یک ماژول

دو روش برای اکسپورت کردن اعلان یک تابع و متغیر وجود دارد. روش اول با افزودن کلیدواژه export در ابتدای اعلان تابع و متغیر است. برای نمونه:

// utils.js
export const greeting = 'Hello World';
export function sum(num1, num2) {
  console.log('Sum:', num1, num2);
  return num1 + num2;
}
export function subtract(num1, num2) {
  console.log('Subtract:', num1, num2);
  return num1 - num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}

روش دوم از طریق افزودن کلیدواژه export در انتهای کدِ شامل نام‌های تابع‌ها و متغیرهایی که می‌خواهیم اکسپورت کنیم است. برای مثال:

// utils.js
function multiply(num1, num2) {
  console.log('Multiply:', num1, num2);
  return num1 * num2;
}
function divide(num1, num2) {
  console.log('Divide:', num1, num2);
  return num1 / num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
export {multiply, divide};

ایمپورت کردن یک ماژول

همانند اکسپورت کردن ماژول، دو روش برای ایمپورت کردن یک ماژول با استفاده از کلیدواژه import وجود دارد. برای نمونه:

ایمپورت چند آیتم به یک باره

// main.js
// importing multiple items
import { sum, multiply } from './utils.js';
console.log(sum(3, 7));
console.log(multiply(3, 7));

ایمپورت کردن همه ماژول

// main.js
// importing all of module
import * as utils from './utils.js';
console.log(utils.sum(3, 7));
console.log(utils.multiply(3, 7));

ایمپورت و اکسپورت را می‌توان با نام مستعار انجام داد

اگر می‌خواهید از تصادم نام (name collision) اجتناب کنید، می‌توانید نام اکسپورت را هم در زمان اکسپورت و هم ایمپورت تغییر دهید. برای نمونه:

تغییر نام یک اکسپورت

// utils.js
function sum(num1, num2) {
  console.log('Sum:', num1, num2);
  return num1 + num2;
}
function multiply(num1, num2) {
  console.log('Multiply:', num1, num2);
  return num1 * num2;
}
export {sum as add, multiply};

تغییر نام یک ایمپورت

// main.js
import { add, multiply as mult } from './utils.js';
console.log(add(3, 7));
console.log(mult(3, 7));

الگوی سینگلتون (Singleton)

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

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

const user = {
  name: 'Peter',
  age: 25,
  job: 'Teacher',
  greet: function() {
    console.log('Hello!');
  }
};

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

اگر تلاش کنیم که متغیر user را به متغیر دیگری کپی کنیم و آن متغیر را تغییر دهیم، باید به صورت زیر عمل کنیم:

const user1 = user;
user1.name = 'Mark';

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

// prints 'Mark'
console.log(user.name);
// prints 'Mark'
console.log(user1.name);
// prints true
console.log(user === user1);

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

let instance = null;
function User() {
  if(instance) {
    return instance;
  }
  instance = this;
  this.name = 'Peter';
  this.age = 25;
  
  return instance;
}
const user1 = new User();
const user2 = new User();
// prints true
console.log(user1 === user2);

هنگامی که تابع سازنده فراخوانی می‌شود، بررسی می‌کند که آیا شیء instance وجود دارد یا نه. اگر شیء وجود نداشته باشد، متغیر this را به متغیر instance انتساب می‌دهد. و اگر شیء وجود داشته باشد، صرفاً آن شیء را بازمی‌گرداند.

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

const singleton = (function() {
  let instance;
  
  function init() {
    return {
      name: 'Peter',
      age: 24,
    };
  }
  return {
    getInstance: function() {
      if(!instance) {
        instance = init();
      }
      
      return instance;
    }
  }
})();
const instanceA = singleton.getInstance();
const instanceB = singleton.getInstance();
// prints true
console.log(instanceA === instanceB);

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

الگوی factory

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

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

class Car{
  constructor(options) {
    this.doors = options.doors || 4;
    this.state = options.state || 'brand new';
    this.color = options.color || 'white';
  }
}
class Truck {
  constructor(options) {
    this.doors = options.doors || 4;
    this.state = options.state || 'used';
    this.color = options.color || 'black';
  }
}
class VehicleFactory {
  createVehicle(options) {
    if(options.vehicleType === 'car') {
      return new Car(options);
    } else if(options.vehicleType === 'truck') {
      return new Truck(options);
      }
  }
}

در این کد یک کلاس car و یک کلاس truck با برخی مقادیر پیش‌فرض ایجاد کرده‌ایم که برای ایجاد اشیای جدید car و truck استفاده می‌شوند. همچنین یک کلاس VehicleFactory تعریف کرده‌ایم که شیء جدیدی را بر مبنای مشخصه vehicleType دریافتی در شیء options ایجاد کرده و بازمی‌گرداند.

const factory = new VehicleFactory();
const car = factory.createVehicle({
  vehicleType: 'car',
  doors: 4,
  color: 'silver',
  state: 'Brand New'
});
const truck= factory.createVehicle({
  vehicleType: 'truck',
  doors: 2,
  color: 'white',
  state: 'used'
});
// Prints Car {doors: 4, state: "Brand New", color: "silver"}
console.log(car);
// Prints Truck {doors: 2, state: "used", color: "white"}
console.log(truck);

می‌بینید که یک شیء جدید factory از کلاس VehicleFactory ایجاد کرده‌ایم. پس از آن می‌توانیم یک شیء car یا truck جدید را با فراخوانی factory.createVehicle و ارسال شیء options با مشخصه vehicleType و با مقدار car یا truck ایجاد کنیم.

الگوی دکوراتور (Decorator)

الگوی دکوراتور برای بسط دادن کارکردهای یک شیء بدون تغییر دادن کلاس موجود یا تابع سازنده استفاده می‌شود. این الگو می‌تواند برای افزودن ویژگی‌های یک شیء بدون اصلاح کد زیربنایی تشکیل‌دهنده‌اش استفاده شود. نمونه ساده‌ای از این الگو به صورت زیر است:

function Car(name) {
  this.name = name;
  // Default values
  this.color = 'White';
}
// Creating a new Object to decorate
const tesla= new Car('Tesla Model 3');
// Decorating the object with new functionality
tesla.setColor = function(color) {
  this.color = color;
}
tesla.setPrice = function(price) {
  this.price = price;
}
tesla.setColor('black');
tesla.setPrice(49000);
// prints black
console.log(tesla.color);

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

class Car() {
}
class CarWithAC() {
}
class CarWithAutoTransmission {
}
class CarWithPowerLocks {
}
class CarWithACandPowerLocks {
}

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

class Car {
  constructor() {
  // Default Cost
  this.cost = function() {
  return 20000;
  }
}
}
// Decorator function
function carWithAC(car) {
  car.hasAC = true;
  const prevCost = car.cost();
  car.cost = function() {
    return prevCost + 500;
  }
}
// Decorator function
function carWithAutoTransmission(car) {
  car.hasAutoTransmission = true;
   const prevCost = car.cost();
  car.cost = function() {
    return prevCost + 2000;
  }
}
// Decorator function
function carWithPowerLocks(car) {
  car.hasPowerLocks = true;
  const prevCost = car.cost();
  car.cost = function() {
    return prevCost + 500;
  }
}

ابتدا یک کلاس پایه car برای ایجاد شیءهای car ایجاد می‌کنیم. سپس دکوراتورهایی برای ویژگی‌هایی که می‌خواهیم به آن اضافه کنیم ایجاد کرده و شیء car را به صورت یک پارامتر ارسال می‌کنیم. سپس تابع هزینه این شیء را که هزینه به‌روزرسانی شده car را بازگشت می‌دهد override می‌کنیم و مشخصه جدیدی به آن شیء اضافه می‌کنیم که نشان می‌دهد این ویژگی افزوده شده است.

برای افزودن ویژگی جدید باید به صورت زیر عمل کنیم:

const car = new Car();
console.log(car.cost());
carWithAC(car);
carWithAutoTransmission(car);
carWithPowerLocks(car);

در نهایت می‌توانیم بهای خودرو را به صورت زیر محاسبه کنیم:

// Calculating total cost of the car
console.log(car.cost());

سخن پایانی

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

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

اگر این مطلب برایتان مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۵ نفر
آیا این مطلب برای شما مفید بود؟
شما قبلا رای داده‌اید!
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
blog.bitsrc.io

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *