العربية

استكشف قوة مزخرفات TypeScript لبرمجة البيانات الوصفية، والبرمجة الموجهة بالجوانب، وتعزيز التعليمات البرمجية بأنماط تعريفية. دليل شامل للمطورين العالميين.

مزخرفات TypeScript: إتقان أنماط برمجة البيانات الوصفية لتطبيقات قوية

في المشهد الواسع لتطوير البرمجيات الحديثة، يعد الحفاظ على قواعد التعليمات البرمجية النظيفة والقابلة للتوسع والقابلة للإدارة أمرًا بالغ الأهمية. يوفر TypeScript، بنظام الأنواع القوي والميزات المتقدمة، للمطورين أدوات لتحقيق ذلك. من بين ميزاته الأكثر إثارة للاهتمام وتحويلًا المزخرفات. على الرغم من أنها لا تزال ميزة تجريبية في وقت الكتابة (اقتراح المرحلة 3 لـ ECMAScript)، إلا أنه يتم استخدام المزخرفات على نطاق واسع في أطر عمل مثل Angular و TypeORM، مما يغير بشكل أساسي كيفية تعاملنا مع أنماط التصميم، وبرمجة البيانات الوصفية، والبرمجة الموجهة بالجوانب (AOP).

سيتعمق هذا الدليل الشامل في مزخرفات TypeScript، ويستكشف آلياتها، وأنواعها المختلفة، وتطبيقاتها العملية، وأفضل الممارسات. سواء كنت تقوم ببناء تطبيقات للمؤسسات واسعة النطاق، أو الخدمات المصغرة، أو واجهات الويب من جانب العميل، فإن فهم المزخرفات سيمكنك من كتابة تعليمات برمجية TypeScript تعريفية وقابلة للصيانة وأكثر قوة.

فهم المفهوم الأساسي: ما هي المزخرفة؟

في جوهرها، المزخرفة هي نوع خاص من الإعلانات التي يمكن ربطها بإعلان فئة، أو طريقة، أو ملحق، أو خاصية، أو معلمة. المزخرفات هي وظائف تعيد قيمة جديدة (أو تعدل قيمة موجودة) للهدف الذي تقوم بتزيينه. الغرض الأساسي منها هو إضافة بيانات وصفية أو تغيير سلوك الإعلان الذي ترتبط به، دون تعديل بنية التعليمات البرمجية الأساسية مباشرة. هذه الطريقة الخارجية والتعريفية لتعزيز التعليمات البرمجية قوية بشكل لا يصدق.

فكر في المزخرفات على أنها تعليقات توضيحية أو تسميات تقوم بتطبيقها على أجزاء من التعليمات البرمجية الخاصة بك. يمكن بعد ذلك قراءة هذه التسميات أو التصرف عليها بواسطة أجزاء أخرى من تطبيقك أو بواسطة أطر العمل، غالبًا في وقت التشغيل، لتوفير وظائف إضافية أو تكوين.

صيغة المزخرفة

يتم مسبوق المزخرفات بالرمز @، متبوعًا باسم وظيفة المزخرفة. يتم وضعها مباشرة قبل الإعلان الذي تقوم بتزيينه.

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

تمكين المزخرفات في TypeScript

قبل أن تتمكن من استخدام المزخرفات، يجب عليك تمكين خيار المترجم experimentalDecorators في ملف tsconfig.json الخاص بك. بالإضافة إلى ذلك، للحصول على إمكانيات انعكاس البيانات الوصفية المتقدمة (التي غالبًا ما تستخدمها أطر العمل)، ستحتاج أيضًا إلى 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
# أو
yarn add reflect-metadata

واستيراده في الجزء العلوي من نقطة إدخال تطبيقك (مثل main.ts أو app.ts):

import "reflect-metadata";
// يتبع تعليماتك البرمجية الخاصة بالتطبيق

مصانع المزخرفات: التخصيص في متناول يدك

بينما المزخرفة الأساسية هي وظيفة، غالبًا ما تحتاج إلى تمرير وسيطات إلى المزخرفة لتكوين سلوكها. يتم تحقيق ذلك باستخدام مصنع مزخرفة. مصنع المزخرفة هو وظيفة تعيد وظيفة المزخرفة الفعلية. عند تطبيق مصنع مزخرفة، يمكنك استدعاؤه بوسيطاته، ثم يعيد وظيفة المزخرفة التي يطبقها TypeScript على التعليمات البرمجية الخاصة بك.

