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

decorator چیست؟

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

در جاوااسکریپت، این ویژگی در حال حاضر در stage two قرار دارد زیرا هنوز در مرورگرها یا Node.js موجود نیست. اما می‌توانیم آن را با استفاده از کامپایلرهایی مانند Babel شبیه‌سازی و تست کنیم.

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

شروع کار با decoratorها

این کار را با ایجاد یک پروژه Node.js خالی شروع می‌کنیم.

$ mkdir typescript-decorators
$ cd typescript decorators
$ npm init -y

سپس تایپ اسکریپت را به عنوان یک dependency توسعه نصب می‌کنیم.

$ npm install -D typescript @types/node

پکیج @types/nodeشامل تعاریف تایپ Node.js برای تایپ اسکریپت است. برای دسترسی به برخی از کتابخانه‌های استاندارد Node.js به این پکیج نیاز داریم.

همینطور برای کامپایل کردن کد تایپ اسکریپت، اسکریپت npm زیر را در فایل package.jsonاضافه می‌کنیم:

{
  // ...
  "scripts": {
    "build": "tsc"
  }
}

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

برای فعال کردن این ویژگی، باید تنظیماتی را در فایل tsconfig.jsonخود انجام دهیم.

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true
  }
}

سپس در ادامه یک فایل تایپ اسکریپت ساده ایجاد می‌کنیم تا آن را تست کنیم.

console.log("Hello, world!");


$ npm run build
$ node index.js
Hello, world!

به جای این که این دستور را بارها و بارها تکرار کنیم می‌توانیم فرآیند کامپایل و اجرا را با استفاده از پکیجی به نام ts-nodeساده‌تر کنیم. این یک پکیج جامع است که به ما این امکان را می‌دهد تا کد تایپ اسکریپت خود را مستقیماً و بدون کامپایل کردن آن اجرا کنیم.

این پکیج را به عنوان یک dependency توسعه به شکل زیر نصب می‌کنیم:

$ npm install -D ts-node

سپس یک اسکریپت startبه فایل package.jsonاضافه می‌کنیم:

{
  "scripts": {
    "build": "tsc",
    "start": "ts-node index.ts"
  }
}

برای اجرای کد خود کافی است دستور npm startرا اجرا کنیم:

$ npm start
Hello, world!

انواع decoratorها

در تایپ اسکریپت، decorator ها توابعی هستند که می‌توانند به کلاس‌ها و اعضای آن‌ها مانند متدها و ویژگی‌ها متصل شوند. در ادامه چند نمونه را باهم بررسی خواهیم کرد.

Class decorator

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

const classDecorator = (target: Function) => {
  // do something with your class
}

@classDecorator
class Rocket {}

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

const addFuelToRocket = (target: Function) => {
  return class extends target {
    fuel = 100
  }
}

@addFuelToRocket
class Rocket {}

اکنون کلاس Rocketما دارای ویژگی fuelبا مقدار پیش‌فرض ۱۰۰خواهد بود.

const rocket = new Rocket()
console.log((rocket).fuel) // 100

Method decorator

یکی دیگر از قسمت‌های خوب برای استفاده از decorator، متد class می‌باشد. در اینجا، ما سه پارامتر را در تابع خود دریافت می‌کنیم که عبارتند از: target، propertyKeyو descriptor.

const myDecorator = (target: Object, propertyKey: string, descriptor: PropertyDescriptor) =>  {
  // do something with your method
}

class Rocket {
  @myDecorator
  launch() {
    console.log("Launching rocket in 3... 2... 1... 🚀")
  }
}

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

اگر بخواهیم functionality متد خود را گسترش دهیم که بعداً به آن خواهیم پرداخت، method decorator می‌تواند بسیار مفید باشد.

Property decorator

در این مورد نیز مانند method decorator، پارامتر targetو propertyKeyرا دریافت می‌کنیم. تنها تفاوت این است که اینجا توصیف‌گر ویژگی وجود ندارد.

const propertyDecorator = (target: Object, propertyKey: string) => {
  // do something with your property
}

موارد استفاده از decoratorها در تایپ اسکریپت

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

محاسبه زمان اجرا

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

class Rocket {
  @measure
  launch() {
    console.log("Launching in 3... 2... 1... 🚀");
  }
}

کلاس Rocketیک متد launchدر داخل خود دارد. برای اندازه‌گیری زمان اجرای متد launchمی‌توانیم دکوراتور measureرا مورد استفاده قرار دهیم.

