עברית

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

דקורטורים (Decorators) ב-TypeScript: שליטה בתבניות תכנות מבוססות מטא-דאטה ליישומים חזקים ועמידים

בנוף העצום של פיתוח תוכנה מודרני, שמירה על בסיסי קוד נקיים, ניתנים להרחבה וקלים לניהול היא בעלת חשיבות עליונה. TypeScript, עם מערכת הטיפוסים החזקה והתכונות המתקדמות שלה, מספקת למפתחים כלים להשיג זאת. בין התכונות המסקרנות והמהפכניות ביותר שלה נמצאים הדקורטורים (Decorators). למרות שבעת כתיבת שורות אלה מדובר עדיין בתכונה ניסיונית (הצעה בשלב 3 עבור ECMAScript), דקורטורים נמצאים בשימוש נרחב במסגרות עבודה (frameworks) כמו Angular ו-TypeORM, ומשנים באופן יסודי את הגישה שלנו לתבניות עיצוב, תכנות מבוסס מטא-דאטה ותכנות היבטי (AOP).

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

הבנת רעיון הליבה: מהו דקורטור?

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

חשבו על דקורטורים כעל הערות (annotations) או תוויות שאתם מחילים על חלקים מהקוד שלכם. תוויות אלו יכולות לאחר מכן להיקרא או להיות מופעלות על ידי חלקים אחרים של היישום או על ידי מסגרות עבודה, לעיתים קרובות בזמן ריצה, כדי לספק פונקציונליות או תצורה נוספת.

התחביר של דקורטור

לפני דקורטורים מופיע הסימן @, ואחריו שם פונקציית הדקורטור. הם ממוקמים מיד לפני ההצהרה שאותה הם מקשטים.

@MyDecorator
class MyClass {
  @AnotherDecorator
  myMethod() {
    // ...
  }
}

הפעלת דקורטורים ב-TypeScript

לפני שתוכלו להשתמש בדקורטורים, עליכם להפעיל את אפשרות המהדר experimentalDecorators בקובץ tsconfig.json שלכם. בנוסף, עבור יכולות השתקפות (reflection) מטא-דאטה מתקדמות (שנמצאות בשימוש תדיר על ידי מסגרות עבודה), תצטרכו גם את emitDecoratorMetadata ואת הפוליפיל reflect-metadata.

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2017",
    "module": "commonjs",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

עליכם להתקין גם את reflect-metadata:

npm install reflect-metadata --save
# or
yarn add reflect-metadata

ולייבא אותו בראש נקודת הכניסה של היישום שלכם (למשל, main.ts או app.ts):

import "reflect-metadata";
// קוד היישום שלכם ממשיך מכאן

מפעל דקורטורים (Decorator Factories): התאמה אישית בקצות אצבעותיכם

בעוד שדקורטור בסיסי הוא פונקציה, לעיתים קרובות תצטרכו להעביר ארגומנטים לדקורטור כדי להגדיר את התנהגותו. זה מושג באמצעות מפעל דקורטורים (decorator factory). מפעל דקורטורים הוא פונקציה שמחזירה את פונקציית הדקורטור בפועל. כאשר אתם מחילים מפעל דקורטורים, אתם קוראים לו עם הארגומנטים שלו, והוא מחזיר את פונקציית הדקורטור ש-TypeScript מחילה על הקוד שלכם.

יצירת דוגמה פשוטה למפעל דקורטורים

בואו ניצור מפעל עבור דקורטור Logger שיכול לרשום הודעות ללוג עם קידומות שונות.

function Logger(prefix: string) {
  return function (target: Function) {
    console.log(`[${prefix}] Class ${target.name} has been defined.`);
  };
}

@Logger("APP_INIT")
class ApplicationBootstrap {
  constructor() {
    console.log("Application is starting...");
  }
}

const app = new ApplicationBootstrap();
// פלט:
// [APP_INIT] Class ApplicationBootstrap has been defined.
// Application is starting...

בדוגמה זו, Logger("APP_INIT") היא קריאת מפעל הדקורטורים. היא מחזירה את פונקציית הדקורטור בפועל אשר מקבלת את target: Function (הבנאי של המחלקה) כארגומנט שלה. זה מאפשר תצורה דינמית של התנהגות הדקורטור.

סוגי דקורטורים ב-TypeScript

TypeScript תומכת בחמישה סוגים נפרדים של דקורטורים, כל אחד ישים לסוג מסוים של הצהרה. חתימת פונקציית הדקורטור משתנה בהתבסס על ההקשר שבו היא מיושמת.