إنشاء مثال بسيط لمصنع مزخرفة

لننشئ مصنعًا لمزخرفة Logger يمكنه تسجيل الرسائل ببادئات مختلفة.

function Logger(prefix: string) {
  return function (target: Function) {
    console.log(`[${prefix}] تم تعريف الفئة ${target.name}.`);
  };
}

@Logger("APP_INIT")
class ApplicationBootstrap {
  constructor() {
    console.log("بدء تشغيل التطبيق...");
  }
}

const app = new ApplicationBootstrap();
// الناتج:
// [APP_INIT] تم تعريف الفئة ApplicationBootstrap.
// بدء تشغيل التطبيق...

في هذا المثال، Logger("APP_INIT") هو استدعاء مصنع المزخرفة. يعيد وظيفة المزخرفة الفعلية التي تأخذ target: Function (منشئ الفئة) كوسيطة. يسمح هذا بالتكوين الديناميكي لسلوك المزخرفة.

أنواع المزخرفات في TypeScript

يدعم TypeScript خمسة أنواع مميزة من المزخرفات، كل منها ينطبق على نوع معين من الإعلانات. يختلف توقيع وظيفة المزخرفة بناءً على السياق الذي تطبق فيه.

1. مزخرفات الفئة

تطبق مزخرفات الفئة على إعلانات الفئة. تتلقى وظيفة المزخرفة منشئ الفئة كوسيطة وحيدة. يمكن للمزخرفة الفئة ملاحظة تعريف الفئة أو تعديله أو حتى استبداله.

التوقيع:

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

قيمة الإرجاع:

إذا أعادت المزخرفة الفئة قيمة، فإنها ستحل محل إعلان الفئة بوظيفة المنشئ المقدمة. هذه ميزة قوية، غالبًا ما تستخدم في المزيج أو لتعزيز الفئة. إذا لم يتم إرجاع قيمة، يتم استخدام الفئة الأصلية.

حالات الاستخدام:

مثال لمزخرفة الفئة: حقن خدمة

