• בלוג
  • תבנית העיצוב Decorator עבור פונקציות ב JavaScript

תבנית העיצוב Decorator עבור פונקציות ב JavaScript

11/08/2019

אחת מתבניות העיצוב הפופולריות של הזמן האחרות היא Decorator, אבל לא זו הקלאסית של תכנות מונחה עצמים אלא Decorator של פונקציות. בשנים האחרונות התבנית קיבלה תמיכה מובנית בשפות תכנות מובילות (בין השאר Python ו JavaScript) ומאפשרת שיתוף קוד בקלות בין פונקציות. בפוסט זה נראה על מה המהומה, למה זה חשוב ואיך להשתמש בזה.

1. מי צריך לשתף קוד בין פונקציות

הרבה פעמים כשאנחנו חושבים על שיתוף קוד בין פונקציות הרעיון הראשון יהיה ליצור פונקציה שלישית שתכיל את הלוגיקה המשותפת ולכתוב את הקוד כך ששתי הפונקציות שלנו יקראו לפונקציה השלישית. ניקח כדוגמא שתי פונקציות שצריכות לחשב עצרת של מספר:

function double_factorial(n) {
  const factorial_n = Array.from(Array(4).keys()).
    map(n => n + 1).
    reduce((a, b) => a * b, 1)

  return factorial_n * 2;
}

function triple_factorial(n) {
  const factorial_n = Array.from(Array(4).keys()).
    map(n => n + 1).
    reduce((a, b) => a * b, 1)

  return factorial_n * 3;
}

אז ברור שהגיוני להוציא החוצה את החלק המשותף לפונקציה שלישית ולקבל קוד הרבה יותר נקי וממוקד:

function factorial(n) {
  return Array.from(Array(4).keys()).
    map(n => n + 1).
    reduce((a, b) => a * b, 1)
}

function double_factorial(n) {
  return factorial(n) * 2;
}

function triple_factorial(n) {
  return factorial(n) * 3;
}

הבעיה שיש לוגיקה שלא כל כך פשוט לשתף בין פונקציות בצורה של יצירת פונקציה שלישית. קחו לדוגמא מנגנון שאומר שפונקציה מסוימת תפעל רק חצי שניה אחרי הפעם האחרונה שקראו לה - למשל כדי לממש באפליקציה מנגנון שמעדכן את השרת רק אחרי שמשתמש סיים להקליד טקסט בתיבה. אנחנו יכולים עם כל שינוי טקסט לקרוא לפונקציה, אבל הפונקציה תדע לעדכן את השרת רק אחרי שחצי שניה לא היו עדכונים. אז הנה שוב שתי פונקציות העצרת שלנו הפעם עם המנגנון החדש:

function factorial(n) {
  return Array.from(Array(4).keys()).
    map(n => n + 1).
    reduce((a, b) => a * b, 1)
}

let double_factorial_timer = null
function double_factorial(n) {  
  if (double_factorial_timer) {
    clearTimeout(double_factorial_timer);
  }
  double_factorial_timer = setTimeout(function() {
    console.log(factorial(n) * 2);
  }, 500);
}

let triple_factorial_timer = null
function triple_factorial(n) {
  if (triple_factorial_timer) {
    clearTimeout(triple_factorial_timer);
  }
  triple_factorial_timer = setTimeout(function() {
    console.log(factorial(n) * 3);
  }, 500);
}

איך משתפים את הקוד הזה בין שתי הפונקציות בצורה קלה?

2. הפיתרון: דקורטור

וכאן אנחנו פוגשים בפיתרון נחמד שנקרא Decorator. הרעיון הוא להחליף את הפונקציה בפונקציה אחרת שבה תהיה הלוגיקה והיא גם תפעיל את הפונקציה המקורית שלנו. הפונקציה האחרת מקבלת פונקציה בתור פרמטר ומחזירה פונקציה חדשה עם ההתנהגות שאנחנו צריכים.

במקרה של הפעלה רק אחרי חצי שניה הקוד נראה כך:

function factorial(n) {
  return Array.from(Array(4).keys()).
    map(n => n + 1).
    reduce((a, b) => a * b, 1)
}

function execute_in_half_a_second(f) {
  let timer = null;
  return function(...args) {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(function() {
      f(...args);
    }, 500);
  }
}

function double_factorial(n) {  
  console.log(factorial(n) * 2);
}

function triple_factorial(n) {
  console.log(factorial(n) * 3);
}

double_factorial = execute_in_half_a_second(double_factorial);
triple_factorial = execute_in_half_a_second(triple_factorial);

double_factorial(4);
double_factorial(4);
double_factorial(4);
double_factorial(4);

ושוב הקוד שלנו הפך נקי וממוקד יותר והלוגיקה המשותפת נכתבת רק פעם אחד ובמקום אחד.

3. תמיכה מובנית בשפת JavaScript

דפדפנים וגם Node עדיין לא תומכים ב Decorators בצורה מובנית, אבל התמיכה כבר מאופיינת ואם אתם משתמשים ב Babel אפשר גם להפעיל את הקוד. מבחינת מימוש יש מספר הבדלים לעומת התבנית שראינו:

  1. דקורייטור יכול לשמש אך ורק על מתודות של מחלקה (או על המחלקה כולה אבל זה סיפור ליום אחר).

  2. הדקורייטור מקבל כקלט פרמטר שמכיל מידע על הפונקציה אותה הוא רוצה להחליף, והחלפת הפונקציה נעשית על ידי השמה לשדה value של אותו האוביקט (במקום להחזיר את הפונקציה החדשה).

סך הכל חיבור שני הסעיפים נותן לנו את הקוד הבא שמשתמש ב Decorators מובנים בשפה:

class Main {
  @execute_in_half_a_second
  double_factorial(n) {  
    console.log(factorial(n) * 2);
  }

  @execute_in_half_a_second
  triple_factorial(n) {
    console.log(factorial(n) * 3);
  }
}

function factorial(n) {
  return Array.from(Array(4).keys()).
    map(n => n + 1).
    reduce((a, b) => a * b, 1)
}

function execute_in_half_a_second({ descriptor }) {
  const originalMethod = descriptor.value;
  let timer = null;
  console.dir(descriptor);
  descriptor.value = function(...args) {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(function() {
      originalMethod(...args);
    }, 500);
  }
}

const m = new Main();
m.double_factorial(4);
m.double_factorial(4);
m.double_factorial(4);
m.double_factorial(4);

וההרפתקנים שביניכם מוזמנים לראות את הקוד בשידור חי וגם לשחק איתו קצת מתוך הקודפן הבא (נשים לב שלמרות שהפעלנו את הפונקציה 4 פעמים בפועל היא רצה רק פעם אחת ולכן המספר 48 מופיע רק פעם אחת על המסך):