پروژه Drag & Drop: ارث‌بری و Generic Type ها (2)

Drag & Drop Project: Inheritance and Generic Types - Part 2

19 مرداد 1399
پروژه ی Drag & Drop: ارث بری و generic type ها (2)

همانطور که می دانید ما در حال پیاده سازی ارث بری در برنامه خود هستیم. در جلسه قبل به پیاده سازی کلاس پدر در ProjectList و کد زیر رسیده بودیم:

// ProjectList Class
class ProjectList extends Component<HTMLDivElement, HTMLElement> {
  assignedProjects: Project[];

من برایتان تضویح دادم که این کلاس با کلیدواژه extends از component ارث بری کرده ایم. همچنین از کدهای قبلی می دانیم که یک HTMLDivElement و یک HTMLElement خواهیم داشت بنابراین این تایپ ها را نیز برایش مشخص کرده ایم. از آنجایی که تعریف خصوصیات پایه کار کلاس component بود دیگر نیازی به تعریف templateElement و hostElement و element نداریم و همه آن ها را پاک کرده ایم. درون constructor این کلاس نیز باید مثل همیشه متد super را صدا بزنیم تا constructor کلاس پدر (component) صدا زده شود. نحوه انجام این کار به شکل زیر است:

// ProjectList Class
class ProjectList extends Component<HTMLDivElement, HTMLElement> {
  assignedProjects: Project[];

  constructor(private type: 'active' | 'finished') {
    super('project-list', 'app', false, `${type}-projects`);
    this.assignedProjects = [];

    this.configure();
    this.renderContent();
  }

زمانی که constructor کلاس component را تعریف کردیم، پارامترهایی را برای آن مشخص کردیم بنابراین باید این پارامترها را به super پاس بدهیم. اولین پارامتر templateId، دومین پارامتر hostElementId و سومین پارامتر InsertAtStart بود. در نهایت یک پارامتر دلخواه به نام newElementId نیز داشتیم. من بر همین اساس و ترتیب، پارامترهای مورد نظر را به super پاس داده ام. همانطور که می بینید متدهای configure و renderContent را نیز صدا زده ام که باید آن ها را نیز تعریف کنیم.

در مرحله بعد باید کدهای اضافی را حذف کنیم. مثلا متد attach در این کلاس را باید حذف کنیم چرا که با متد attach در کلاس پدر (Component) تداخل پیدا می کند. سپس از آنجایی که متدهای abstract قابلیت private شدن ندارند، کلیدواژه private را از متد renderContent حذف می کنیم. مشکل بعدی این است که هنوز متد configure را تعریف نکرده ایم بنابراین آن را پس از constructor تعریف می کنیم:

  configure() {
    projectState.addListener((projects: Project[]) => {
      const relevantProjects = projects.filter(prj => {
        if (this.type === 'active') {
          return prj.status === ProjectStatus.Active;
        }
        return prj.status === ProjectStatus.Finished;
      });
      this.assignedProjects = relevantProjects;
      this.renderProjects();
    });
  }

همانطور که می بینید این کد مربوط به listener ما است (در جلسات قبل نوشته شد) که آن را به درون این متد انتقال داده ایم. اگر دوست ندارید این کار را بکنید، می توانید پیاده سازی متد configure در کلاس پدر را غیر الزامی کنید:

  abstract configure?(): void;

با قرار دادن علامت سوال مطمئن می شویم که این متد غیر الزامی است و کلاس های فرزند مجبور به اضافه کردن آن نیستند. راه دیگر این است که این متد را بدون اینکه کار خاصی بکند بنویسید (به صورت یک متد خالی). من تصمیم گرفتم که این متد اجباری باقی بماند و در عوض کلاس configure کدهای listener را بگیرد.

سوال: چرا configure و renderContent را درون constructor کلاس فرزند صدا زده ایم؟ آیا نمی توانیم آن ها را در کلاس پدر صدا بزنیم؟

پاسخ: بله شما می توانید آن ها را در کلاس پدر صدا بزنید اما با این کار امکان ایجاد یک باگ را در برنامه خود قوی می کنید. در واقع ممکن است کلاس فرزند متدی را صدا بزند که پس از constructor کلاس پدر اجرا شود و متدهای configure و renderContent به آن وابسته باشند. بدین صورت وابستگی این متدها را از آن ها گرفته ایم و باعث ایجاد خطا در برنامه می شویم. به صورت یک قانون کلی همیشه یادتان باشد که صدا زدن متدها در کلاس فرزند بهتر است.

حالا که این کلاس را تصحیح کرده ایم نوبت به کلاس ProjectInput است. ابتدا کلاس پایه را extend کرده و خصوصیات اضافه (templateElement و hostElement و element) را حذف کنید:

// ProjectInput Class
class ProjectInput extends Component<HTMLDivElement, HTMLFormElement> {
  titleInputElement: HTMLInputElement;
  descriptionInputElement: HTMLInputElement;
  peopleInputElement: HTMLInputElement;
// بقیه کدها //
 

خصوصیات باقی مانده در کد بالا را نگه می داریم چرا که مخصوص این کلاس هستند. سپس مثل همیشه super را صدا می زنیم:

// ProjectInput Class
class ProjectInput extends Component<HTMLDivElement, HTMLFormElement> {
  titleInputElement: HTMLInputElement;
  descriptionInputElement: HTMLInputElement;
  peopleInputElement: HTMLInputElement;

