پروژه Drag & Drop: مدیریت state برنامه با singleton

Drag & Drop Project: Managing Program State with Singleton

16 مرداد 1399
پروژه ی Drag & Drop: مدیریت state برنامه با singleton

در این قسمت هدف ما رساندن یک پروژه از A به B است، یعنی زمانی که کاربر پروژه جدیدی را می سازند ما باید یک شیء جدید برایش بسازیم و سپس آن شیء را به کلاس projectList ارسال می کنیم تا درون تگ های ul قرار بگیرد. برای این کار می توانیم به سادگی با دستور getElementById به کدهای HTML دسترسی داشته باشیم اما من می خواهم برنامه ما پیشرفته تر از این حرف ها باشد تا تمرین کردن مان نتیجه بدهد. روش بعدی پاس دادن یک متد تعریف شده به constructor یک کلاس دیگر است تا بتوانیم درون آن کلاس از این متد استفاده کنیم که یک روش کاملا صحیح می باشد اما من می خواهم از راه حل انتزاعی تری استفاده کنم.

من یک کلاس می سازم که مدیریت state برنامه ما را بر عهده می گیرد و سپس در قسمت هایی از برنامه که مد نظرمان هستند از event-listener ها استفاده می کنیم. اگر با برنامه نویسی شیء گرا یا آنگولار یا react و redux آشنا باشید احتمالا این الگو نیز برایتان آشنا باشد، یک شیء سراسری که State ما می باشد و سپس event-listener هایی که به تغییرات ایجاد شده گوش می دهند. بر همین اساس در ابتدای فایل App.ts یک کلاس به نام ProjectState ایجاد می کنم:

// Project State Management

class ProjectState {
    private projects: any[] = [];
}

هدف من این است که آرایه ای از پروژه ها داشته باشم و فعلا تایپ آن را روی any می گذاریم چرا که هنوز نمی دانم ساختار آن به چه شکلی خواهد بود (بعدا آن را تغییر خواهیم داد). در مرحله بعد باید متدی داشته باشیم که پروژه های جدید را از فرم دریافت کرده (از کاربر) و آن ها را به این آرایه اضافه کند:

// Project State Management

class ProjectState {
    private projects: any[] = [];

    addProject(title: string, description: string, numOfPeople: number) {
        const newProject = {
            id: Math.random().toString(),
            title: title,
            description: description,
            people: numOfPeople
        };
        this.projects.push(newProject);
    }
}

من نام این متد را addProject گذاشته ام که سه پارامتر ورودی دارد: title یا عنوان که از نوع رشته است، description یا توضیحات پروژه که باز هم رشته ای است و numOfPeople یا تعداد افراد حاضر در پروژه که عددی است. سپس درون این متد یک شیء ساده به نام newProject ساخته ام. ما فعلا سروری نداریم که بخواهد به ما id خاصی بدهد بنابراین از تابع Math.random استفاده کرده ام تا عددی تصادفی برایم بسازد. این روش اصولی نیست چرا که ممکن است دو عدد تصادفی با هم یکسان باشند اما فعلا برای پیش بردن برنامه به همین روش اکتفا می کنیم. در نهایت آن را با استفاده از toString به رشته تبدیل کرده ام. برای خصوصیات بعدی نیز از همان مقادیر ورودی استفاده کرده ام تا title و Description و people را تعریف کنم. در مرحله بعد این شیء را به درون آرایه پروژه ها push (اضافه) می کنیم.

حالا سوال اینجاست که چطور می توانیم این متد را از درون کلاس دیگر (کلاس ProjectInput) صدا بزنیم؟ ما باید آن را درون متد submitHandler از کلاس ProjectInput صدا بزنیم تا هنگامی که کاربر داده هایش را وارد و سپس ثبت کرد، addProject بتواند آن را به state ما اضافه کند. از طرفی چطور کاری کنیم که با هر بار تغییر آرایه پروژه ها (به زبان ساده تر، state ما) آرایه جدید به کلاس ProjectList پاس داده شود؟ این ها دو مشکل اساسی برای برنامه ما هستند.

