آشنایی با Decorator ها (به فصل جدید خوش آمدید)

Introduction to Decorators

13 مرداد 1399
آشنایی با Decorator ها (به فصل جدید خوش آمدید)

حالا که با generics آشنا شده ایم نوبت به یک مبحث پیشرفته تر به نام decorator ها می رسد. Decorator ها بیشتر برای meta programing هستند و به زبان ساده تر آنچنان تاثیری روی محصول نهایی شما ندارند بلکه به طور مثال کدنویسی در یک تیم و درک کد توسط دیگر اعضای تیم را آسان تر می کنند. برای شروع کار با decorator ها از همان پروژه اولیه همیشگی خودمان استفاده می کنیم. اولین کار این است که فایل tsconfig.json را باز کرده و حتما target را روی es6 بگذارید. سپس experimentalDecorators را نیز فعال کنید:

"target": "es6"
// …
"experimentalDecorators": true

بعد از اینکه این کار را انجام دادید می توانیم شروع به کار کنیم. Decorator ها معمولا برای کار با کلاس ها استفاده می شوند و اولین decorator ای که می خواهیم بنویسیم نیز از این قاعده مستثنی نیست. فرض کنید کلاس زیر را داشته باشیم:

class Person {
  name = 'Max';

  constructor() {
    console.log('Creating person object...');
  }
}

const pers = new Person();
console.log(pers);

با اجرای این کد در مرورگر مقادیر زیر را در کنسول مرورگر مشاهده می کنیم:

Creating person object…
Person {name: “Max”}

تا اینجا چیز جدیدی نداشتیم اما حالا می خواهم یک Decorator را تعریف کنم. یادتان باشد که decorator ها فقط یک تابع عادی هستند که در نهایت روی چیزی مثل یک کلاس اعمال می شوند. معمولا نام decorator ها را به حرف بزرگ شروع می کنند، بنابراین من هم از این قرار داد پیروی کرده و این کار را انجام می دهم:

function Logger() {
  console.log('Logging...');
}

همانطور که می بینید این تابع کار خاصی انجام نمی دهد. برای اعمال کردن این تابع روی کلاس خودمان باید به صورت زیر عمل کنیم:

function Logger() {
  console.log('Logging...');
}

@Logger
class Person {
  name = 'Max';

  constructor() {
    console.log('Creating person object...');
  }
}

یعنی با استفاده از علامت @ و ذکر نام decorator آن را روی کلاس اعمال می کنیم. در حال حاضر تایپ اسکریپت به ما خطا می دهد که Logger هیچ آرگومانی دریافت نکرده و باید آرگومان های بیشتری دریافت کند. اینکه هر decorator چند آرگومان می گیرد به محلی بستگی دارد که در آن از decorator استفاده می کنیم. اینجا و در کد ما که decorator به کلاس اضافه شده است یک آرگومان می گیریم و آن هم target است که همان constructor ما می باشد:

function Logger(target: Function) {
  console.log('Logging...');
}

البته این نام قابل تغییر و سلیقه ای است. مثلا از آنجایی که target ما همان constructor است من نام این آرگومان را constructor می گذارم:

function Logger(constructor: Function) {
  console.log('Logging...');
}

همچنین خود constructor را نیز console.log می کنم تا آن را هم در مرورگر ببینیم:

function Logger(constructor: Function) {
  console.log('Logging...');
  console.log(constructor);
}

حالا اگر کدها را ذخیره کرده و به مرورگر برویم چنین صحنه ای را مشاهده می کنیم:

نمایش constructor ما
نمایش constructor ما

یعنی علاوه بر کدهای قبلی که به آن ها دست نزدیم، constructor را هم می بینیم. احتمالا بپرسید چرا کل کلاس را می بینیم؟ دلیلش این است که کلاس ها در واقع شکل مدرن تر همان constructor های es5 هستند و در نسخه های جدید تر نیز تغییری نکرده اند. همچنین توجه داشته باشید که decorator ما اول از همه چاپ شده است چرا که decorator ها زمانی اجرا می شوند که کلاس شما تعریف بشود (نه زمانی که از کلاس نمونه ای ساخته شود).

Decorator factory چیست؟

علاوه بر تعریف کردن مستقیم Decorator ها به شکل بالا، راه دیگری نیز برای تعریف آن ها وجود دارد. به طور مثال می توانیم از decorator factory ها استفاده کنیم که توابع decorator را برمی گرداند اما به ما اجازه می دهند که هنگام انتساب decorator ها به یک کلاس مشخص، آن ها را تنظیم کنیم. برای نشان دادن این قابلیت من همان تابع Logger را به یک decorator factory تبدیل می کنم:

function Logger() {
  return function(constructor: Function) {
    console.log('Logging...');
    console.log(constructor);
  };
}