1. דקורטורים של מחלקה (Class Decorators)

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

חתימה:

function ClassDecorator(target: Function) { ... }

ערך מוחזר:

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

מקרי שימוש:

דוגמה לדקורטור מחלקה: הזרקת שירות

דמיינו תרחיש פשוט של הזרקת תלויות שבו אתם רוצים לסמן מחלקה כ-"ניתנת להזרקה" (injectable) ואופציונלית לספק לה שם במאגר.

const InjectableServiceRegistry = new Map<string, Function>();

function Injectable(name?: string) {
  return function<T extends { new(...args: any[]): {} }>(constructor: T) {
    const serviceName = name || constructor.name;
    InjectableServiceRegistry.set(serviceName, constructor);
    console.log(`Registered service: ${serviceName}`);

    // באופן אופציונלי, ניתן להחזיר כאן מחלקה חדשה כדי להרחיב את ההתנהגות
    return class extends constructor {
      createdAt = new Date();
      // מאפיינים או מתודות נוספים לכל השירותים המוזרקים
    };
  };
}

@Injectable("UserService")
class UserDataService {
  getUsers() {
    return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];
  }
}

@Injectable()
class ProductDataService {
  getProducts() {
    return [{ id: 101, name: "Laptop" }, { id: 102, name: "Mouse" }];
  }
}

console.log("--- Services Registered ---");
console.log(Array.from(InjectableServiceRegistry.keys()));

const userServiceConstructor = InjectableServiceRegistry.get("UserService");
if (userServiceConstructor) {
  const userServiceInstance = new userServiceConstructor();
  console.log("Users:", userServiceInstance.getUsers());
  // console.log("User Service Created At:", userServiceInstance.createdAt); // אם נעשה שימוש במחלקה המוחזרת
}

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

2. דקורטורים של מתודה (Method Decorators)

דקורטורים של מתודה מוחלים על הצהרות מתודה. הם מקבלים שלושה ארגומנטים: אובייקט היעד (עבור חברים סטטיים, פונקציית הבנאי; עבור חברי מופע, ה-prototype של המחלקה), שם המתודה, ומתאר המאפיין (property descriptor) של המתודה.

חתימה:

function MethodDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }

ערך מוחזר:

דקורטור מתודה יכול להחזיר PropertyDescriptor חדש. אם הוא עושה זאת, מתאר זה ישמש להגדרת המתודה. זה מאפשר לכם לשנות או להחליף את היישום המקורי של המתודה, מה שהופך אותו לחזק להפליא עבור AOP.

מקרי שימוש:

דוגמה לדקורטור מתודה: ניטור ביצועים

בואו ניצור דקורטור MeasurePerformance כדי לרשום את זמן הריצה של מתודה.

function MeasurePerformance(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function(...args: any[]) {
    const start = process.hrtime.bigint();
    const result = originalMethod.apply(this, args);
    const end = process.hrtime.bigint();
    const duration = Number(end - start) / 1_000_000;
    console.log(`Method "${propertyKey}" executed in ${duration.toFixed(2)} ms`);
    return result;
  };

  return descriptor;
}

class DataProcessor {
  @MeasurePerformance
  processData(data: number[]): number[] {
    // מדמה פעולה מורכבת וגוזלת זמן
    for (let i = 0; i < 1_000_000; i++) {
      Math.sin(i);
    }
    return data.map(n => n * 2);
  }

  @MeasurePerformance
  fetchRemoteData(id: string): Promise<string> {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(`Data for ID: ${id}`);
      }, 500);
    });
  }
}

const processor = new DataProcessor();
processor.processData([1, 2, 3]);
processor.fetchRemoteData("abc").then(result => console.log(result));

הדקורטור MeasurePerformance עוטף את המתודה המקורית בלוגיקת תזמון, ומדפיס את משך הביצוע מבלי להעמיס על הלוגיקה העסקית בתוך המתודה עצמה. זוהי דוגמה קלאסית של תכנות היבטי (AOP).

3. דקורטורים של Accessor

דקורטורים של accessor מוחלים על הצהרות accessor (get ו-set). בדומה לדקורטורים של מתודה, הם מקבלים את אובייקט היעד, שם ה-accessor, ומתאר המאפיין שלו.

חתימה:

function AccessorDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }

ערך מוחזר:

דקורטור של accessor יכול להחזיר PropertyDescriptor חדש, שישמש להגדרת ה-accessor.

מקרי שימוש:

דוגמה לדקורטור Accessor: שמירת Getters במטמון

