اعتبارسنجی با استفاده از decorator ها

Validation Using Decorators

14 مرداد 1399
اعتبار سنجی با استفاده از decorator ها

ما در قسمت قبل یک decorator تعریف کردیم که مشکل کلیدواژه this در جاوا اسکریپت را حل کند اما حل کردن مشکل this یا bind کردن خودکار تنها کاری نیست که decorator ها انجام می دهند. ما انواع مختلفی از decorator ها را داریم که هر کدام کار خاصی را انجام می دهند. به طور مثال می توانیم decorator ای را بنویسیم که کار اعتبارسنجی داده ها را انجام دهد!

فرض کنید در فایل index.html ما یک فرم ساده وجود داشته باشد:

  <body>
    <div id="app"></div>
    <button>Click me</button>
    <form>
      <input type="text" placeholder="Course title" id="title" />
      <input type="text" placeholder="Course price" id="price" />
      <button type="submit">Save</button>
    </form>
  </body>

برای شروع اعتبارسنجی یک کلاس تعریف می کنیم که حاوی title (عنوان) و price (قیمت) دوره های ما باشد:

class Course {
  title: string;
  price: number;

  constructor(t: string, p: number) {
    this.title = t;
    this.price = p;
  }
}

در مرحله بعد یک event-listener را به فرم متصل می کنیم تا مقادیر تایپ شده توسط کاربر را دریافت کنیم:

const courseForm = document.querySelector('form')!;

courseForm.addEventListener('submit', event => {
  event.preventDefault();
  const titleEl = document.getElementById('title') as HTMLInputElement;
  const priceEl = document.getElementById('price') as HTMLInputElement;

  const title = titleEl.value;
  const price = +priceEl.value;
}); 

همانطور که می بینید ابتدا خود فرم را با querySelector گرفته ایم. قبلا هم توضیح داده بودیم که علامت تعجب انتهای آن به تایپ اسکریپت می گوید که ما می دانیم چنین form ای وجود خواهد داشت بنابراین نیازی به خطا گرفتن نیست. سپس event-listener خود را به فرم دریافت شده متصل کرده ایم تا هنگام ثبت فرم آن را preventDefault کنیم (با این کار ثبت فرم متوقف می شود). سپس عناصر input در HTML خود را گرفته و با type casting آن ها را به عنوان input به تایپ اسکریپت معرفی کرده ایم. نهایتا با value مقدار آن ها را خارج کرده ایم. توجه داشته باشید که علامت + در ابتدای priceEl برای تبدیل کردن آن از رشته به عدد است (تمام مقادیر دریافتی از کاربران در فضای وب به صورت رشته ای هستند، حتی اگر در ظاهر عدد باشند).

تا این قسمت، کد ما ثبت نمی شود بلکه فقط آن را دریافت می کنیم اما کابر هیچ چیزی را مشاهده نمی کند. در مرحله بعد باید یک دوره آموزشی را بر اساس کلاسی که تعریف کرده ایم بسازیم:

const courseForm = document.querySelector('form')!;

courseForm.addEventListener('submit', event => {
  event.preventDefault();
  const titleEl = document.getElementById('title') as HTMLInputElement;
  const priceEl = document.getElementById('price') as HTMLInputElement;

  const title = titleEl.value;
  const price = +priceEl.value;

  const createdCourse = new Course(title, price);
  console.log(createdCourse);
}); 

این کد به خوبی کار می کند (می توانید آن را در مرورگر تست کنید) اما یک مشکل دارد. اگر کاربر هیچ چیزی را در فیلدهای input وارد نکنید، باز هم یک دوره آموزشی خالی با قیمت صفر در کنسول مرورگر نمایش داده می شود. اینجاست که مبحث اعتبارسنجی پیش می آید و باید کدهایی را اضافه کنیم که جلوی چنین رفتاری را بگیرند. در اکثر مواقع کسانی که با جاوا اسکریپت ساده کار می کنند، کار اعتبارسنجی را به صورت یک شرط ساده if در همین قسمت پیاده سازی می کنند. مثلا:

  if (title.trim().length > 0) {
    // ورودی معتبر است
  }

اما اگر بخواهیم طبق این روش عمل کنیم باید هر زمانی که خواستیم دوره جدیدی را اضافه کنیم، منطق اعتبارسنجی را دوباره نویسی کنیم. بنابراین بهتر است اعتبارسنجی را با استفاده از decorator ها درون خود کلاس قرار دهیم.

من می خواهم دو decorator را به نام های Required و PositiveNumber تعریف کنم بنابراین همین ابتدا (قبل از آنکه تعریفشان کنم) هر دو را به کلاس خودم اضافه می کنم:

class Course {
  @Required
  title: string;
  @PositiveNumber
  price: number;

  constructor(t: string, p: number) {
    this.title = t;
    this.price = p;
  }
}

البته تابع سومی نیز خواهیم داشت (مثلا نامش را validate می گذاریم) که منطق اعتبارسنجی نهایی را از دو decorator قبلی گرفته و پیاده سازی می کند بنابراین آن را از درون event-listener صدا می زنم:

courseForm.addEventListener('submit', event => {
  event.preventDefault();
  const titleEl = document.getElementById('title') as HTMLInputElement;
  const priceEl = document.getElementById('price') as HTMLInputElement;

  const title = titleEl.value;
  const price = +priceEl.value;

  const createdCourse = new Course(title, price);

  if (!validate(createdCourse)) {
    alert('Invalid input, please try again!');
    return;
  }
  console.log(createdCourse);
});