تخيل سيناريو حقن تبعية بسيط حيث تريد وضع علامة على فئة كـ "قابلة للحقن" وتوفير اسم لها اختياريًا في حاوية.

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(`تم تسجيل الخدمة: ${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("--- الخدمات المسجلة ---");
console.log(Array.from(InjectableServiceRegistry.keys()));

const userServiceConstructor = InjectableServiceRegistry.get("UserService");
if (userServiceConstructor) {
  const userServiceInstance = new userServiceConstructor();
  console.log("المستخدمون:", userServiceInstance.getUsers());
  // console.log("تم إنشاء خدمة المستخدم في:", userServiceInstance.createdAt); // إذا تم استخدام الفئة المرجعة
}

يوضح هذا المثال كيف يمكن للمزخرفة الفئة تسجيل فئة وحتى تعديل منشئها. المزخرفة Injectable تجعل الفئة قابلة للاكتشاف بواسطة نظام حقن تبعية نظري.

2. مزخرفات الطرق

تطبق مزخرفات الطرق على إعلانات الطرق. تتلقى ثلاث وسيطات: الكائن الهدف (للأعضاء الثابتين، وظيفة المنشئ؛ للأعضاء المثيل، النموذج الأولي للفئة)، اسم الطريقة، ووصف خاصية الطريقة.

التوقيع:

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(`تم تنفيذ الطريقة "${propertyKey}" في ${duration.toFixed(2)} مللي ثانية`);
    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(`بيانات للمعرف: ${id}`);
      }, 500);
    });
  }
}

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

تقوم المزخرفة MeasurePerformance بلف الطريقة الأصلية بمنطق التوقيت، وطباعة مدة التنفيذ دون تشويش على منطق العمل داخل الطريقة نفسها. هذا مثال كلاسيكي للبرمجة الموجهة بالجوانب (AOP).

3. مزخرفات الملحقات

تطبق مزخرفات الملحقات على إعلانات الملحقات (get و set). على غرار مزخرفات الطرق، تتلقى الكائن الهدف، واسم الملحق، ووصف خاصيته.

التوقيع:

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

قيمة الإرجاع:

يمكن لمزخرفة الملحق إرجاع PropertyDescriptor جديد، والذي سيتم استخدامه لتحديد الملحق.

حالات الاستخدام:

مثال لمزخرفة الملحق: تخزين النتائج المحسوبة للملحقات مؤقتًا

لننشئ مزخرفة تخزن نتيجة حساب ملحق مكلف مؤقتًا.

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(`[فشل التخزين المؤقت] حساب القيمة لـ ${String(propertyKey)}`);
        this[cacheKey] = originalGetter.apply(this);
      } else {
        console.log(`[نجاح التخزين المؤقت] استخدام القيمة المخزنة مؤقتًا لـ ${String(propertyKey)}`);
      }
      return this[cacheKey];
    };
  }
  return descriptor;
}

class ReportGenerator {
  private data: number[];

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

  // محاكاة حساب مكلف
  @CachedGetter
  get expensiveSummary(): number {
    console.log("إجراء حساب ملخص مكلف...");
    return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;
  }
}

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

console.log("الوصول الأول:", generator.expensiveSummary);
console.log("الوصول الثاني:", generator.expensiveSummary);
console.log("الوصول الثالث:", generator.expensiveSummary);

تضمن هذه المزخرفة أن حساب الملحق expensiveSummary يتم مرة واحدة فقط، وتعيد الاستدعاءات اللاحقة القيمة المخزنة مؤقتًا. هذا النمط مفيد جدًا لتحسين الأداء حيث يتضمن الوصول إلى الخصائص حسابات ثقيلة أو استدعاءات خارجية.

4. مزخرفات الخصائص

تطبق مزخرفات الخصائص على إعلانات الخصائص. تتلقى وسيطتين: الكائن الهدف (للأعضاء الثابتين، وظيفة المنشئ؛ للأعضاء المثيل، النموذج الأولي للفئة)، واسم الخاصية.

التوقيع:

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

قيمة الإرجاع:

لا يمكن لمزخرفات الخصائص إرجاع أي قيمة. استخدامها الأساسي هو تسجيل بيانات وصفية حول الخاصية. لا يمكنها تعديل قيمة الخاصية أو وصفها مباشرة في وقت الزخرفة، نظرًا لأن وصف الخاصية لم يتم تحديده بالكامل بعد عند تشغيل مزخرفات الخصائص.

حالات الاستخدام:

مثال لمزخرفة الخاصية: التحقق من حقل مطلوب

لننشئ مزخرفة لوضع علامة على خاصية كـ "مطلوبة" ثم التحقق منها في وقت التشغيل.

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)} مطلوب.`
  });
  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("أخطاء التحقق من المستخدم 1:", validate(user1)); // []

const user2 = new UserProfile("", "Smith");
console.log("أخطاء التحقق من المستخدم 2:", validate(user2)); // ["firstName مطلوب."]

const user3 = new UserProfile("Alice", "");
console.log("أخطاء التحقق من المستخدم 3:", validate(user3)); // ["lastName مطلوب."]

تقوم المزخرفة Required ببساطة بتسجيل قاعدة التحقق مع خريطة validationRules المركزية. بعد ذلك، تستخدم وظيفة validate منفصلة هذه البيانات الوصفية للتحقق من المثيل في وقت التشغيل. يفصل هذا النمط منطق التحقق من صحة تعريف البيانات، مما يجعله قابلاً لإعادة الاستخدام ونظيفًا.

5. مزخرفات المعلمات

تطبق مزخرفات المعلمات على معلمات داخل منشئ الفئة أو الطريقة. تتلقى ثلاث وسيطات: الكائن الهدف (للأعضاء الثابتين، وظيفة المنشئ؛ للأعضاء المثيل، النموذج الأولي للفئة)، اسم الطريقة (أو 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(`جلب المستخدم بالمعرف: ${userId}، الرمز: ${authToken || "غير متوفر"}`);
    return { id: userId, name: "Jane Doe" };
  }

  deleteUser(@RequestParam("id") userId: string) {
    console.log(`حذف المستخدم بالمعرف: ${userId}`);
    return { status: "deleted", id: userId };
  }
}

const userController = new UserController();

// محاكاة طلب وارد
const mockRequest = {
  id: "user123",
  token: "abc-123",
  someOtherProp: "xyz"
};

console.log("\n--- تنفيذ getUser ---");
executeWithParams(userController, "getUser", mockRequest);