בואו ניצור דקורטור ששומר במטמון את התוצאה של חישוב getter יקר.

function CachedGetter(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalGetter = descriptor.get;
  const cacheKey = `_cached_${String(propertyKey)}`;

  if (originalGetter) {
    descriptor.get = function() {
      if (this[cacheKey] === undefined) {
        console.log(`[Cache Miss] Computing value for ${String(propertyKey)}`);
        this[cacheKey] = originalGetter.apply(this);
      } else {
        console.log(`[Cache Hit] Using cached value for ${String(propertyKey)}`);
      }
      return this[cacheKey];
    };
  }
  return descriptor;
}

class ReportGenerator {
  private data: number[];

  constructor(data: number[]) {
    this.data = data;
  }

  // מדמה חישוב יקר
  @CachedGetter
  get expensiveSummary(): number {
    console.log("Performing expensive summary calculation...");
    return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;
  }
}

const generator = new ReportGenerator([10, 20, 30, 40, 50]);

console.log("First access:", generator.expensiveSummary);
console.log("Second access:", generator.expensiveSummary);
console.log("Third access:", generator.expensiveSummary);

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

4. דקורטורים של מאפיין (Property Decorators)

דקורטורים של מאפיין מוחלים על הצהרות מאפיין. הם מקבלים שני ארגומנטים: אובייקט היעד (עבור חברים סטטיים, פונקציית הבנאי; עבור חברי מופע, ה-prototype של המחלקה), ושם המאפיין.

חתימה:

function PropertyDecorator(target: Object, propertyKey: string | symbol) { ... }

ערך מוחזר:

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

מקרי שימוש:

דוגמה לדקורטור מאפיין: ולידציית שדה חובה

בואו ניצור דקורטור כדי לסמן מאפיין כ"נדרש" (required) ולאחר מכן לבצע ולידציה בזמן ריצה.

interface ValidationRule {
  property: string | symbol;
  validate: (value: any) => boolean;
  message: string;
}

const validationRules: Map<Function, ValidationRule[]> = new Map();

function Required(target: Object, propertyKey: string | symbol) {
  const rules = validationRules.get(target.constructor) || [];
  rules.push({
    property: propertyKey,
    validate: (value: any) => value !== null && value !== undefined && value !== "",
    message: `${String(propertyKey)} is required.`
  });
  validationRules.set(target.constructor, rules);
}

function validate(instance: any): string[] {
  const classRules = validationRules.get(instance.constructor) || [];
  const errors: string[] = [];

  for (const rule of classRules) {
    if (!rule.validate(instance[rule.property])) {
      errors.push(rule.message);
    }
  }
  return errors;
}

class UserProfile {
  @Required
  firstName: string;

  @Required
  lastName: string;

  age?: number;

  constructor(firstName: string, lastName: string, age?: number) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }
}

const user1 = new UserProfile("John", "Doe", 30);
console.log("User 1 validation errors:", validate(user1)); // []

const user2 = new UserProfile("", "Smith");
console.log("User 2 validation errors:", validate(user2)); // ["firstName is required."]

const user3 = new UserProfile("Alice", "");
console.log("User 3 validation errors:", validate(user3)); // ["lastName is required."]

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

5. דקורטורים של פרמטר (Parameter Decorators)

דקורטורים של פרמטר מוחלים על פרמטרים בתוך בנאי של מחלקה או מתודה. הם מקבלים שלושה ארגומנטים: אובייקט היעד (עבור חברים סטטיים, פונקציית הבנאי; עבור חברי מופע, ה-prototype של המחלקה), שם המתודה (או undefined עבור פרמטרים של בנאי), והאינדקס הסודר של הפרמטר ברשימת הפרמטרים של הפונקציה.

חתימה:

function ParameterDecorator(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) { ... }

ערך מוחזר:

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

מקרי שימוש:

דוגמה לדקורטור פרמטר: הזרקת נתוני בקשה

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

interface ParameterMetadata {
  index: number;
  key: string | symbol;
  resolver: (request: any) => any;
}

const parameterResolvers: Map<Function, Map<string | symbol, ParameterMetadata[]>> = new Map();

function RequestParam(paramName: string) {
  return function (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) {
    const targetKey = propertyKey || "constructor";
    let methodResolvers = parameterResolvers.get(target.constructor);
    if (!methodResolvers) {
      methodResolvers = new Map();
      parameterResolvers.set(target.constructor, methodResolvers);
    }
    const paramMetadata = methodResolvers.get(targetKey) || [];
    paramMetadata.push({
      index: parameterIndex,
      key: targetKey,
      resolver: (request: any) => request[paramName]
    });
    methodResolvers.set(targetKey, paramMetadata);
  };
}