برای حل این مشکل ابتدا باید یک نمونه از همین کلاس را دقیقا پس از تعریفش بسازیم:

// Project State Management

class ProjectState {
    private projects: any[] = [];

    addProject(title: string, description: string, numOfPeople: number) {
        const newProject = {
            id: Math.random().toString(),
            title: title,
            description: description,
            people: numOfPeople
        };
        this.projects.push(newProject);
    }
}

const projectState = new ProjectState();

با این کار یک ثابت سراسری به نام projectState داریم که می تواند از هر قسمتی از فایل صدا زده شود (چه داخل و چه خارج از کلاس های دیگر). این کار مشکل ما را حل می کند اما همانطور که از عنوان این مقاله می بینید من دوست دارم از الگوی singleton برای تعریف این کلاس استفاده کنم. به همین دلیل می توانیم از یک private constructor استفاده کنیم و کدهایمان را به شکل زیر بنویسیم:

// Project State Management

class ProjectState {
    private projects: any[] = [];
    private static instance: ProjectState;

    private constructor() { }

    static getInstance() {
        if (this.instance) {
            return this.instance;
        }
        this.instance = new ProjectState();
        return this.instance;
    }

    addProject(title: string, description: string, numOfPeople: number) {
        const newProject = {
            id: Math.random().toString(),
            title: title,
            description: description,
            people: numOfPeople
        };
        this.projects.push(newProject);
    }
}

const projectState = ProjectState.getInstance();

در ابتدا یک خصوصیت به نام instances تعریف کرده ام که از نوع استاتیک است و تایپ آن را ProjectState (خود کلاس) گذاشته ام. سپس طبق الگوی singleton یک private constructor داریم. در مرحله بعد متدی استاتیک به نام getInstance را تعریف کرده ام. زمانی که this.instance وجود داشته باشد (قبلا یک نمونه از این شیء ساخته شده باشد) همان را برمی گردانیم و در غیر این صورت یک نمونه (instance) جدید ساخته و آن را برمی گردانیم. در نهایت ثابت projectState را ویرایش کرده ایم تا getInstance را صدا بزند. با این کار مطمئن می شویم که همیشه در حال کار با یک شیء واحد هستیم و در سر تا سر برنامه فقط یک شیء از این نوع را داریم.

حالا به کلاس ProjectInput و متد submitHandler می رویم تا به جای log کردن داده ها از کلاس تعریف شده خودمان استفاده کنیم:

  @autobind
  private submitHandler(event: Event) {
    event.preventDefault();
    const userInput = this.gatherUserInput();
    if (Array.isArray(userInput)) {
      const [title, desc, people] = userInput;
      projectState.addProject(title, desc, people);
      this.clearInputs();
    }
  }

بدین شکل به جای log کردن داده ها آن ها را به addProject می دهیم. در مرحله بعد باید این اطلاعات جدید را که تازه به State اضافه شده است، به کلاس projectList ارسال کنیم تا حالت به روز رسانی شده را نمایش دهد. برای این کار به کلاس ProjectState رفته و خصوصیتی دیگر را تعریف می کنیم که لیستی از event-listener های ما خواهد بود و هر بار که تغییری رخ می دهد، صدا زده خواهد شد:

class ProjectState {
    private listeners: any[] = [];
    private projects: any[] = [];
    private static instance: ProjectState;
// بقیه کدها //

فعلا این خصوصیت جدید (listeners) را نیز از تایپ any تعیین می کنیم تا بعدا آن را تغییر دهیم. در مرحله بعد باید متد مربوط به این خصوصیت را تعریف کنیم:

    static getInstance() {
      if (this.instance) {
        return this.instance;
      }
      this.instance = new ProjectState();
      return this.instance;
    }
  
    addListener(listenerFn: Function) {
      this.listeners.push(listenerFn);
    }

این متد یک تابع listener را دریافت می کند و آن را به لیست listener های ما push (اضافه) می کند. ما می خواهیم کاری کنیم که با ایجاد هر تغییر (مثلا اضافه کردن یک پروژه جدید) تمام توابع listener را صدا بزنیم. من برای این کار از حلقه for استفاده می کنم:

