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

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

decorator چیست؟

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

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

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

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

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$ mkdir typescript-decorators
$ cd typescript decorators
$ npm init -y
$ mkdir typescript-decorators $ cd typescript decorators $ npm init -y
$ mkdir typescript-decorators
$ cd typescript decorators
$ npm init -y

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$ npm install -D typescript @types/node
$ npm install -D typescript @types/node
$ npm install -D typescript @types/node

پکیج

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

همینطور برای کامپایل کردن کد تایپ اسکریپت، اسکریپت npm زیر را در فایل

package.json
package.jsonاضافه می‌کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
{
// ...
"scripts": {
"build": "tsc"
}
}
{ // ... "scripts": { "build": "tsc" } }
{
  // ...
  "scripts": {
    "build": "tsc"
  }
}

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

برای فعال کردن این ویژگی، باید تنظیماتی را در فایل

tsconfig.json
tsconfig.jsonخود انجام دهیم.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true } }
{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true
  }
}

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
console.log("Hello, world!");
$ npm run build
$ node index.js
Hello, world!
console.log("Hello, world!"); $ npm run build $ node index.js Hello, world!
console.log("Hello, world!");


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

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

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

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$ npm install -D ts-node
$ npm install -D ts-node
$ npm install -D ts-node

سپس یک اسکریپت

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
{
"scripts": {
"build": "tsc",
"start": "ts-node index.ts"
}
}
{ "scripts": { "build": "tsc", "start": "ts-node index.ts" } }
{
  "scripts": {
    "build": "tsc",
    "start": "ts-node index.ts"
  }
}

برای اجرای کد خود کافی است دستور

npm start
npm startرا اجرا کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$ npm start
Hello, world!
$ npm start Hello, world!
$ npm start
Hello, world!

انواع decoratorها

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

Class decorator

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const classDecorator = (target: Function) => {
// do something with your class
}
@classDecorator
class Rocket {}
const classDecorator = (target: Function) => { // do something with your class } @classDecorator class Rocket {}
const classDecorator = (target: Function) => {
  // do something with your class
}

@classDecorator
class Rocket {}

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const addFuelToRocket = (target: Function) => {
return class extends target {
fuel = 100
}
}
@addFuelToRocket
class Rocket {}
const addFuelToRocket = (target: Function) => { return class extends target { fuel = 100 } } @addFuelToRocket class Rocket {}
const addFuelToRocket = (target: Function) => {
  return class extends target {
    fuel = 100
  }
}

@addFuelToRocket
class Rocket {}

اکنون کلاس

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const rocket = new Rocket()
console.log((rocket).fuel) // 100
const rocket = new Rocket() console.log((rocket).fuel) // 100
const rocket = new Rocket()
console.log((rocket).fuel) // 100

Method decorator

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

target
target،
propertyKey
propertyKeyو
descriptor
descriptor.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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... 🚀")
}
}
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... 🚀") } }
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
targetو
propertyKey
propertyKeyرا دریافت می‌کنیم. تنها تفاوت این است که اینجا توصیف‌گر ویژگی وجود ندارد.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const propertyDecorator = (target: Object, propertyKey: string) => {
// do something with your property
}
const propertyDecorator = (target: Object, propertyKey: string) => { // do something with your property }
const propertyDecorator = (target: Object, propertyKey: string) => {
  // do something with your property
}

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

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

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

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Rocket {
@measure
launch() {
console.log("Launching in 3... 2... 1... 🚀");
}
}
class Rocket { @measure launch() { console.log("Launching in 3... 2... 1... 🚀"); } }
class Rocket {
  @measure
  launch() {
    console.log("Launching in 3... 2... 1... 🚀");
  }
}

کلاس

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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;
};
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; };
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
measureمتد اصلی را با متد جدیدی جایگزین می‌کند که این امکان را می‌دهد تا زمان اجرای متد اصلی را محاسبه کرده و نتیجه را در کنسول نمایش دهد.

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

نمونه جدیدی از

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const rocket = new Rocket();
rocket.launch();
const rocket = new Rocket(); rocket.launch();
const rocket = new Rocket();
rocket.launch();

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Launching in 3... 2... 1... 🚀
Execution time: 1.0407989993691444 milliseconds
Launching in 3... 2... 1... 🚀 Execution time: 1.0407989993691444 milliseconds
Launching in 3... 2... 1... 🚀
Execution time: 1.0407989993691444 milliseconds

Decorator factory

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

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const changeValue = (value) => (target: Object, propertyKey: string) => {
Object.defineProperty(target, propertyKey, { value });
};
const changeValue = (value) => (target: Object, propertyKey: string) => { Object.defineProperty(target, propertyKey, { value }); };
const changeValue = (value) => (target: Object, propertyKey: string) => {
  Object.defineProperty(target, propertyKey, { value });
};

تابع

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Rocket {
@changeValue(100)
fuel = 50
}
const rocket = new Rocket()
console.log(rocket.fuel) // 100
class Rocket { @changeValue(100) fuel = 50 } const rocket = new Rocket() console.log(rocket.fuel) // 100
class Rocket {
  @changeValue(100)
  fuel = 50
}

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

حال اگر decorator factory خود را به ویژگی

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

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

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Rocket {
fuel = 50;
launchToMars() {
console.log("Launching to Mars in 3... 2... 1... 🚀");
}
}
class Rocket { fuel = 50; launchToMars() { console.log("Launching to Mars in 3... 2... 1... 🚀"); } }
class Rocket {
  fuel = 50;

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

فرض کنید یک کلاس

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

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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;
};
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; };
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
MinimumFuel یک decorator factory است. پارامتر
fuel
fuel، که بیانگر میزان سوخت لازم برای پرتاب موشک می‌باشد را دریافت می‌کند.

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

اکنون می‌توانیم decorator خود را به متد

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Rocket {
fuel = 50;
@minimumFuel(100)
launchToMars() {
console.log("Launching to Mars in 3... 2... 1... 🚀");
}
}
class Rocket { fuel = 50; @minimumFuel(100) launchToMars() { console.log("Launching to Mars in 3... 2... 1... 🚀"); } }
class Rocket {
  fuel = 50;

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

حال اگر از متد

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const rocket = new Rocket()
rocket.launchToMars()
Not enough fuel!
const rocket = new Rocket() rocket.launchToMars() Not enough fuel!
const rocket = new Rocket()
rocket.launchToMars()


Not enough fuel!

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

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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... 🚀")
}
}
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... 🚀") } }
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... 🚀")
  }
}

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const rocket = new Rocket()
rocket.launchToMoon()
Launching to Moon in 3... 2... 1... 🚀
const rocket = new Rocket() rocket.launchToMoon() Launching to Moon in 3... 2... 1... 🚀
const rocket = new Rocket()
rocket.launchToMoon()


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

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

جمع‌بندی

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

 

دیدگاه‌ها:

افزودن دیدگاه جدید