الدليل الشامل لاحتراف المُزينات (Decorators) في TypeScript: من الأساسيات إلى هندسة البرمجيات المتقدمة
في عالم هندسة البرمجيات الحديث وتطوير النظم المعقدة، تمثل كتابة شيفرة قابلة للتوسع والصيانة التحدي الأكبر لأي مطور. عند العمل على منصات برمجية ضخمة، نجد أنفسنا نكرر كتابة نفس المنطق البرمجي عبر مئات الفئات والدوال: فحص الصلاحيات، تسجيل العمليات (Logging)، قياس الأداء (Profiling)، التحقق من صحة المدخلات (Validation)، والتعامل مع الأخطاء. هنا تبرز قوة البرمجة الوصفية (Metaprogramming) وتتجسد في لغة TypeScript عبر ميزة المُزينات (Decorators).
تُعد المُزينات تطبيقاً مباشراً لنمط التصميم الشهير Decorator Pattern المعرّف ضمن أنماط عصابة الأربعة (GoF)، وهي وسيلة لفصل الاهتمامات (Separation of Concerns) عبر تمكينك من إضافة وتعديل سلوك الفئات وأعضائها أثناء وقت التصميم والتنفيذ دون المساس بالبنية البرمجية الأصلية.
1. المفهوم المعماري: ما هي المُزينات وكيف تعمل تحت الغطاء؟
المُزين في جوهره هو مجرد دالة عادية (First-Class Function) تُستدعى بواسطة محرك JavaScript أثناء مرحلة تعريف الفئة أو العضو البرمجي (Evaluation Time)، وليس عند لحظة إنشاء الكائن أو استدعاء الميثود. تستقبل هذه الدالة معلومات وصفية حول العنصر المستهدف (Target Information)، وتقوم بفحصه، أو إضافة حقول وخصائص جديدة إليه، أو استبدال تنفيذه بالكامل بتنفيذ مُعدّل.
تعتمد آلية التزيين البرمجي على التلاعب بـ Object.defineProperty و Prototype Chain الخاصة بلغة JavaScript، مما يتيح لك تعديل واصف الخاصية (Property Descriptor) وتغيير سلوك القراءة والكتابة والتنفيذ بشكل مرن ومحكم.
تهيئة بيئة العمل في ملف tsconfig.json
تاريخياً، قُدمت المُزينات في TypeScript كميزة تجريبية متبنية لاقتراح TC39 المبكر (المعروف بـ Stage 2 / Experimental Decorators). لاستخدام هذه الميزة، يجب التأكد من ضبط إعدادات بيئة العمل داخل ملف tsconfig.json كالتالي:
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strict": true
}
}الخاصية experimentalDecorators تفعل دعم الصياغة الوصفية @name، بينما تتيح emitDecoratorMetadata تمرير البيانات الوصفية الخاصة بالأنواع تلقائياً لدعم حقن التبعيات (Dependency Injection) وقراءة أنواع المعاملات وقت التشغيل بواسطة مكتبات مثل reflect-metadata.
2. تشريح الأنواع الأربعة للمُزينات في TypeScript
في نظام TypeScript التجريبي والمستخدم في كبرى الأطر البرمجية مثل Angular و NestJS، تنقسم المُزينات إلى أربعة أنواع رئيسية، يمتلك كل منها توقيعاً مستقلاً من المعاملات:
أ. مُزينات الفئات (Class Decorators)
يُطبق مُزين الفئة مباشرة فوق تصريح الفئة (Class Declaration). يُمرر إلى دالة المُزين وسيط وحيد وهو دالة البناء (Constructor Function) الخاصة بالفئة:
// توقيع مُزين الفئة:
type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;إذا قامت دالة مُزين الفئة بإرجاع دالة بناء جديدة، فسيتم استبدال الفئة الأصلية بالكامل بالصنف الجديد المرتجع. لنأخذ مثالاً عملياً يضيف طابعاً زمنياً وتاريخ إنشاء تلقائي لأي كائن يُنشأ من الفئة:
function AddTimestamps<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
createdAt: Date = new Date();
updatedAt: Date = new Date();
};
}
@AddTimestamps
class Article {
title: string;
constructor(title: string) {
this.title = title;
}
}
const article = new Article("دليل المزينات الشامل");
console.log(article.title); // "دليل المزينات الشامل"
console.log((article as any).createdAt); // تاريخ الإنشاء اللحظيب. مُزينات الدوال والطرائق (Method Decorators)
تُطبق مُزينات الدوال فوق تعريف أي دالة تابعة للفئة. تستقبل دالة التزيين ثلاثة معاملات أساسية ومحورية:
- target: النموذج الأولي للفئة (Prototype) إذا كانت الدالة عادية، أو دالة البناء (Constructor) إذا كانت الدالة ثابتة (Static).
- propertyKey: اسم الدالة كنص أو رمز (string | symbol).
- descriptor: واصف الخاصية (TypedPropertyDescriptor) الذي يتحكم بخاصية
value، وwritable، وenumerable، وconfigurable.
function LogExecution(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
): PropertyDescriptor {
// 1. الاحتفاظ بالدالة الأصلية
const originalMethod = descriptor.value;
// 2. استبدال الدالة بتنفيذ جديد يغلف الأصلي
descriptor.value = function (...args: any[]) {
console.log(`[LOG] جاري تنفيذ: ${propertyKey} بالمعاملات: ${JSON.stringify(args)}`);
const startTime = performance.now();
const result = originalMethod.apply(this, args);
const endTime = performance.now();
console.log(`[LOG] انتهى تنفيذ: ${propertyKey} في ${(endTime - startTime).toFixed(3)} مللي ثانية.`);
return result;
};
return descriptor;
}
class InvoiceService {
@LogExecution
generateInvoice(orderId: number, amount: number): string {
// محاكاة معالجة حسابية
return `فاتورة الطلب ${orderId} بقيمة ${amount}$ تم إصدارها بنجاح.`;
}
}
const service = new InvoiceService();
service.generateInvoice(1045, 350);ج. مُزينات الخصائص (Property Decorators)
تُستخدم فوق متغيرات وحقول الفئة. لا تتلقى هذه المُزينات واصف الخاصية (Property Descriptor) لأن الخصائص تُعرّف على مستوى نسخ الكائنات (Instances) وليس على النموذج الأولي (Prototype). تستقبل معلمتين فقط:
- target: النموذج الأولي للفئة أو دالة البناء الثابتة.
- propertyKey: اسم الخاصية المستهدفة.
يوضح المثال التالي كيفية استخدام مُزين الخاصية لجعل حقل معين مخصصاً للقراءة فقط (Read-Only) ومنع الكتابة فوقه:
function ReadOnly(target: any, propertyKey: string): void {
// نحدد سلوك الحقل عبر تعديل واصف الخاصية عند التعيين
let value: any;
const getter = () => value;
const setter = (newVal: any) => {
if (value !== undefined) {
throw new Error(`خطأ: لا يمكن تعديل الخاصية ${propertyKey} لأنها للقراءة فقط!`);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class SystemConfig {
@ReadOnly
apiKey: string = "SEC-XYZ-9988";
}
const config = new SystemConfig();
console.log(config.apiKey); // يعمل بنجاح
// config.apiKey = "NEW-KEY"; // يرمي استثناءً فورياً عند محاولة التعديلد. مُزينات المعاملات (Parameter Decorators)
تُطبق مباشرة قبل المعاملات داخل توقيع الدالة. تستقبل ثلاثة وسائط:
- target: النموذج الأولي للفئة أو دالة البناء.
- propertyKey: اسم الدالة الحاوية للمعامل.
- parameterIndex: الترتيب الموضعي للمعامل داخل مصفوفة معاملات الدالة (يبدأ من 0).
const requiredMetadataKey = Symbol("required");
function Required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
const existingRequiredParameters: number[] =
Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
class UserService {
registerUser(@Required username: string, @Required email: string, age?: number) {
console.log(`تم تسجيل المستخدم: ${username} - ${email}`);
}
}3. مصانع المُزينات (Decorator Factories) والمُزينات المعلمة
المُزين البسيط يطبق سلوكاً ثابتاً ومحدداً مسبقاً. ولكن في السيناريوهات البرمجية الحقيقية، نحتاج إلى تمرير خيارات إعداد وتخصيص للمُزين أثناء استدعائه. هنا يأتي دور مصنع المُزينات (Decorator Factory).
المصنع هو مجرد دالة تقبل أي عدد من المعاملات التي ترغب فيها، وتقوم بإرجاع الدالة المزينة الفعلية التي تتوافق مع توقيع العنصر المستهدف:
interface RetryOptions {
maxRetries: number;
delayMs: number;
}
// دالة المصنع تقبل خيارات إعادة المحاولة
function Retry(options: RetryOptions) {
// إرجاع دالة تزيين الطريقة
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
let attempts = 0;
while (attempts < options.maxRetries) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
attempts++;
console.warn(`فشلت محاولة ${attempts} للدالة ${propertyKey}. إعادة المحاولة بعد ${options.delayMs}ms...`);
if (attempts >= options.maxRetries) {
throw new Error(`فشلت الدالة ${propertyKey} بعد ${options.maxRetries} محاولات: ${error}`);
}
await new Promise(resolve => setTimeout(resolve, options.delayMs));
}
}
};
return descriptor;
};
}
class PaymentGateway {
@Retry({ maxRetries: 3, delayMs: 1000 })
async processPayment(transactionId: string): Promise<boolean> {
// محاكاة اتصال قد يفشل شبكياً
if (Math.random() > 0.3) {
throw new Error("انقطاع في الاتصال مع البنك");
}
return true;
}
}4. قواعد ترتيب التنفيذ وأسبقية المُزينات (Composition & Evaluation Order)
عند استخدام مُزينات متعددة على نفس العنصر، تتبع لغة TypeScript قواعد صارمة مستوحاة من تركيب الدوال في الرياضيات (Function Composition $f(g(x))$). تنقسم العملية إلى مرحلتين واضحتين:
- مرحلة التقييم (Evaluation Phase): تُقيّم دوال المصانع من الأعلى إلى الأسفل (Top to Bottom).
- مرحلة التطبيق والتنفيذ (Execution Phase): تُنفذ الدوال المرجعة من الأسفل إلى الأعلى (Bottom to Top).
function First() {
console.log("1. تقييم مصنع First");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("4. تنفيذ مزين First");
};
}
function Second() {
console.log("2. تقييم مصنع Second");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("3. تنفيذ مزين Second");
};
}
class Demonstration {
@First()
@Second()
runMethod() {
console.log("تنفيذ الميثود الأصلي");
}
}
// مخرجات وحدة التحكم أثناء التهيئة:
// 1. تقييم مصنع First
// 2. تقييم مصنع Second
// 3. تنفيذ مزين Second
// 4. تنفيذ مزين Firstأما على مستوى ترتيب عناصر الفئة المختلفة ككل، فتُنفذ المُزينات بالترتيب المعياري التالي: معاملات الدوال أولاً، تليها دوال الفئة والخصائص، ثم الأعضاء الثابتة (Static Members)، وأخيراً مُزين الفئة نفسها.
5. دراسات حالة تطبيقية وهندسة برمجية واقعية
لفهم الجدوى الحقيقية للمُزينات في المشاريع الكبرى، نستعرض أربعة أنظمة معمارية متكاملة يمكن بناؤها باستخدام المُزينات:
دراسة حالة 1: نظام التخزين المؤقت للنتائج (Memoization / Caching)
في العمليات الحسابية أو استعلامات قواعد البيانات الثقيلة، نرغب في تفادي إعادة تنفيذ نفس العملية إذا كانت المدخلات متطابقة. يقوم هذا المُزين بحفظ نتائج الاستدعاء في جدول تجزئة محلي بناءً على المعاملات الممررة:
function CacheResult() {
const cache = new Map<string, any>();
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const cacheKey = `${propertyKey}_${JSON.stringify(args)}`;
if (cache.has(cacheKey)) {
console.log(`[CACHE HIT] جلب النتيجة المخزنة للمفتاح: ${cacheKey}`);
return cache.get(cacheKey);
}
console.log(`[CACHE MISS] حساب القيمة وتخزينها للمفتاح: ${cacheKey}`);
const result = originalMethod.apply(this, args);
cache.set(cacheKey, result);
return result;
};
return descriptor;
};
}
class MathOperations {
@CacheResult()
calculateFibonacci(n: number): number {
if (n <= 1) return n;
// محاكاة عملية ثقيلة
let a = 0, b = 1;
for (let i = 2; i <= n; i++) {
let temp = a + b;
a = b;
b = temp;
}
return b;
}
}
const math = new MathOperations();
console.log(math.calculateFibonacci(40)); // حساب لأول مرة
console.log(math.calculateFibonacci(40)); // استرجاع فوري من الكاشدراسة حالة 2: نظام الحماية وفحص الصلاحيات (Role-Based Access Control)
يوفر هذا المُزين طبقة أمان تفصل منطق التحقق من صلاحيات المستخدم عن منطق الأعمال (Business Logic) داخل النظام:
interface RequestContext {
user: {
id: string;
roles: string[];
};
}
function AuthorizeRoles(...allowedRoles: string[]) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (context: RequestContext, ...args: any[]) {
const userRoles = context?.user?.roles || [];
const hasAccess = allowedRoles.some(role => userRoles.includes(role));
if (!hasAccess) {
throw new Error(`خطأ وصول 403: المستخدم لا يملك الصلاحيات الكافية لتنفيذ ${propertyKey}`);
}
return originalMethod.apply(this, [context, ...args]);
};
return descriptor;
};
}
class DashboardService {
@AuthorizeRoles("ADMIN", "SUPERVISOR")
deleteCustomerAccount(context: RequestContext, customerId: string): string {
return `تم حذف العميل ${customerId} بنجاح.`;
}
}
const adminContext: RequestContext = { user: { id: "U1", roles: ["ADMIN"] } };
const guestContext: RequestContext = { user: { id: "U2", roles: ["GUEST"] } };
const dashboard = new DashboardService();
console.log(dashboard.deleteCustomerAccount(adminContext, "C-901")); // ينجح
// dashboard.deleteCustomerAccount(guestContext, "C-901"); // يرمي استثناء الصلاحيات فوراًدراسة حالة 3: محاكاة فحص صحة المدخلات تلقائياً (Validation Decorator)
function ValidateInput(validator: (...args: any[]) => boolean, errorMessage: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const isValid = validator(...args);
if (!isValid) {
throw new Error(`فشل التحقق في دالة ${propertyKey}: ${errorMessage}`);
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class MemberRegistration {
@ValidateInput(
(email: string, age: number) => email.includes("@") && age >= 18,
"البريد غير صحيح أو العمر أقل من 18 سنة"
)
register(email: string, age: number) {
console.log(`تم تسجيل العضو: ${email}`);
}
}
const reg = new MemberRegistration();
reg.register("dev@code-elta6ur.com", 22); // ينجح
// reg.register("invalid-email", 15); // يرمي خطأ التحقق6. الانتقال إلى المعيار الحديث: ECMAScript Stage 3 Decorators
بدءاً من إصدار TypeScript 5.0، أصبح بالإمكان استخدام المُزينات وفق المعيار القياسي الرسمي لـ JavaScript (TC39 Stage 3 Decorators) دون الحاجة لتفعيل خيار experimentalDecorators في ملف التكوين. يتميز المعيار الجديد بالنقاط التالية:
- كائن السياق (Decorator Context Object): تستقبل المُزينات الآن وسيطاً ثانياً يحمل بيانات وصفية غنية مثل اسم العنصر (
context.name)، ونوعه (context.kind)، ووظائف مساعدة مثلcontext.addInitializer. - الأمان النوعي المحسّن (Type Safety): توفر واجهات السياق فحصاً نوعياً صارماً يمنع تطبيق مُزين الطرق على الخصائص أو الفئات عن طريق الخطأ.
- إلغاء الاعتماد على واصف الخاصية التقليدي: لم يعد المُزين يعدل كائن
descriptorمباشرة، بل يقوم بإرجاع دالة بديلة مباشرة.
// مثال على مُزين الطريقة وفق معيار TypeScript 5.0+ Stage 3
function LogMethodModern<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
const methodName = String(context.name);
return function (this: This, ...args: Args): Return {
console.log(`[Stage 3 Log] استدعاء الميثود: ${methodName}`);
return target.call(this, ...args);
};
}
class ModernApp {
@LogMethodModern
greetUser(name: string): string {
return `مرحباً ${name}!`;
}
}7. استكشاف الأخطاء وتصحيحها (Debugging & Troubleshooting)
التعامل مع البرمجة الوصفية ينطوي على تحديات خاصة قد تسبب أخطاء خفية يصعب رصدها في وقت التشغيل. إليك أهم المزالق البرمجية وكيفية تجنبها:
أ. فقدان سياق التنفيذ (Loss of “this” Context)
عند تغليف الدوال بواسطة descriptor.value، يؤدي استخدام الدوال السهمية (Arrow Functions) كبديل للدالة الأصلية إلى كسر ربط this بحالة الكائن المستدعي. القاعدة الذهبية هي استخدام دالة عادية مع apply(this, args) أو call(this, ...args) دائماً:
// ❌ خطأ: استخدام دالة سهمية يفقد سياق الكائن
descriptor.value = (...args: any[]) => {
return originalMethod.apply(this, args); // this هنا تشير إلى النطاق المعجمي الخارجي!
};
// ✅ صحيح: استخدام دالة عادية تحافظ على سياق الكائن
descriptor.value = function (...args: any[]) {
return originalMethod.apply(this, args); // this تشير للكائن الفعلي
};ب. تلوث النماذج الأولية (Prototype Pollution عبر Shared State)
عند تعريف متغيرات الحالة داخل نطاق المُزين الخارجي بدلاً من ربطها بنسخة الكائن (Instance)، ستتشارك جميع الكائنات المنشأة من نفس الفئة نفس القيمة المتغيرة. لتفادي ذلك، استخدم WeakMap مفهرسة بنسخة الكائن this لحفظ الحالات المستقلة.
ج. استكشاف أخطاء التنفيذ وتتبع الستاك (Stack Tracing)
بسبب تداخل طبقات التغليف، قد تصبح رسائل تتبع الأخطاء (Error Stack Trace) مليئة باستدعاءات دوال التزيين. لحل ذلك، احرص على تسمية الدوال البديلة داخل واصف الخاصية (Named Functions) بدلاً من الدوال المجهولة (Anonymous Functions) لتسهيل قراءتها داخل وحدة التحكم والمراقبة.
خلاصة معمارية ومصادر موثوقة للتعلم
تُعد المُزينات في TypeScript قمة النضج البرمجي لفصل الاهتمامات وتطبيق مبادئ الكود النظيف (Clean Code). من خلال إتقانك لمُزينات الفئات، الطرق، الخصائص، والمعاملات، وتوظيف مصانع المُزينات الديناميكية، تصبح قادراً على كتابة مكتبات وهياكل تحتية برمجية تعتمد على التعبيرية العالية وتقليل التكرار البرمجي إلى الصفر.
للمزيد من التوسع والاطلاع على أحدث المستجدات التقنية في عالم المُزينات:
- التوثيق الرسمي لـ TypeScript: الدليل المرجعي للمُزينات التجريبية
- مدونة مايكروسوفت التقنية: الإعلان عن دعم معيار TC39 Stage 3 Decorators في TypeScript 5.0
- مستودع مقترح TC39 Decorators الرسمي على GitHub لمعاينة مسار معيار جافا سكريبت
اكتشاف المزيد من كود التطور
اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.