console.log("\n--- تنفيذ deleteUser ---");
executeWithParams(userController, "deleteUser", { id: "user456" });

يوضح هذا المثال كيف يمكن لمزخرفات المعلمات جمع المعلومات حول معلمات الطرق المطلوبة. يمكن لإطار عمل بعد ذلك استخدام هذه البيانات الوصفية المجمعة لحل وتوفير القيم المناسبة تلقائيًا عند استدعاء الطريقة، مما يبسط منطق وحدة التحكم أو الخدمة بشكل كبير.

تكوين المزخرفات وترتيب التنفيذ

يمكن تطبيق المزخرفات في مجموعات مختلفة، وفهم ترتيب تنفيذها أمر بالغ الأهمية للتنبؤ بالسلوك وتجنب المشكلات غير المتوقعة.

مزخرفات متعددة على هدف واحد

عند تطبيق مزخرفات متعددة على إعلان واحد (مثل فئة، طريقة، أو خاصية)، فإنها تنفذ بترتيب معين: من الأسفل إلى الأعلى، أو من اليمين إلى اليسار، لتقييمها. ومع ذلك، يتم تطبيق نتائجها بترتيب عكسي.

@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] استدعاء ${String(propertyKey)} مع الحجج:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`[LOG] أعادت الطريقة ${String(propertyKey)}:`, 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] تم رفض الوصول إلى ${String(propertyKey)}. الأدوار المطلوبة: ${roles.join(", ")}`);
        throw new Error("وصول غير مصرح به");
      }
      console.log(`[AUTH] تم منح الوصول إلى ${String(propertyKey)}`);
      return originalMethod.apply(this, args);
    };
    return descriptor;
  };
}

class SecureService {
  @LogCall
  @Authorization(["admin"])
  deleteSensitiveData(id: string) {
    console.log(`حذف بيانات حساسة للمعرف: ${id}`);
    return `تم حذف بيانات المعرف ${id}.`;
  }

  @Authorization(["user"])
  @LogCall // تم تغيير الترتيب هنا
  fetchPublicData(query: string) {
    console.log(`جلب بيانات عامة باستخدام الاستعلام: ${query}`);
    return `بيانات عامة للاستعلام: ${query}`; 
  }
}

const service = new SecureService();

try {
  console.log("\n--- استدعاء deleteSensitiveData (مستخدم إداري) ---");
  service.deleteSensitiveData("record123");
} catch (error: any) {
  console.error(error.message);
}

try {
  console.log("\n--- استدعاء fetchPublicData (مستخدم غير إداري) ---");
  // محاكاة مستخدم غير إداري يحاول الوصول إلى fetchPublicData الذي يتطلب دور "user"
  const mockUserRoles = ["guest"]; // سيفشل هذا التحقق من الصحة
  // لجعل هذا ديناميكيًا، ستحتاج إلى نظام DI أو سياق ثابت لأدوار المستخدم الحالية.
  // لتبسيط الأمور، نفترض أن مزخرفة Authorization لديها وصول إلى سياق المستخدم الحالي.
  // لنعدل مزخرفة Authorization لتفترض دائمًا "admin" لأغراض العرض، 
  // حتى ينجح الاستدعاء الأول ويفشل الثاني لإظهار مسارات مختلفة.
  
  // إعادة التشغيل بدور المستخدم لـ fetchPublicData للنجاح.
  // تخيل أن أدوار المستخدم الحالية في Authorization تصبح: ['user']
  // لهذا المثال، دعنا نحافظ على البساطة ونعرض تأثير الترتيب.
  service.fetchPublicData("مصطلح البحث"); // سيتم تنفيذ Auth -> Log
} catch (error: any) {
  console.error(error.message);
}

/* الناتج المتوقع لـ deleteSensitiveData:
[AUTH] تم منح الوصول إلى deleteSensitiveData
[LOG] استدعاء deleteSensitiveData مع الحجج: [ 'record123' ]
حذف بيانات حساسة للمعرف: record123
[LOG] أعادت الطريقة deleteSensitiveData: تم حذف بيانات المعرف record123.
*/

/* الناتج المتوقع لـ fetchPublicData (إذا كان لدى المستخدم دور "user"):
[LOG] استدعاء fetchPublicData مع الحجج: [ "مصطلح البحث" ]
[AUTH] تم منح الوصول إلى fetchPublicData
جلب بيانات عامة باستخدام الاستعلام: مصطلح البحث
[LOG] أعادت الطريقة fetchPublicData: بيانات عامة للاستعلام: مصطلح البحث
*/

لاحظ الترتيب: بالنسبة لـ deleteSensitiveData، يتم تشغيل Authorization (الأسفل) أولاً، ثم يلتف LogCall (الأعلى) حولها. يتم تنفيذ المنطق الداخلي لـ Authorization أولاً. بالنسبة لـ fetchPublicData، يتم تشغيل LogCall (الأسفل) أولاً، ثم يلتف Authorization (الأعلى) حولها. هذا يعني أن جانب LogCall سيكون خارج جانب Authorization. هذا الاختلاف حاسم للمخاوف المتقاطعة مثل التسجيل أو معالجة الأخطاء، حيث يمكن أن يؤثر ترتيب التنفيذ بشكل كبير على السلوك.

ترتيب التنفيذ لأهداف مختلفة

عندما يكون للفئة وأعضائها ومعلماتها مزخرفات، يكون ترتيب التنفيذ محددًا جيدًا:

  1. تطبق مزخرفات المعلمات أولاً، لكل معلمة، بدءًا من المعلمة الأخيرة إلى الأولى.
  2. ثم، تطبق مزخرفات الطرق أو الملحقات أو الخصائص لكل عضو.
  3. أخيرًا، تطبق مزخرفات الفئة على الفئة نفسها.

ضمن كل فئة، يتم تطبيق المزخرفات المتعددة على نفس الهدف من الأسفل إلى الأعلى (أو من اليمين إلى اليسار).

مثال: ترتيب التنفيذ الكامل

function log(message: string) {
  return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {
    if (typeof descriptorOrIndex === 'number') {
      console.log(`مزخرفة المعلمة: ${message} على المعلمة #${descriptorOrIndex} من ${String(propertyKey || "constructor")}`);
    } else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {
      if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {
        console.log(`مزخرفة الطريقة/الملحق: ${message} على ${String(propertyKey)}`);
      } else {
        console.log(`مزخرفة الخاصية: ${message} على ${String(propertyKey)}`);
      }
    } else {
      console.log(`مزخرفة الفئة: ${message} على ${target.name}`);
    }
    return descriptorOrIndex; // إرجاع الوصف للطريقة/الملحق، غير محدد لغيرها
  };
}

