تغییر یک کلاس با استفاده از decorator ها

?How to Change a Class Using Decorators

14 مرداد 1399
تغییر یک کلاس با استفاده از decorator ها

قبل از آنکه بخواهیم به مباحث پیشرفته تر برسیم باید بدانید که برخی از decorator ها (مانند class decorator ها و method decorator ها) می توانند چیزی را return کنند. منظور من برگرداندن تابع decorator در decorator factory نیست بلکه منظورم برگردان یک مقدار واقعی است. برای شروع اگر سورس کد جلسه قبل را ندارید، آن را از این لینک دانلود کنید.

من می خواهم در ابتدای کار با استفاده از WithTemplate یک مقدار واقعی را برگردانم تا منظور من را متوجه شوید. اینکه چه چیزی را می توانید return کنید به نوع decorator شما بستگی دارد (آیا روی کل کلاس اعمال می شود؟ آیا مخصوص یک متد است؟ و...). در حال حاضر WithTemplate به شکل زیر است:

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

این decorator یک Class decorator است یعنی به کل کلاس اضافه می شود. در چنین decorator هایی می توانیم یک تابع constructor جدید را برگردانیم که جایگزین constructor قبلی می شود:

function WithTemplate(template: string, hookId: string) {
  console.log('TEMPLATE FACTORY');
  return function(originalConstructor: any) {
    console.log('Rendering template');
    const hookEl = document.getElementById(hookId);
    const p = new originalConstructor();
    if (hookEl) {
      hookEl.innerHTML = template;
      hookEl.querySelector('h1')!.textContent = p.name;
    }
    return class extends originalConstructor {
      // کدهای جدید
    }
  };
}

احتمالا با خودتان بگویید که چرا یک کلاس را برگردانده ای؟ اگر یادتان باشد برایتان توضیح دادم که در جاوا اسکریپت Class همان constructor است که در نسخه های ES6 زیبا تر شده است. بنابراین با برگرداندن class همان constructor را برمی گردانیم. از طرفی از آنجایی که نمی خواهیم خصوصیات و کدهای کلاس قبلی خودم را از دست بدهم، constructor جدید را extend کرده ام اما شما الزامی به انجام این کار ندارید. حالا درون این کلاس می توانم کدهای جدید به constructor اضافه کنم. در ضمن من نام constructor دریافتی از پارامتر را به originalConstructor تغییر داده ام، چرا که می خواهم بعدا از نام constructor استفاده کنم و نمی خواهم constructor جدید از constructor قدیمی غیر قابل تفکیک باشد. حالا یک constructor جدید تعریف می کنیم:

function WithTemplate(template: string, hookId: string) {
  console.log('TEMPLATE FACTORY');
  return function(originalConstructor: any) {
    console.log('Rendering template');
    const hookEl = document.getElementById(hookId);
    const p = new originalConstructor();
    if (hookEl) {
      hookEl.innerHTML = template;
      hookEl.querySelector('h1')!.textContent = p.name;
    }
    return class extends originalConstructor {
      constructor () {
        super();
      }
    }
  };
}

دلیل صدا زدن super را می دانید؟ قبلا هم گفته بودیم که اگر کلاس ما کلاس دیگری را extend کند، باید درون constructor آن حتما super را صدا بزنیم. بعد از صدا زدن super می توانید هر کدی که خواستید را بنویسید. به طور مثال می توانیم کدهای قبل که مخصوص نمایش تگ h1 بودند را به درون این constructor منتقل کنیم:

function WithTemplate(template: string, hookId: string) {
  console.log('TEMPLATE FACTORY');
  return function (originalConstructor: any) {
    return class extends originalConstructor {
      constructor() {
        super();
        console.log('Rendering template');
        const hookEl = document.getElementById(hookId);
        const p = new originalConstructor();
        if (hookEl) {
          hookEl.innerHTML = template;
          hookEl.querySelector('h1')!.textContent = p.name;
        }
      }
    }
  };
}