// פונקציה היפותטית של מסגרת עבודה להפעלת מתודה עם פרמטרים שפוענחו
function executeWithParams(instance: any, methodName: string, request: any) {
  const classResolvers = parameterResolvers.get(instance.constructor);
  if (!classResolvers) {
    return (instance[methodName] as Function).apply(instance, []);
  }
  const methodParamMetadata = classResolvers.get(methodName);
  if (!methodParamMetadata) {
    return (instance[methodName] as Function).apply(instance, []);
  }

  const args: any[] = Array(methodParamMetadata.length);
  for (const meta of methodParamMetadata) {
    args[meta.index] = meta.resolver(request);
  }
  return (instance[methodName] as Function).apply(instance, args);
}

class UserController {
  getUser(@RequestParam("id") userId: string, @RequestParam("token") authToken?: string) {
    console.log(`Fetching user with ID: ${userId}, Token: ${authToken || "N/A"}`);
    return { id: userId, name: "Jane Doe" };
  }

  deleteUser(@RequestParam("id") userId: string) {
    console.log(`Deleting user with ID: ${userId}`);
    return { status: "deleted", id: userId };
  }
}

const userController = new UserController();

// מדמה בקשה נכנסת
const mockRequest = {
  id: "user123",
  token: "abc-123",
  someOtherProp: "xyz"
};

console.log("\n--- Executing getUser ---");
executeWithParams(userController, "getUser", mockRequest);

console.log("\n--- Executing deleteUser ---");
executeWithParams(userController, "deleteUser", { id: "user456" });

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

הרכבת דקורטורים וסדר הביצוע

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

מספר דקורטורים על יעד יחיד

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

@DecoratorA
@DecoratorB
class MyClass {
  // ...
}

כאן, DecoratorB יוערך ראשון, ואז DecoratorA. אם הם משנים את המחלקה (למשל, על ידי החזרת בנאי חדש), השינוי מ-DecoratorA יעטוף או יוחל על השינוי מ-DecoratorB.

דוגמה: שרשור דקורטורים של מתודה

שקלו שני דקורטורים של מתודה: LogCall ו-Authorization.

function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[LOG] Calling ${String(propertyKey)} with args:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`[LOG] Method ${String(propertyKey)} returned:`, result);
    return result;
  };
  return descriptor;
}

function Authorization(roles: string[]) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const currentUserRoles = ["admin"]; // מדמה קבלת תפקידי משתמש נוכחי
      const authorized = roles.some(role => currentUserRoles.includes(role));
      if (!authorized) {
        console.warn(`[AUTH] Access denied for ${String(propertyKey)}. Required roles: ${roles.join(", ")}`);
        throw new Error("Unauthorized access");
      }
      console.log(`[AUTH] Access granted for ${String(propertyKey)}`);
      return originalMethod.apply(this, args);
    };
    return descriptor;
  };
}

class SecureService {
  @LogCall
  @Authorization(["admin"])
  deleteSensitiveData(id: string) {
    console.log(`Deleting sensitive data for ID: ${id}`);
    return `Data ID ${id} deleted.`;
  }

  @Authorization(["user"])
  @LogCall // הסדר שונה כאן
  fetchPublicData(query: string) {
    console.log(`Fetching public data with query: ${query}`);
    return `Public data for query: ${query}`; 
  }
}

const service = new SecureService();

try {
  console.log("\n--- Calling deleteSensitiveData (Admin User) ---");
  service.deleteSensitiveData("record123");
} catch (error: any) {
  console.error(error.message);
}

try {
  console.log("\n--- Calling fetchPublicData (Non-Admin User) ---");
  // מדמה משתמש שאינו מנהל המנסה לגשת ל-fetchPublicData הדורש תפקיד 'user'
  const mockUserRoles = ["guest"]; // זה ייכשל באימות
  // כדי להפוך את זה לדינמי, תצטרכו מערכת DI או הקשר סטטי לתפקידי המשתמש הנוכחי.
  // לשם הפשטות, אנו מניחים שלדקורטור Authorization יש גישה להקשר המשתמש הנוכחי.
  // בואו נשנה את דקורטור Authorization כך שיניח תמיד 'admin' לצורך הדגמה,
  // כך שהקריאה הראשונה תצליח והשנייה תיכשל כדי להראות מסלולים שונים.
  
  // נריץ מחדש עם תפקיד 'user' כדי ש-fetchPublicData יצליח.
  // דמיינו ש-currentUserRoles ב-Authorization הופך ל: ['user']
  // לדוגמה זו, נשאיר זאת פשוט ונציג את השפעת הסדר.
  service.fetchPublicData("search term"); // זה יבצע Auth -> Log
} catch (error: any) {
  console.error(error.message);
}