@log("مزخرفة الفئة D")
@log("مزخرفة الفئة C")
class MyDecoratedClass {
  @log("خاصية ثابتة A")
  static staticProp: string = "";

  @log("خاصية مثيل B")
  instanceProp: number = 0;

  @log("طريقة D")
  @log("طريقة C")
  myMethod(
    @log("معلمة Z") paramZ: string,
    @log("معلمة Y") paramY: number
  ) {
    console.log("تم تنفيذ طريقة myMethod.");
  }

  @log("ملحق F")
  get myAccessor() {
    return "";
  }

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

  constructor() {
    console.log("تم تنفيذ المنشئ.");
  }
}

new MyDecoratedClass();
// استدعاء الطريقة لتشغيل مزخرفة الطريقة
new MyDecoratedClass().myMethod("hello", 123);

/* ترتيب الإخراج المتوقع (تقريبي، اعتمادًا على إصدار TypeScript المحدد والترجمة):
مزخرفة المعلمة: معلمة Y على المعلمة #1 من myMethod
مزخرفة المعلمة: معلمة Z على المعلمة #0 من myMethod
مزخرفة الخاصية: خاصية ثابتة A على staticProp
مزخرفة الخاصية: خاصية مثيل B على instanceProp
مزخرفة الطريقة/الملحق: ملحق F على myAccessor
مزخرفة الطريقة/الملحق: طريقة C على myMethod
مزخرفة الطريقة/الملحق: طريقة D على myMethod
مزخرفة الفئة: مزخرفة الفئة C على MyDecoratedClass
مزخرفة الفئة: مزخرفة الفئة D على MyDecoratedClass
تم تنفيذ المنشئ.
تم تنفيذ طريقة myMethod.
*/