     addProject(title: string, description: string, numOfPeople: number) {
      const newProject = {
        id: Math.random().toString(),
        title: title,
        description: description,
        people: numOfPeople
      };
      this.projects.push(newProject);
      for (const listenerFn of this.listeners) {
        listenerFn(this.projects.slice());
      }
    }

همانطور که مشاهده می کنید در انتهای این متد حلقه for خودم را اضافه کرده ام که بین توابع موجود در آرایه listeners گردش کرده و هر کدام از آن ها را صدا می زند. من یک Slice از پروژه ها را به این listener ها پاس داده ام چرا که می خواهم یک کپی از آن آرایه را برگردانم، نه اینکه خود آرایه اصلی را برگردانم. اگر آرایه اصلی را پاس بدهیم، می توانیم آن را از خارج از کلاس ویرایش کنیم، بنابراین با این کار مطمئن می شوم که آرایه از قسمتی که listener صدا زده می شود قابل ویرایش نیست.

حالا به کلاس ProjectList رفته و constructor را پیدا می کنیم تا یک خصوصیت جدید را در آن ایجاد کنیم:

// ProjectList Class
class ProjectList {
  templateElement: HTMLTemplateElement;
  hostElement: HTMLDivElement;
  element: HTMLElement;
  assignedProjects: any[];
// بقیه کدها //

این خصوصیت جدید یک آرایه از پروژه های ما می باشد تا بدانیم این کلاس چه پروژه هایی دارد. سپس قبل از آنکه محتوا را attach و renderContent را صدا بزنیم، از addListener استفاده می کنم تا یک listener جدید به آن اضافه کنیم:

// بقیه کدها //
    this.element = importedNode.firstElementChild as HTMLElement;
    this.element.id = `${this.type}-projects`;

    projectState.addListener((projects: any[]) => {
      this.assignedProjects = projects;
      this.renderProjects();
    });

    this.attach();
    this.renderContent();
  }

در اینجا متدی به نام addListener را صدا می زنیم که پارامتر ورودی اش یک تابع خواهد بود (یادتان باشد که این اتفاقات زمانی می افتد که تغییری در State ما رخ دهد، یعنی پروژه جدیدی اضافه شود). سپس این تابع لیست پروژه های قدیمی (assignedProjects) را با پروژه های جدید (projects) جایگزین می کند. در نهایت متدی به نام renderProjects را صدا زده ایم که البته هنوز تعریف نشده است بنابراین آن را هم تعریف می کنیم:

// بقیه کدها //
    projectState.addListener((projects: any[]) => {
      this.assignedProjects = projects;
      this.renderProjects();
    });

    this.attach();
    this.renderContent();
  }

  private renderProjects() {
    const listEl = document.getElementById(`${this.type}-projects-list`)! as HTMLUListElement;
    for (const prjItem of this.assignedProjects) {
      const listItem = document.createElement('li');
      listItem.textContent = prjItem.title;
      listEl.appendChild(listItem)
    }
  }

ما در این متد ابتدا ul را دریافت کرده و سپس با یک حلقه for بین پروژه های پاس داده شده به این کلاس (assignedProjects) گردش می کنیم. درون این شرط برای هر کدام از پروژه ها، یک li جدید ساخته و متن آن را برابر با title دریافتی می گذاریم. در نهایت نیز آن را به ul خودمان append می کنیم.

در حال حاضر خطا می گیریم که assignedProjects را به عنوان خصوصیت تعریف کرده ایم اما هیچ گاه در constructor از آن استفاده نکرده ایم. برای حل این مشکل درون constructor می گوییم:

  constructor(private type: 'active' | 'finished') {
    this.templateElement = document.getElementById(
      'project-list'
    )! as HTMLTemplateElement;
    this.hostElement = document.getElementById('app')! as HTMLDivElement;
    this.assignedProjects = [];

با تنظیم آن روی یک آرایه خالی از آن استفاده کرده ایم تا خطا را برطرف کنیم. کدهای ما فعلا دارای باگ هستند (البته کار می کنند) و در قسمت بعد تکمیل و تصحیح خواهیم کرد.

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

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

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