const measure = (
  target: Object,
  propertyKey: string,
  descriptor: PropertyDescriptor
) => {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args) {
    const start = performance.now();
    const result = originalMethod.apply(this, args);
    const finish = performance.now();
    console.log(`Execution time: ${finish - start} milliseconds`);
    return result;
  };

  return descriptor;
};

همانطور که می بینیم، دکوراتور measureمتد اصلی را با متد جدیدی جایگزین می‌کند که این امکان را می‌دهد تا زمان اجرای متد اصلی را محاسبه کرده و نتیجه را در کنسول نمایش دهد.

برای محاسبه زمان اجرا، از Performance Hooks API از کتابخانه استاندارد Node.js استفاده می‌کنیم.

نمونه جدیدی از Rocketرا ایجاد کرده و متد launchرا فراخوانی می‌کنیم.

const rocket = new Rocket();
rocket.launch();

در نهایت نتیجه زیر را دریافت خواهیم کرد:

Launching in 3... 2... 1... 🚀
Execution time: 1.0407989993691444 milliseconds

Decorator factory

برای پیکربندی decoratorهای خود به گونه‌ای که در یک سناریوی خاص به گونه‌ای متفاوت عمل کنند، می‌توانیم از مفهومی به نام decorator factory استفاده کنیم.

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

const changeValue = (value) => (target: Object, propertyKey: string) => {
  Object.defineProperty(target, propertyKey, { value });
};

تابع changeValueیک decorator را برمی‌گرداند که مقدار ویژگی را بر اساس مقدار ارسال شده تغییر می‌دهد.

class Rocket {
  @changeValue(100)
  fuel = 50
}

const rocket = new Rocket()
console.log(rocket.fuel) // 100

حال اگر decorator factory خود را به ویژگی fuelمتصل کنیم، مقدار آن برابر با ۱۰۰خواهد شد.

گارد خودکار خطا

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

class Rocket {
  fuel = 50;

  launchToMars() {
    console.log("Launching to Mars in 3... 2... 1... 🚀");
  }
}

فرض کنید یک کلاس Rocketداریم که متد launchToMarsرا شامل می‌شود. برای پرتاب موشک به مریخ، سطح سوخت باید بالای ۱۰۰ باشد.

حال decorator را برای آن ایجاد می‌کنیم:

const minimumFuel = (fuel: number) => (
  target: Object,
  propertyKey: string,
  descriptor: PropertyDescriptor
) => {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args) {
    if (this.fuel > fuel) {
      originalMethod.apply(this, args);
    } else {
      console.log("Not enough fuel!");
    }
  };

  return descriptor;
};

MinimumFuel یک decorator factory است. پارامتر fuel، که بیانگر میزان سوخت لازم برای پرتاب موشک می‌باشد را دریافت می‌کند.

برای بررسی وضعیت سوخت، متد اصلی را مانند مورد استفاده قبلی داخل متد جدید قرار می‌دهیم.

اکنون می‌توانیم decorator خود را به متد launchToMarsمتصل کرده و حداقل سطح سوخت را تنظیم کنیم.

class Rocket {
  fuel = 50;

  @minimumFuel(100)
  launchToMars() {
    console.log("Launching to Mars in 3... 2... 1... 🚀");
  }
}

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

const rocket = new Rocket()
rocket.launchToMars()


Not enough fuel!

نکته جالب در مورد این decorator این است که می‌توانیم همان منطق را در روشی متفاوت بدون بازنویسی کل عبارت if-else اعمال کنیم.

فرض کنید می‌خواهیم روش جدیدی برای پرتاب موشک این بار به ماه ایجاد کنیم. برای انجام این کار، سطح سوخت باید بالای ۲۵ باشد. همان کد را تکرار کرده و پارامتر را تغییر می‌دهیم.

class Rocket {
  fuel = 50;

  @minimumFuel(100)
  launchToMars() {
    console.log("Launching to Mars in 3... 2... 1... 🚀");
  }

  @minimumFuel(25)
  launchToMoon() {
    console.log("Launching to Moon in 3... 2... 1... 🚀")
  }
}

اکنون می‌توانیم این موشک را به ماه پرتاب کنیم.

const rocket = new Rocket()
rocket.launchToMoon()


Launching to Moon in 3... 2... 1... 🚀

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

جمع‌بندی

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