اینجا گفته ایم که اگر validate شیء createdCourse را بررسی کند و نتیجه اش true نباشد باید یک alert به کاربر نمایش دهیم که داده های ورودی غیرمعتبر هستند. این منطق کلی کار ما است بنابراین باید decorator ها را تعریف کنیم.

در ابتدا برای شروع کار یک interface به شکل زیر ایجاد می کنم:

interface ValidatorConfig {
  [property: string]: {
    [validatableProp: string]: string[]; // ['required', 'positive']
  };
}

این interface برای ذخیره کردن نتیجه اعتبارسنجی توسط دو decorator استفاده خواهد شد. در اینجا قرار است چند خصوصیت داشته باشیم به همین دلیل با علامت های [] شروع کرده ایم. در واقع property نام کلاسی خواهد بود که برایش اعتبارسنجی خواهیم کرد (توجه کنید که خودش یک شیء است). سپس درون آن validatableProp را داریم که خود خصوصیات شیء هستند (آن هایی که decorator ها را با خود دارند). مقدار این خصوصیات باید یک آرایه رشته ای باشد تا بتوانیم مشخص کنیم که هر فیلد required (اجباری) یا positive (مثبت) یا ... باشد.

در مرحله بعد باید محل ذخیره validator (اعتبارسنج) اولیه خود را به صورت یک شیء خالی تعریف کنیم چرا که در هنگام بارگذاری اولیه برنامه هنوز چیزی نداریم که بخواهد ذخیره شود:

const registeredValidators: ValidatorConfig = {};

سپس decorator اول خود را تعریف می کنیم تا مقادیر مورد نظر را به registeredValidators اضافه کند. از آنجایی که این decorator روی یک خصوصیت اعمال می شود، 2 پارامتر می گیرد. اولین پارامتر target و دومین پارامتر نام خصوصیت است:

function Required(target: any, propName: string) {
  registeredValidators[target.constructor.name] = {
    ...registeredValidators[target.constructor.name],
    [propName]: ['required']
  };
}

مقدار target.constructor به ما constructor کلاس را برمی گرداند و با name به نام آن دسترسی پیدا می کنیم که در واقع همان نام کلاس است (گفتم که کلاس های ES6 در اصل همان constructor هستند). سپس برای اینکه قوانین و مقادیر قبلی درون registeredValidators را حذف یا overwrite نکنیم، از اپراتور spread (علامت سه نقطه) استفاده کرده ام تا مقادیر قبلی را حتما کپی کنیم و قانون جدید را به آنان اضافه کنیم. قانون جدید required یا اجباری بودن فیلد مورد نظر ما است.

Decorator بعدی ما نیز دقیقا مانند همین Decorator است:

function PositiveNumber(target: any, propName: string) {
  registeredValidators[target.constructor.name] = {
    ...registeredValidators[target.constructor.name],
    [propName]: ['positive']
  };
}

تنها تفاوت اینجاست که به جای required از positive استفاده کرده ایم (مثبت بودن قیمت دوره آموزشی). اینجا نوبت به تابع سوم (validate) می رسد که قوانین تعریف شده درون دو decorator قبلی را روی شیء اصلی ما پیاده سازی کند:

function validate(obj: any) {
  const objValidatorConfig = registeredValidators[obj.constructor.name];
  if (!objValidatorConfig) {
    return true;
  }
}

در ابتدا به دنبال نام کلاسی درون registeredValidators که قوانین برایش تنظیم شده باشد. همچنین با یک شرط if می گوییم اگر چیزی پیدا نکردی فقط true را برگردان و نیازی به اعتبارسنجی نیست. اما اگر چیزی پیدا کردیم باید درون آن گردش کنیم:

function validate(obj: any) {
  const objValidatorConfig = registeredValidators[obj.constructor.name];
  if (!objValidatorConfig) {
    return true;
  }
  let isValid = true;
  for (const prop in objValidatorConfig) {
    for (const validator of objValidatorConfig[prop]) {
      switch (validator) {
        case 'required':
          isValid = isValid && !!obj[prop];
          break;
        case 'positive':
          isValid = isValid && obj[prop] > 0;
          break;
      }
    }
  }
  return isValid;
}

حلقه اولی که می بینید (گردش در objValidatorConfig) نام خصوصیات را به من می دهد و از آنجای که مقدار هر نام (propName) برابر یک آرایه است (به دو decorator دقت کنید) باید یک حلقه دیگر بنویسیم که آن مقادیر را جداگانه برای ما بررسی کند. این کار بر عهده حلقه دوم است (objValidatorConfig[prop]). حالا برای هر مقدار از یک switch استفاده می کنیم. در هر حالت required یا positive بررسی کرده ایم که isValid باید برابر با خودش و obj[prop] باشد. به زبان ساده تر اگر isValid خودش true بود و obj[prop] نیز برابر true بود آنگاه isValid روی true باقی می ماند و در غیر این صورت برابر false می شود. همچنین اپراتور !! برای تبدیل کردن obj[prop] به یک مقدار واقعی true و false است که باید با آن آشنا باشید.

حالا کدهای ما بدون مشکل اجرا می شوند و می توانید آن ها را در مرورگر خود تست کنید.

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

دیدگاه‌های شما (1 دیدگاه)

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

حسین
28 مرداد 1399
مثل همیشه عالی ، ولی یه سوال اگر بخوایم روی یک پراپرتی هم Required باشه هم PositiveNumber آیا جوابگو هست؟ یا باید در آرایه validator های پراپرتی ها هم مقادیر قبلی رو spread کنیم؟

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

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