قد يختلف التوقيت الدقيق لتسجيل الدخول قليلاً بناءً على وقت استدعاء المنشئ أو الطريقة، ولكن الترتيب الذي يتم به تنفيذ وظائف المزخرفة نفسها (وتطبيق آثارها الجانبية أو قيمها المرجعة) يتبع القواعد المذكورة أعلاه.

تطبيقات عملية وأنماط تصميم مع المزخرفات

المزخرفات، خاصة عند دمجها مع تكميل reflect-metadata، تفتح عالمًا جديدًا من البرمجة الموجهة بالبيانات الوصفية. يتيح ذلك أنماط تصميم قوية تجرد التعليمات البرمجية المتكررة والمخاوف المتقاطعة.

1. حقن التبعية (DI)

أحد أبرز استخدامات المزخرفات هو في أطر عمل حقن التبعية (مثل @Injectable()، @Component()، إلخ في Angular، أو الاستخدام المكثف لـ DI في NestJS). تسمح المزخرفات لك بالإعلان عن التبعيات مباشرة على المنشئات أو الخصائص، مما يمكن إطار العمل من إنشاء وتقديم الخدمات الصحيحة تلقائيًا.

مثال: حقن خدمة مبسط

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(`الفئة ${target.name} ليست معلمة كـ @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(`لا يمكن حل المعلمة في الفهرس ${index} لـ ${target.name}. قد تكون تبعية دائرية أو نوعًا بدائيًا بدون @Inject صريح.`);
      }
      return Container.resolve(token);
    });

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

// تعريف الخدمات
@Injectable()
class DatabaseService {
  connect() {
    console.log("الاتصال بقاعدة البيانات...");
    return "اتصال DB";
  }
}

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

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

  login() {
    console.log(`خدمة المصادقة: المصادقة باستخدام ${this.db.connect()}`);
    return "تم تسجيل دخول المستخدم";
  }
}

@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("خدمة المستخدم: جلب ملف تعريف المستخدم...");
    return { id: 1, name: "المستخدم العالمي" };
  }
}

// حل الخدمة الرئيسية
console.log("--- حل UserService ---");
const userService = Container.resolve(UserService);
console.log(userService.getUserProfile());

console.log("\n--- حل AuthService (يجب أن يكون مخزنًا مؤقتًا) ---");
const authService = Container.resolve(AuthService);
authService.login();

يوضح هذا المثال المفصل كيف تسمح المزخرفات @Injectable و @Inject، جنبًا إلى جنب مع reflect-metadata، لحاوية Container مخصصة بحل وتوفير التبعيات تلقائيًا. بيانات design:paramtypes الوصفية التي تصدرها TypeScript تلقائيًا (عند تمكين emitDecoratorMetadata) أمر بالغ الأهمية هنا.

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] الدخول إلى الطريقة: ${String(propertyKey)} مع الحجج:`, args);
    try {
      const result = originalMethod.apply(this, args);
      console.log(`[LOG AOP] الخروج من الطريقة: ${String(propertyKey)} مع النتيجة:`, result);
      return result;
    } catch (error: any) {
      console.error(`[LOG AOP] خطأ في الطريقة ${String(propertyKey)}:`, error.message);
      throw error;
    }
  };
  return descriptor;
}

class PaymentProcessor {
  @LogMethod
  processPayment(amount: number, currency: string) {
    if (amount <= 0) {
      throw new Error("يجب أن يكون مبلغ الدفع موجبًا.");
    }
    console.log(`معالجة دفعة بقيمة ${amount} ${currency}...`);
    return `تمت معالجة دفعة بقيمة ${amount} ${currency} بنجاح.`;
  }

  @LogMethod
  refundPayment(transactionId: string) {
    console.log(`استرداد دفعة لمعرف المعاملة: ${transactionId}...`);
    return `تم بدء استرداد للمعاملة ${transactionId}.`;
  }
}

const processor = new PaymentProcessor();
processor.processPayment(100, "USD");
try {
  processor.processPayment(-50, "EUR");
} catch (error: any) {
  console.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)} يجب أن يكون رقمًا موجبًا.`);
}

function MaxLength(maxLength: number) {
  return function (target: Object, propertyKey: string | symbol) {
    addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} يجب أن يكون بحد أقصى ${maxLength} حرفًا.`);
  };
}

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("أخطاء المنتج 1:", Product.validate(product1)); // []