/* פלט צפוי עבור deleteSensitiveData:
[AUTH] Access granted for deleteSensitiveData
[LOG] Calling deleteSensitiveData with args: [ 'record123' ]
Deleting sensitive data for ID: record123
[LOG] Method deleteSensitiveData returned: Data ID record123 deleted.
*/

/* פלט צפוי עבור fetchPublicData (אם למשתמש יש תפקיד 'user'):
[LOG] Calling fetchPublicData with args: [ 'search term' ]
[AUTH] Access granted for fetchPublicData
Fetching public data with query: search term
[LOG] Method fetchPublicData returned: Public data for query: search term
*/

שימו לב לסדר: עבור deleteSensitiveData, הדקורטור Authorization (התחתון) רץ ראשון, ואז LogCall (העליון) עוטף אותו. הלוגיקה הפנימית של Authorization מבוצעת ראשונה. עבור fetchPublicData, הדקורטור LogCall (התחתון) רץ ראשון, ואז Authorization (העליון) עוטף אותו. משמעות הדבר היא שההיבט של LogCall יהיה מחוץ להיבט של Authorization. הבדל זה קריטי עבור עניינים חוצי-חתך (cross-cutting concerns) כמו רישום לוג או טיפול בשגיאות, שבהם סדר הביצוע יכול להשפיע באופן משמעותי על ההתנהגות.

סדר ביצוע עבור יעדים שונים

כאשר למחלקה, לחבריה ולפרמטרים שלה יש דקורטורים, סדר הביצוע מוגדר היטב:

  1. דקורטורים של פרמטרים מוחלים ראשונים, עבור כל פרמטר, החל מהפרמטר האחרון ועד לראשון.
  2. לאחר מכן, דקורטורים של מתודה, Accessor, או מאפיין מוחלים עבור כל חבר.
  3. לבסוף, דקורטורים של מחלקה מוחלים על המחלקה עצמה.

בתוך כל קטגוריה, דקורטורים מרובים על אותו יעד מוחלים מלמטה למעלה (או מימין לשמאל).

דוגמה: סדר ביצוע מלא

function log(message: string) {
  return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {
    if (typeof descriptorOrIndex === 'number') {
      console.log(`Param Decorator: ${message} on parameter #${descriptorOrIndex} of ${String(propertyKey || "constructor")}`);
    } else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {
      if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {
        console.log(`Method/Accessor Decorator: ${message} on ${String(propertyKey)}`);
      } else {
        console.log(`Property Decorator: ${message} on ${String(propertyKey)}`);
      }
    } else {
      console.log(`Class Decorator: ${message} on ${target.name}`);
    }
    return descriptorOrIndex; // החזר מתאר עבור מתודה/accessor, undefined עבור אחרים
  };
}

@log("Class Level D")
@log("Class Level C")
class MyDecoratedClass {
  @log("Static Property A")
  static staticProp: string = "";

  @log("Instance Property B")
  instanceProp: number = 0;

  @log("Method D")
  @log("Method C")
  myMethod(
    @log("Parameter Z") paramZ: string,
    @log("Parameter Y") paramY: number
  ) {
    console.log("Method myMethod executed.");
  }

  @log("Getter/Setter F")
  get myAccessor() {
    return "";
  }

  set myAccessor(value: string) {
    //...
  }

  constructor() {
    console.log("Constructor executed.");
  }
}

new MyDecoratedClass();
// קריאה למתודה כדי להפעיל דקורטור מתודה
new MyDecoratedClass().myMethod("hello", 123);

/* סדר פלט חזוי (בקירוב, תלוי בגרסת TypeScript וקומפילציה ספציפית):
Param Decorator: Parameter Y on parameter #1 of myMethod
Param Decorator: Parameter Z on parameter #0 of myMethod
Property Decorator: Static Property A on staticProp
Property Decorator: Instance Property B on instanceProp
Method/Accessor Decorator: Getter/Setter F on myAccessor
Method/Accessor Decorator: Method C on myMethod
Method/Accessor Decorator: Method D on myMethod
Class Decorator: Class Level C on MyDecoratedClass
Class Decorator: Class Level D on MyDecoratedClass
Constructor executed.
Method myMethod executed.
*/

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