قبل از انجام این کار، تگ h1 به محض اجرا شدن فایل ما در مرورگر نمایش داده می شد اما حالا که منطق مربوط به نمایش آن درون constructor قرار دارد، تا زمانی که خودمان این کلاس را instantiate نکنیم (از آن شیء نسازیم) هیچ تگ h1 ای در مرورگر نمایش داده نمی شود. البته در کد بالا دیگر constructor را صدا نمی زنیم (چرا که درون آن هستیم) بنابراین کد زیر حذف می شود:

const p = new originalConstructor();

همچنین به جای p.name از this.name استفاده می کنیم و نسخه صحیح کار به شکل زیر در می آید:

    return class extends originalConstructor {
      constructor() {
        super();
        console.log('Rendering template');
        const hookEl = document.getElementById(hookId);
        if (hookEl) {
          hookEl.innerHTML = template;
          hookEl.querySelector('h1')!.textContent = this.name;
        }
      }
    }

با انجام این کار در خود کلاس با خطای تایپ ها روبرو می شویم. برای حل این مشکل تابع decorator خود را به یک تابع generic تبدیل می کنیم:

function WithTemplate(template: string, hookId: string) {
  console.log('TEMPLATE FACTORY');
  return function<T extends {new(...arg: any[]): {}}> (originalConstructor: T) {
    return class extends originalConstructor {
      constructor(...arg: any[]) {
        super();
        console.log('Rendering template');
        const hookEl = document.getElementById(hookId);
        if (hookEl) {
          hookEl.innerHTML = template;
          hookEl.querySelector('h1')!.textContent = this.name;
        }
      }
    }
  };
}

به قسمت {new} توجه کنید. new یک کلیدواژه زرزو شده است و {new} یک تایپ خاص است که می گوید عنصر مورد نظر ما قابل new شدن است (قابل استفاده با کلیدواژه new است) که طبیعتا یک constructor است. سپس گفته ام این {new} می تواند هر تعداد آرگومانی را دریافت کند (args...) که یعنی constructor کلاس ما می تواند هر نوع آرگومانی را دریافت نماید. این تابع در نهایت یک شیء را برمی گرداند ({}:). سپس همین قسمت را برای constructor نیز کپی کرده ام تا خود constructor هم بتواند هر نوع آرگومانی را دریافت کند.

با انجام این کار تایپ اسکریپت خطایی به ما می دهد که معلوم نیست چنین decorator ای را به چه کلاسی وصل خواهیم کرد بنابراین مشخص نخواهد بود که خصوصیت this.name وجود داشته باشد. برای حل این مشکل باید بگوییم decorator ما به جای آنکه یک شیء برگرداند، شیء ای را برمی گرداند که دارای خصوصیت name است:

  return function <T extends { new(...arg: any[]): { name: string } }>(originalConstructor: T) {

در این مرحله تایپ اسکریپت به ما اجازه کامپایل نمی دهد چرا که درون constructor پارامترهای args را ذکر کرده ایم اما هیچ جا از آن استفاده نکرده ایم. برای حل این مشکل از علامت _ استفاده می کنیم:

  return function <T extends { new(...arg: any[]): { name: string } }>(originalConstructor: T) {
    return class extends originalConstructor {
      constructor(..._: any[]) {
// بقیه کدها //

حالا می توانید کدهایتان را ذخیره کرده و به مرورگر بروید. با refresh کردن صفحه متوجه می شوید که نام Max هنوز هم مثل قبل روی صفحه قرار دارد بنابراین مشکلی نیست اما نکته جالب اینجاست که اگر کد instantiation را کامنت کنید دیگر Max را نخواهید دید:

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

توجه داشته باشید از آنجایی که super را صدا زده ام، کدهای constructor قدیمی باقی می ماند و هنوز هم اجرا می شود. بنابراین به سادگی توانسته ایم decorator خود را به سطح پیشرفته ای ببریم که بتواند کلاس ما را تغییر دهد. قبل از این هر زمان که به تعریف کلاس می رسیدیم، decorator های ما اجرا می شدند اما با این روش جدید کاری کردیم که اثر decorator ها تا زمان نمونه سازی (instantiate) ظاهر نشود.

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

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

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

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