همانطور که می بینید حالا Logger یک تابع anonymous را برمی گرداند که همان تابع مورد نظر ما است. اگر از Decorator factory ها استفاده کنیم باید آن را به صورت یک تابع انتساب بدهیم. یعنی حتما جلوی آن پرانتز بگذاریم:

@Logger()
class Person {
  name = 'Max';

  constructor() {
    console.log('Creating person object...');
  }
}

با انجام این کار می توانید پارامترهای مختلفی را نیز به decorator ها پاس بدهید. به طور مثال من تابع بالا را به شکل زیر ویرایش می کنم تا یک پارامتر رشته ای دلخواه بگیرد:

function Logger(logString: string) {
  return function(constructor: Function) {
    console.log(logString);
    console.log(constructor);
  };
}

سپس هنگام انتساب آن به کلاس خودمان این پارامتر را به آن پاس می دهم:

@Logger('LOGGING - PERSON')
class Person {
  name = 'Max';

  constructor() {
    console.log('Creating person object...');
  }
}

بنابراین با استفاده از decorator factory ها می توانیم مقادیر مورد استفاده decorator را به صورت شخصی سازی شده و جداگانه تنظیم کنیم. تا اینجا decorator های ما آنچنان کارایی نداشته اند بنابراین بهتر است چند decorator کارآمدتر بنویسیم:

function WithTemplate(template: string, hookId: string) {
  return function(constructor: Function) {

  }
}

این یک decorator factory جدید است که دو پارامتر می گیرد و نهایتا خود decorator را برمی گرداند. در واقع من می خواهم درون این decorator یک قالب (template) به زبان HTML بسازم و بعدا آن را به عنصری متصل کنم که id آن برابر hookId پاس داده شده باشد. بنابراین به فایل index.html رفته و یک div ساده را در آن قرار دهید تا بتوانیم این کار را انجام دهیم:

<body>
  <div id="app"></div>
</body>

سپس decorator factory خود را تکمیل می کنیم:

function WithTemplate(template: string, hookId: string) {
  return function (constructor: Function) {
    const hookEl = document.getElementById(hookId);
    if (hookEl) {
      hookEl.innerHTML = template;
    }
  }
}

کد بالا می گوید که ابتدا عنصر مورد نظر با id برابر با hookId را درون hookEl قرار بده و سپس اگر hookEl وجود داشت (یعنی چنین عنصری در DOM وجود داشت) مقدار innerHTML آن را برابر Template یا همان آرگومان اول قرار بده. البته تایپ اسکریپت به من می گوید که در خود decorator از constructor استفاده نکرده ام اما آن را به عنوان آرگومان دریافت کرده ام. برای اینکه به تایپ اسکریپت بگوییم فعلا با constructor کاری نداریم باید از علامت _ استفاده کنیم:

function WithTemplate(template: string, hookId: string) {
  return function (_: Function) {
    const hookEl = document.getElementById(hookId);
    if (hookEl) {
      hookEl.innerHTML = template;
    }
  }
}

حالا از همان کلاس قبلی استفاده می کنیم و withTemplate را به آن انتساب می دهیم:

@WithTemplate('<h1>My Person Object</h1>', 'app')
 class Person {
  name = 'Max';

  constructor() {
    console.log('Creating person object...');
  }
}

حالا اگر کدها را ذخیره کرده و به مرورگر بروید، شاهد نمایش تگ h1 در مرورگر هستید. ما می توانیم کار های بیشتری نیز انجام بدهیم، مثلا من می خواهم از خصوصیت name درون کلاس استفاده کرده و آن را چاپ کنم. بنابراین:

function WithTemplate(template: string, hookId: string) {
  return function(constructor:any) {
    const hookEl = document.getElementById(hookId);
    const p = new constructor();
    if (hookEl) {
      hookEl.innerHTML = template;
      hookEl.querySelector('h1')!.textContent = p.name;
    }
  }
}

این کد ابتدا constructor را صدا می زند و برای این کار تایپ آن را روی Any گذاشته ایم. چرا؟ به دلیل اینکه constructor یک تابع خاص است و تایپ اسکریپت آن را به عنوان Function عادی تشخیص نمی دهد بنابراین any گذاشته ام تا با خطای تایپ مواجه نشویم. با صدا زدن constructor خصوصیت name ساخته می شود و حالا می توانیم با querySelector تگ h1 را پیدا کرده و متن آن را به name تغییر بدهیم. علامت تعجب را نیز از این جهت گذاشته ام که به تایپ اسکریپت بگوییم مطمئن هستیم تگ h1 وجود دارد و به خطا نمی خوریم. این نوع از Template ها را به تکرار در فریم ورک هایی همچون angular مشاهده می کنید.

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

دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

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