יישומים מעשיים ותבניות עיצוב עם דקורטורים

דקורטורים, במיוחד בשילוב עם הפוליפיל reflect-metadata, פותחים תחום חדש של תכנות מונחה-מטא-דאטה. זה מאפשר תבניות עיצוב חזקות שמפשטות קוד boilerplate ועניינים חוצי-חתך.

1. הזרקת תלויות (DI)

אחד השימושים הבולטים ביותר של דקורטורים הוא במסגרות עבודה של הזרקת תלויות (כמו @Injectable(), @Component() וכו' של Angular, או השימוש הנרחב של NestJS ב-DI). דקורטורים מאפשרים לכם להצהיר על תלויות ישירות על בנאים או מאפיינים, ומאפשרים למסגרת העבודה ליצור ולספק אוטומטית את השירותים הנכונים.

דוגמה: הזרקת שירות פשוטה

import "reflect-metadata"; // חיוני עבור emitDecoratorMetadata

const INJECTABLE_METADATA_KEY = Symbol("injectable");
const INJECT_METADATA_KEY = Symbol("inject");

function Injectable() {
  return function (target: Function) {
    Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);
  };
}

function Inject(token: any) {
  return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {
    const existingInjections: any[] = Reflect.getOwnMetadata(INJECT_METADATA_KEY, target, propertyKey) || [];
    existingInjections[parameterIndex] = token;
    Reflect.defineMetadata(INJECT_METADATA_KEY, existingInjections, target, propertyKey);
  };
}

class Container {
  private static instances = new Map<any, any>();

  static resolve<T>(target: { new (...args: any[]): T }): T {
    if (Container.instances.has(target)) {
      return Container.instances.get(target);
    }

    const isInjectable = Reflect.getMetadata(INJECTABLE_METADATA_KEY, target);
    if (!isInjectable) {
      throw new Error(`Class ${target.name} is not marked as @Injectable.`);
    }

    // קבלת טיפוסי הפרמטרים של הבנאי (דורש emitDecoratorMetadata)
    const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", target) || [];
    const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];

    const dependencies = paramTypes.map((paramType, index) => {
      // השתמש בטוקן @Inject מפורש אם סופק, אחרת הסק את הטיפוס
      const token = explicitInjections[index] || paramType;
      if (token === undefined) {
        throw new Error(`Cannot resolve parameter at index ${index} for ${target.name}. It might be a circular dependency or primitive type without explicit @Inject.`);
      }
      return Container.resolve(token);
    });

    const instance = new target(...dependencies);
    Container.instances.set(target, instance);
    return instance;
  }
}

// הגדרת שירותים
@Injectable()
class DatabaseService {
  connect() {
    console.log("Connecting to database...");
    return "DB Connection";
  }
}

@Injectable()
class AuthService {
  private db: DatabaseService;

  constructor(db: DatabaseService) {
    this.db = db;
  }

  login() {
    console.log(`AuthService: Authenticating using ${this.db.connect()}`);
    return "User logged in";
  }
}

@Injectable()
class UserService {
  private authService: AuthService;
  private dbService: DatabaseService; // דוגמה להזרקה דרך מאפיין באמצעות דקורטור מותאם אישית או תכונת מסגרת עבודה

  constructor(@Inject(AuthService) authService: AuthService,
              @Inject(DatabaseService) dbService: DatabaseService) {
    this.authService = authService;
    this.dbService = dbService;
  }

  getUserProfile() {
    this.authService.login();
    this.dbService.connect();
    console.log("UserService: Fetching user profile...");
    return { id: 1, name: "Global User" };
  }
}

// פענוח השירות הראשי
console.log("--- Resolving UserService ---");
const userService = Container.resolve(UserService);
console.log(userService.getUserProfile());

console.log("\n--- Resolving AuthService (should be cached) ---");
const authService = Container.resolve(AuthService);
authService.login();

דוגמה מורכבת זו מדגימה כיצד הדקורטורים @Injectable ו-@Inject, בשילוב עם reflect-metadata, מאפשרים ל-Container מותאם אישית לפענח ולספק תלויות באופן אוטומטי. המטא-דאטה design:paramtypes הנפלט אוטומטית על ידי TypeScript (כאשר emitDecoratorMetadata מוגדר כ-true) הוא קריטי כאן.

2. תכנות היבטי (AOP)

AOP מתמקד במודולריזציה של עניינים חוצי-חתך (למשל, רישום לוג, אבטחה, טרנזקציות) החוצים מספר מחלקות ומודולים. דקורטורים מתאימים באופן מצוין ליישום מושגי AOP ב-TypeScript.