  constructor() {
    super('project-input', 'app', true, 'user-input');
    this.titleInputElement = this.element.querySelector(
      '#title'
    ) as HTMLInputElement;
    this.descriptionInputElement = this.element.querySelector(
      '#description'
    ) as HTMLInputElement;
    this.peopleInputElement = this.element.querySelector(
      '#people'
    ) as HTMLInputElement;
    this.configure();
  }

ترتیب پارامترهای super مانند قبل است. من پارامتر سوم را true گذاشته ام چرا که می خواهم عنصر تازه ساخته شده در ابتدا (start) اضافه شود. با این کار تمام کدهای تکراری را حذف می کنیم. ممکن است بخواهید کدهای بالا (querySelector ها) را درون متد configure قرار دهید و سپس configure را درون constructor صدا بزنید. مشکل اینجاست که تایپ اسکریپت نمی تواند داخل configure را بخواند بنابراین به شما خطا می دهد که خصوصیات را initialize یا تعریف نکرده اید. به همین دلیل بهتر است آن ها را درون خود constructor باقی بگذاریم.

در نهایت تمام کدهای این کلاس به شکل زیر در می آید:

// ProjectInput Class
class ProjectInput extends Component<HTMLDivElement, HTMLFormElement> {
  titleInputElement: HTMLInputElement;
  descriptionInputElement: HTMLInputElement;
  peopleInputElement: HTMLInputElement;

  constructor() {
    super('project-input', 'app', true, 'user-input');
    this.titleInputElement = this.element.querySelector(
      '#title'
    ) as HTMLInputElement;
    this.descriptionInputElement = this.element.querySelector(
      '#description'
    ) as HTMLInputElement;
    this.peopleInputElement = this.element.querySelector(
      '#people'
    ) as HTMLInputElement;
    this.configure();
  }

  configure() {
    this.element.addEventListener('submit', this.submitHandler);
  }

  renderContent() {}

  private gatherUserInput(): [string, string, number] | void {
    const enteredTitle = this.titleInputElement.value;
    const enteredDescription = this.descriptionInputElement.value;
    const enteredPeople = this.peopleInputElement.value;

    const titleValidatable: Validatable = {
      value: enteredTitle,
      required: true
    };
    const descriptionValidatable: Validatable = {
      value: enteredDescription,
      required: true,
      minLength: 5
    };
    const peopleValidatable: Validatable = {
      value: +enteredPeople,
      required: true,
      min: 1,
      max: 5
    };

    if (
      !validate(titleValidatable) ||
      !validate(descriptionValidatable) ||
      !validate(peopleValidatable)
    ) {
      alert('Invalid input, please try again!');
      return;
    } else {
      return [enteredTitle, enteredDescription, +enteredPeople];
    }
  }

یعنی متد private attach را حذف کرده ایم تا تداخل ایجاد نکند. سپس متد configure را از حالت private در می آورریم (قبلا هم گفتم این متدها abstract هستند و نمی توانند private باشند). در نهایت نیازی به تغییر state نداریم اما من می خواهم برای تمرین بیشتر یک کلاس پدر را برای State تعریف می کنیم:

// Project State Management
type Listener<T> = (items: T[]) => void;

class State<T> {
  protected listeners: Listener<T>[] = [];

  addListener(listenerFn: Listener<T>) {
    this.listeners.push(listenerFn);
  }
}

از آنجا که آرایه listeners و همچنین متد addListener هیمشه و در تمام state ها تکرار می شوند، آن ها را درون این کلاس پایه قرار داده ام.

حالا که کلاس پدر را برای state تعریف کرده ایم معلوم نیست listener ما همیشه آرایه ای از Project برگرداند. در پروژه های بزرگ ممکن است چندین state داشته باشیم و با اینکه پروژه ما چنین پروژه ای نیست اما فرض می کنیم که بعدا می خواهیم آن را بزرگ کنیم. بنابر این فرض این کلاس قرار است یک کلاس پدر برای state های مختلف باشد، مثلا state پروژه ها و state کاربران (لاگین بودن یا نبودن و ...) و state های دیگر. به همین دلیل می گویم که معلوم نیست همیشه projects را داشته باشیم، بلکه کلاس های فرزند مختلف ممکن است چیز های مختلفی را بسازند به همین دلیل generic type خود را از آرایه ای از project ها به کد بالا (آرایه ای از T ها) تغییر داده ام.

توجه داشته باشید که خصوصیت listeners را روی protected گذاشته ام تا کلاس های State دیگر که از این کلاس ارث بری دارند بتوانند به این خصوصیت دسترسی داشته باشند.

حالا برای کلاس ProjectState که قرار است از این کلاس ارث بری کند، باید generic type را مشخص تر و کرده و اصلاحات را اعمال کنیم:

class ProjectState extends State<Project> {
  private projects: Project[] = [];
  private static instance: ProjectState;

  private constructor() {
    super();
  }

پس از مشخص کردن generic type (تنظیم روی Project) به constructor رفته ام و super را صدا زده ام. حالا کدهای این قسمت از پروژه ما تکمیل شده است! تبریک می گویم!

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

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

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

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