آشنایی با 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 مشاهده می کنید.

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

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

علی
11 دی 1401
عالی بدون پیچیدگی

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

ali
02 شهریور 1401
خیلی عالی و کامل از اکثر سایت های خارجی هم بهتر بود

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

abbas
25 خرداد 1401
سلام واقعا عالی و رسا توضیح دادین ممنون خدا قوت

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

روکسو
11 مرداد 1401
سلام. از لطف شما ممنونیم.

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

فرزاد
21 اسفند 1400
ممنون از آموزش فوق العاده خوب. اما من یکجا رو متوجه نشدم. همانطور که می دانیم کلاس ها همان فانکشن ها هستند که کار ما را برای ساخت یک آبجکت ساده تر کردند. اما چرا حتما باید نوع تابع سازنده رو زمانی که بخوایم ازش داخل فانکشن دکوریتور یک شی بسازیم تایپ اون رو any یا typeof className بگذاریم؟ من از هر دو نوع نمونه خروجی گرفتم console.log(typeof constructor) //function console.log(typeof className) //function خروجی هر دو فانکشن است پس چرا عمل نمی کند؟ من گیج شدم و کلی سرچ کردم و به نتیجه نرسیدم.

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

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