דוגמה: רישום לוג עם דקורטור מתודה

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

function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[LOG AOP] Entering method: ${String(propertyKey)} with args:`, args);
    try {
      const result = originalMethod.apply(this, args);
      console.log(`[LOG AOP] Exiting method: ${String(propertyKey)} with result:`, result);
      return result;
    } catch (error: any) {
      console.error(`[LOG AOP] Error in method ${String(propertyKey)}:`, error.message);
      throw error;
    }
  };
  return descriptor;
}

class PaymentProcessor {
  @LogMethod
  processPayment(amount: number, currency: string) {
    if (amount <= 0) {
      throw new Error("Payment amount must be positive.");
    }
    console.log(`Processing payment of ${amount} ${currency}...`);
    return `Payment of ${amount} ${currency} processed successfully.`;
  }

  @LogMethod
  refundPayment(transactionId: string) {
    console.log(`Refunding payment for transaction ID: ${transactionId}...`);
    return `Refund initiated for ${transactionId}.`;
  }
}

const processor = new PaymentProcessor();
processor.processPayment(100, "USD");
try {
  processor.processPayment(-50, "EUR");
} catch (error: any) {
  console.error("Caught error:", error.message);
}

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

3. ולידציה וטרנספורמציה

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

דוגמה: ולידציית נתונים עם דקורטורים של מאפיין

הדוגמה של @Required מוקדם יותר כבר הדגימה זאת. הנה דוגמה נוספת עם ולידציית טווח מספרי.

interface FieldValidationRule {
  property: string | symbol;
  validator: (value: any) => boolean;
  message: string;
}

const fieldValidationRules = new Map<Function, FieldValidationRule[]>();

function addValidationRule(target: Object, propertyKey: string | symbol, validator: (value: any) => boolean, message: string) {
  const rules = fieldValidationRules.get(target.constructor) || [];
  rules.push({ property: propertyKey, validator, message });
  fieldValidationRules.set(target.constructor, rules);
}

function IsPositive(target: Object, propertyKey: string | symbol) {
  addValidationRule(target, propertyKey, (value: number) => value > 0, `${String(propertyKey)} must be a positive number.`);
}

function MaxLength(maxLength: number) {
  return function (target: Object, propertyKey: string | symbol) {
    addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} must be at most ${maxLength} characters long.`);
  };
}

class Product {
  @MaxLength(50)
  name: string;

  @IsPositive
  price: number;

  constructor(name: string, price: number) {
    this.name = name;
    this.price = price;
  }

  static validate(instance: any): string[] {
    const errors: string[] = [];
    const rules = fieldValidationRules.get(instance.constructor) || [];
    for (const rule of rules) {
      if (!rule.validator(instance[rule.property])) {
        errors.push(rule.message);
      }
    }
    return errors;
  }
}

const product1 = new Product("Laptop", 1200);
console.log("Product 1 errors:", Product.validate(product1)); // []

const product2 = new Product("Very long product name that exceeds fifty characters limit for testing purpose", 50);
console.log("Product 2 errors:", Product.validate(product2)); // ["name must be at most 50 characters long."]

const product3 = new Product("Book", -10);
console.log("Product 3 errors:", Product.validate(product3)); // ["price must be a positive number."]

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

שיטות עבודה מומלצות ושיקולים

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

מתי להשתמש בדקורטורים (ומתי לא)

השלכות על ביצועים

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

תחזוקתיות וקריאות

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

סטטוס ניסיוני ועתיד הדקורטורים

חשוב לחזור ולהדגיש שדקורטורים של TypeScript מבוססים על הצעה בשלב 3 של TC39. משמעות הדבר היא שהמפרט יציב ברובו אך עדיין יכול לעבור שינויים קלים לפני שיהפוך לחלק מהתקן הרשמי של ECMAScript. מסגרות עבודה כמו Angular אימצו אותם, בהימור על הפיכתם לתקן בסופו של דבר. זה מרמז על רמה מסוימת של סיכון, אם כי בהתחשב באימוץ הנרחב שלהם, שינויים שוברי-תאימות משמעותיים אינם סבירים.

הצעת TC39 התפתחה. היישום הנוכחי של TypeScript מבוסס על גרסה ישנה יותר של ההצעה. קיימת הבחנה בין "דקורטורים מדור קודם" (Legacy Decorators) לבין "דקורטורים סטנדרטיים" (Standard Decorators). כאשר התקן הרשמי יגיע, סביר להניח ש-TypeScript תעדכן את היישום שלה. עבור רוב המפתחים המשתמשים במסגרות עבודה, מעבר זה ינוהל על ידי מסגרת העבודה עצמה. עבור כותבי ספריות, הבנת ההבדלים הדקים בין דקורטורים מדור קודם לדקורטורים סטנדרטיים עתידיים עשויה להיות נחוצה.