const product2 = new Product("اسم منتج طويل جدًا يتجاوز حد الخمسين حرفًا لاختبار الغرض", 50);
console.log("أخطاء المنتج 2:", Product.validate(product2)); // ["name يجب أن يكون بحد أقصى 50 حرفًا."]

const product3 = new Product("Book", -10);
console.log("أخطاء المنتج 3:", Product.validate(product3)); // ["price يجب أن يكون رقمًا موجبًا."]

يتيح لك هذا الإعداد تحديد قواعد التحقق من الصحة بشكل تعريفي على خصائص النموذج الخاصة بك، مما يجعل نماذج البيانات الخاصة بك تصف نفسها ذاتيًا من حيث قيودها.

أفضل الممارسات والاعتبارات

بينما المزخرفات قوية، يجب استخدامها بحكمة. قد يؤدي سوء استخدامها إلى تعليمات برمجية يصعب تصحيحها أو فهمها.

متى تستخدم المزخرفات (ومتى لا تستخدمها)

تأثيرات الأداء

تنفيذ المزخرفات في وقت الترجمة (أو وقت التعريف في وقت تشغيل JavaScript إذا تم ترجمتها). يحدث التحويل أو جمع البيانات الوصفية عند تعريف الفئة / الطريقة، وليس عند كل استدعاء. لذلك، فإن تأثير أداء وقت التشغيل *لتطبيق* المزخرفات ضئيل. ومع ذلك، فإن *المنطق الموجود داخل* مزخرفاتك يمكن أن يؤثر على الأداء، خاصة إذا كانت تؤدي عمليات مكلفة في كل استدعاء طريقة (على سبيل المثال، حسابات معقدة داخل مزخرفة طريقة).

قابلية الصيانة وقابلية القراءة

يمكن للمزخرفات، عند استخدامها بشكل صحيح، تحسين قابلية القراءة بشكل كبير عن طريق نقل التعليمات البرمجية المتكررة خارج المنطق الرئيسي. ومع ذلك، إذا قامت بتحويلات معقدة مخفية، يمكن أن يصبح تصحيح الأخطاء صعبًا. تأكد من توثيق مزخرفاتك جيدًا وأن سلوكها يمكن التنبؤ به.

الحالة التجريبية ومستقبل المزخرفات

من المهم التأكيد على أن مزخرفات TypeScript تستند إلى اقتراح TC39 في المرحلة 3. هذا يعني أن المواصفات مستقرة إلى حد كبير ولكنها لا تزال قد تخضع لتغييرات طفيفة قبل أن تصبح جزءًا من معيار ECMAScript الرسمي. تبنت أطر العمل مثل Angular هذه المزخرفات، مراهنة على توحيدها النهائي. هذا يعني مستوى معينًا من المخاطرة، على الرغم من اعتمادها الواسع، فمن غير المحتمل حدوث تغييرات كاسحة كبيرة.

تطور اقتراح TC39. يعتمد تنفيذ TypeScript الحالي على إصدار أقدم من الاقتراح. هناك تمييز بين "المزخرفات القديمة" و "المزخرفات القياسية". عندما يتم إصدار المعيار الرسمي، من المرجح أن تقوم TypeScript بتحديث تنفيذها. بالنسبة لمعظم المطورين الذين يستخدمون أطر العمل، سيتم إدارة هذا الانتقال بواسطة إطار العمل نفسه. بالنسبة لمؤلفي المكتبات، قد يصبح فهم الفروق الدقيقة بين المزخرفات القديمة والمستقبلية القياسية ضروريًا.

خيار المترجم emitDecoratorMetadata

هذا الخيار، عند تعيينه على true في tsconfig.json، يوجه مترجم TypeScript إلى إصدار بيانات تعريف نوع وقت التصميم معينة في JavaScript المترجمة. تتضمن هذه البيانات الوصفية نوع معلمات المنشئ (design:paramtypes)، ونوع إرجاع الطرق (design:returntype)، ونوع الخصائص (design:type).

هذه البيانات الوصفية المصدرة ليست جزءًا من وقت تشغيل JavaScript القياسي. عادةً ما تستهلكها تكميلة reflect-metadata، والتي تجعلها بعد ذلك متاحة عبر وظائف Reflect.getMetadata(). هذا أمر بالغ الأهمية لأنماط متقدمة مثل حقن التبعية، حيث يحتاج الحاوية إلى معرفة أنواع التبعيات التي يتطلبها الفئة دون تكوين صريح.