אפשרות המהדר emitDecoratorMetadata

אפשרות זו, כאשר מוגדרת כ-true ב-tsconfig.json, מורה למהדר של TypeScript לפלוט מטא-דאטה מסוים של טיפוסים מזמן התכנון (design-time) אל ה-JavaScript המהודר. מטא-דאטה זה כולל את טיפוס הפרמטרים של הבנאי (design:paramtypes), את טיפוס ההחזרה של מתודות (design:returntype), ואת טיפוס המאפיינים (design:type).

מטא-דאטה נלווה זה אינו חלק מזמן הריצה הסטנדרטי של JavaScript. הוא נצרך בדרך כלל על ידי הפוליפיל reflect-metadata, אשר הופך אותו לנגיש באמצעות פונקציות Reflect.getMetadata(). זה קריטי לחלוטין עבור תבניות מתקדמות כמו הזרקת תלויות, שבהן מאגר צריך לדעת את טיפוסי התלויות שמחלקה דורשת ללא תצורה מפורשת.

תבניות מתקדמות עם דקורטורים

ניתן לשלב ולהרחיב דקורטורים כדי לבנות תבניות מתוחכמות עוד יותר.

1. קישוט דקורטורים (דקורטורים מסדר גבוה)

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

// דקורטור המבטיח שמתודה תירשם בלוג וגם דורשת תפקידי מנהל
function AdminAndLoggedMethod() {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // החלת Authorization ראשון (פנימי)
    Authorization(["admin"])(target, propertyKey, descriptor);
    // ואז החלת LogCall (חיצוני)
    LogCall(target, propertyKey, descriptor);

    return descriptor; // החזרת המתאר המעודכן
  };
}

class AdminPanel {
  @AdminAndLoggedMethod()
  deleteUserAccount(userId: string) {
    console.log(`Deleting user account: ${userId}`);
    return `User ${userId} deleted.`;
  }
}

const adminPanel = new AdminPanel();
adminPanel.deleteUserAccount("user007");
/* פלט צפוי (בהנחה של תפקיד מנהל):
[AUTH] Access granted for deleteUserAccount
[LOG] Calling deleteUserAccount with args: [ 'user007' ]
Deleting user account: user007
[LOG] Method deleteUserAccount returned: User user007 deleted.
*/

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

2. שימוש בדקורטורים עבור Mixins

בעוד ש-TypeScript מציעה דרכים אחרות ליישם mixins, ניתן להשתמש בדקורטורים כדי להזריק יכולות למחלקות באופן דקלרטיבי.

function ApplyMixins(constructors: Function[]) {
  return function (derivedConstructor: Function) {
    constructors.forEach(baseConstructor => {
      Object.getOwnPropertyNames(baseConstructor.prototype).forEach(name => {
        Object.defineProperty(
          derivedConstructor.prototype,
          name,
          Object.getOwnPropertyDescriptor(baseConstructor.prototype, name) || Object.create(null)
        );
      });
    });
  };
}

class Disposable {
  isDisposed: boolean = false;
  dispose() {
    this.isDisposed = true;
    console.log("Object disposed.");
  }
}

class Loggable {
  log(message: string) {
    console.log(`[Loggable] ${message}`);
  }
}

@ApplyMixins([Disposable, Loggable])
class MyResource implements Disposable, Loggable {
  // מאפיינים/מתודות אלו מוזרקים על ידי הדקורטור
  isDisposed!: boolean;
  dispose!: () => void;
  log!: (message: string) => void;

  constructor(public name: string) {
    this.log(`Resource ${this.name} created.`);
  }

  cleanUp() {
    this.dispose();
    this.log(`Resource ${this.name} cleaned up.`);
  }
}

const resource = new MyResource("NetworkConnection");
console.log(`Is disposed: ${resource.isDisposed}`);
resource.cleanUp();
console.log(`Is disposed: ${resource.isDisposed}`);

הדקורטור @ApplyMixins מעתיק באופן דינמי מתודות ומאפיינים מבנאי בסיס ל-prototype של המחלקה הנגזרת, ובכך למעשה "מערבב פנימה" (mixing in) פונקציונליות.

סיכום: העצמת פיתוח TypeScript מודרני

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

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

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