أنماط متقدمة مع المزخرفات

يمكن دمج المزخرفات وتوسيعها لبناء أنماط أكثر تطوراً.

1. تزيين المزخرفات (مزخرفات من الدرجة العالية)

يمكنك إنشاء مزخرفات تعدل أو تجمع مزخرفات أخرى. هذا أقل شيوعًا ولكنه يوضح الطبيعة الوظيفية للمزخرفات.

// مزخرفة تضمن تسجيل طريقة وتتطلب أيضًا أدوار إدارية
function AdminAndLoggedMethod() {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // تطبيق التفويض أولاً (داخلي)
    Authorization(["admin"])(target, propertyKey, descriptor);
    // ثم تطبيق LogCall (خارجي)
    LogCall(target, propertyKey, descriptor);

    return descriptor; // إرجاع الوصف المعدل
  };
}

class AdminPanel {
  @AdminAndLoggedMethod()
  deleteUserAccount(userId: string) {
    console.log(`حذف حساب المستخدم: ${userId}`);
    return `تم حذف المستخدم ${userId}.`;
  }
}

const adminPanel = new AdminPanel();
adminPanel.deleteUserAccount("user007");
/* الناتج المتوقع (بافتراض دور إداري):
[AUTH] تم منح الوصول إلى deleteUserAccount
[LOG] استدعاء deleteUserAccount مع الحجج: [ "user007" ]
حذف حساب المستخدم: user007
[LOG] أعادت الطريقة deleteUserAccount: تم حذف المستخدم user007.
*/

هنا، AdminAndLoggedMethod هو مصنع يعيد مزخرفة، وداخل تلك المزخرفة، يطبق مزخرفين آخرين. يمكن لهذا النمط تغليف تراكيب المزخرفات المعقدة.

2. استخدام المزخرفات للمزيج

بينما توفر TypeScript طرقًا أخرى لتنفيذ المزيج، يمكن استخدام المزخرفات لحقن الإمكانيات في الفئات بطريقة تعريفية.

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("تم التخلص من الكائن.");
  }
}

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(`تم إنشاء المورد ${this.name}.`);
  }

  cleanUp() {
    this.dispose();
    this.log(`تم تنظيف المورد ${this.name}.`);
  }
}

const resource = new MyResource("NetworkConnection");
console.log(`هل تم التخلص منه: ${resource.isDisposed}`);
resource.cleanUp();
console.log(`هل تم التخلص منه: ${resource.isDisposed}`);

تقوم مزخرفة @ApplyMixins بنسخ الطرق والخصائص ديناميكيًا من المنشئات الأساسية إلى النموذج الأولي للفئة المشتقة، مما ي "مزج" الوظائف بشكل فعال.

الخلاصة: تمكين تطوير TypeScript الحديث

تعد مزخرفات TypeScript ميزة قوية وتعبيرية تتيح نموذجًا جديدًا للبرمجة الموجهة بالبيانات الوصفية والموجهة بالجوانب. إنها تسمح للمطورين بتعزيز وتعديل وإضافة سلوكيات تعريفية إلى الفئات والطرق والخصائص والملحقات والمعلمات دون تغيير منطقها الأساسي. يؤدي هذا الفصل بين الاهتمامات إلى تعليمات برمجية أنظف وأكثر قابلية للصيانة وقابلة لإعادة الاستخدام بدرجة عالية.

من تبسيط حقن التبعية وتنفيذ أنظمة تحقق قوية، إلى إضافة مخاوف متقاطعة مثل التسجيل ومراقبة الأداء، توفر المزخرفات حلاً أنيقًا للعديد من تحديات التطوير الشائعة. بينما تستدعي حالتها التجريبية الوعي، فإن اعتمادها الواسع في أطر العمل الرئيسية يشير إلى قيمتها العملية وأهميتها المستقبلية.

من خلال إتقان مزخرفات TypeScript، تكتسب أداة كبيرة في ترسانتك، مما يتيح لك بناء تطبيقات أكثر قوة وقابلية للتوسع وذكاءً. احتضنها بمسؤولية، وافهم آلياتها، وافتح مستوى جديدًا من القوة التعريفية في مشاريع TypeScript الخاصة بك.