دکوراتورها در تایپ اسکریپت: آشنایی با Decoratorهای قدرتمند و کاربردها

فهرست مطالب

دکوراتورها در تایپ اسکریپت: آشنایی با Decoratorهای قدرتمند و کاربردها

در دنیای پرشتاب توسعه نرم‌افزار، بهینه‌سازی کد، افزایش خوانایی، و کاهش تکرار (DRY – Don’t Repeat Yourself) از اهمیت بالایی برخوردار است. برنامه‌نویسان همواره به دنبال ابزارهایی هستند که این اهداف را تسهیل کنند. یکی از این ابزارهای قدرتمند در اکوسیستم تایپ اسکریپت، دکوراتورها (Decorators) هستند. دکوراتورها، با الهام از مفهوم AOP (Aspect-Oriented Programming)، راهی برای افزودن فراداده (metadata) یا تغییر رفتار کلاس‌ها، متدها، خصوصیات، و پارامترها در زمان طراحی (design-time) بدون نیاز به تغییر مستقیم کدهای اصلی ارائه می‌دهند.

این قابلیت، به توسعه‌دهندگان امکان می‌دهد تا کدهای جداگانه برای نگرانی‌های متقاطع (cross-cutting concerns) مانند لاگ‌برداری، اعتبار‌سنجی، کشینگ، یا مدیریت دسترسی بنویسند و آن‌ها را به صورت اعلانی (declarative) به بخش‌های مختلف برنامه خود اعمال کنند. نتیجه این رویکرد، کدی تمیزتر، ماژولارتر و قابل نگهداری‌تر است. در این مقاله جامع، ما به صورت عمیق به دنیای دکوراتورها در تایپ اسکریپت قدم می‌گذاریم، از مفاهیم پایه‌ای تا کاربردهای پیشرفته و الگوهای طراحی که می‌توانند با استفاده از این ویژگی پیاده‌سازی شوند.

با ما همراه باشید تا نه تنها بفهمید دکوراتورها چه هستند و چگونه کار می‌کنند، بلکه چگونه می‌توانید از این ابزار قدرتمند برای نوشتن کدی بهینه‌تر و حرفه‌ای‌تر در پروژه‌های تایپ اسکریپت خود بهره ببرید.

دکوراتور چیست و چرا به آن نیاز داریم؟

دکوراتورها در تایپ اسکریپت، تابعی هستند که می‌توانند به تعاریف کلاس، متد، accessor، خصوصیت یا پارامتر اعمال شوند. این توابع، در زمان تعریف (compile-time) کد اجرا می‌شوند و به ما اجازه می‌دهند تا فراداده را به ساختارهای کد اضافه کنیم یا رفتار آن‌ها را بدون دست‌کاری مستقیم کد منبع تغییر دهیم. در واقع، دکوراتورها شکلی از برنامه‌نویسی متا (metaprogramming) را فراهم می‌کنند که در آن کد، کدهای دیگر را دست‌کاری می‌کند.

تاریخچه و وضعیت فعلی

مفهوم دکوراتورها در ابتدا به عنوان بخشی از پیشنهاد Stage 2 برای ECMAScript معرفی شد، اما در حال حاضر به Stage 3 رسیده است. تایپ اسکریپت، پیش از این، این قابلیت را به صورت آزمایشی (experimental) پیاده‌سازی کرد و در بسیاری از فریم‌ورک‌های محبوب مانند Angular، NestJS و TypeORM به ستون فقرات آن‌ها تبدیل شد. برای استفاده از دکوراتورها در پروژه تایپ اسکریپت خود، باید گزینه "experimentalDecorators": true را در فایل tsconfig.json فعال کنید. این نشان می‌دهد که هرچند این قابلیت بسیار پرکاربرد است، اما هنوز یک ویژگی کاملاً استاندارد شده در جاوا اسکریپت نیست و ممکن است در آینده تغییراتی کند، هرچند که تغییرات عمده بعید به نظر می‌رسد با توجه به پذیرش گسترده آن در اکوسیستم.

چرا به دکوراتورها نیاز داریم؟

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

  • جداسازی نگرانی‌ها (Separation of Concerns): در برنامه‌های بزرگ، اغلب عملکردهای متقاطعی (مانند لاگ‌برداری، اعتبار‌سنجی، امنیت) وجود دارند که در سراسر برنامه پخش شده‌اند. دکوراتورها به شما اجازه می‌دهند این نگرانی‌ها را در توابع جداگانه encapsule کرده و به صورت اعلانی به بخش‌های مربوطه اعمال کنید، بدون اینکه کدهای اصلی را آشفته کنید.
  • کاهش تکرار کد (DRY Principle): به جای کپی و چسباندن کد برای لاگ‌برداری یا اعتبار‌سنجی در هر متد یا کلاس، می‌توانید یک دکوراتور عمومی ایجاد کرده و آن را در هر جایی که نیاز است، اعمال کنید.
  • افزایش خوانایی و اعلانی بودن (Readability and Declarative Nature): با دیدن یک دکوراتور مانند @Log بالای یک متد، بلافاصله متوجه می‌شوید که این متد قابلیت لاگ‌برداری دارد، بدون اینکه نیاز به بررسی جزئیات پیاده‌سازی داشته باشید.
  • پشتیبانی از الگوهای طراحی پیشرفته: دکوراتورها ابزاری اساسی برای پیاده‌سازی الگوهایی مانند تزریق وابستگی (Dependency Injection – DI)، برنامه‌نویسی جنبه‌گرا (Aspect-Oriented Programming – AOP)، و ORM (Object-Relational Mapping) هستند که در فریم‌ورک‌های مدرن به وفور دیده می‌شوند.
  • انعطاف‌پذیری و توسعه‌پذیری: با استفاده از دکوراتورها، می‌توانید رفتار یک کلاس یا متد را بدون دست‌کاری مستقیم کد منبع آن تغییر دهید یا توسعه دهید. این کار، نگهداری و گسترش کد را بسیار آسان‌تر می‌کند.

در ادامه، به بررسی عمیق‌تر انواع مختلف دکوراتورها و نحوه عملکرد آن‌ها خواهیم پرداخت.

انواع دکوراتورها در تایپ اسکریپت و نحوه کارکرد آن‌ها

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

  1. Class Decorators
  2. Method Decorators
  3. Property Decorators
  4. Parameter Decorators
  5. Accessor Decorators

همه دکوراتورها توابعی هستند که در زمان تعریف (runtime) اجرا می‌شوند و اطلاعاتی در مورد عنصری که به آن اعمال شده‌اند، دریافت می‌کنند. اجازه دهید هر یک را با جزئیات و مثال‌های عملی بررسی کنیم.

۱. Class Decorators (دکوراتورهای کلاس)

دکوراتورهای کلاس به تعریف یک کلاس اعمال می‌شوند. آن‌ها یک آرگومان دریافت می‌کنند: سازنده کلاس (constructor function).

ساختار و کاربرد

یک دکوراتور کلاس یک تابع است که تنها آرگومان آن، تابع سازنده کلاس است:


function ClassDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
    // constructor: تابع سازنده کلاس
    // اینجا می‌توانید متادیتای کلاس را بخوانید یا رفتار آن را تغییر دهید.
    console.log(`Class ${constructor.name} was decorated.`);

    // مثال: افزودن یک خصوصیت جدید به کلاس
    return class extends constructor {
        newProperty = "Hello from decorator!";
    };
}

@ClassDecorator
class MyClass {
    constructor(public name: string) {}
    greet() {
        console.log(`Hello, ${this.name}!`);
    }
}

const instance = new MyClass("World");
instance.greet();
// console.log((instance as any).newProperty); // "Hello from decorator!" - نیاز به type assertion داریم زیرا newProperty در تایپ اصلی MyClass وجود ندارد.

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

مثال کاربردی: افزودن قابلیت زمان‌بندی به متدهای کلاس

فرض کنید می‌خواهیم یک دکوراتور کلاس ایجاد کنیم که بتواند تمام متدهای یک کلاس را برای لاگ‌برداری زمان اجرای آن‌ها تغییر دهد.


function MeasureExecutionTime<T extends { new (...args: any[]): {} }>(constructor: T) {
    return class extends constructor {
        constructor(...args: any[]) {
            super(...args);
            for (const key of Object.getOwnPropertyNames(constructor.prototype)) {
                const descriptor = Object.getOwnPropertyDescriptor(constructor.prototype, key);
                if (descriptor && typeof descriptor.value === 'function' && key !== 'constructor') {
                    const originalMethod = descriptor.value;
                    descriptor.value = function (...methodArgs: any[]) {
                        const start = Date.now();
                        const result = originalMethod.apply(this, methodArgs);
                        const end = Date.now();
                        console.log(`Method '${key}' executed in ${end - start}ms.`);
                        return result;
                    };
                    Object.defineProperty(constructor.prototype, key, descriptor);
                }
            }
        }
    };
}

@MeasureExecutionTime
class Calculator {
    add(a: number, b: number): number {
        // فرض کنید اینجا یک عملیات پیچیده انجام می‌شود
        for (let i = 0; i < 1000000; i++) {} // شبیه‌سازی کار سنگین
        return a + b;
    }

    subtract(a: number, b: number): number {
        return a - b;
    }
}

const calc = new Calculator();
calc.add(5, 3);
calc.subtract(10, 4);

در این مثال، @MeasureExecutionTime یک دکوراتور کلاس است که در زمان ساخت نمونه از Calculator، تمام متدهای آن را بررسی کرده و آن‌ها را با یک wrapper جدید جایگزین می‌کند که زمان اجرای متد اصلی را محاسبه و لاگ می‌کند. این یک نمونه عالی از AOP است.

۲. Method Decorators (دکوراتورهای متد)

دکوراتورهای متد به تعریف یک متد در یک کلاس اعمال می‌شوند. این دکوراتورها سه آرگومان دریافت می‌کنند:

  1. target: اگر متد یک متد ایستا (static) باشد، تابع سازنده کلاس است؛ در غیر این صورت، پروتوتایپ کلاس است.
  2. propertyKey: نام متد (به صورت string یا symbol).
  3. descriptor: شیء PropertyDescriptor متد. این شیء شامل `value` (خود متد)، `writable`، `enumerable` و `configurable` است.

ساختار و کاربرد


function MethodDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    console.log(`Decorating method: ${String(propertyKey)}`);
    // target: پروتوتایپ کلاس برای متدهای نمونه، سازنده کلاس برای متدهای استاتیک
    // propertyKey: نام متد (مثلاً "myMethod")
    // descriptor: شیء PropertyDescriptor متد

    const originalMethod = descriptor.value; // متد اصلی

    // بازنویسی متد با یک تابع جدید
    descriptor.value = function (...args: any[]) {
        console.log(`Arguments passed to ${String(propertyKey)}:`, args);
        const result = originalMethod.apply(this, args); // فراخوانی متد اصلی با context صحیح
        console.log(`Result of ${String(propertyKey)}:`, result);
        return result;
    };

    return descriptor; // باید descriptor تغییر یافته را برگرداند
}

class MyService {
    @MethodDecorator
    getData(id: number): string {
        return `Data for ID: ${id}`;
    }
}

const service = new MyService();
service.getData(123);

مثال کاربردی: دکوراتور لاگ‌برداری برای متدها (Log Decorator)

یک دکوراتور لاگ می‌تواند جزئیات مربوط به فراخوانی متد، آرگومان‌ها و نتایج را ثبت کند:


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

    descriptor.value = function (...args: any[]) {
        const className = target.constructor.name;
        console.log(`[LOG] Calling method: ${className}.${String(propertyKey)} with arguments:`, args);
        try {
            const result = originalMethod.apply(this, args);
            console.log(`[LOG] Method: ${className}.${String(propertyKey)} returned:`, result);
            return result;
        } catch (error) {
            console.error(`[LOG] Method: ${className}.${String(propertyKey)} threw an error:`, error);
            throw error;
        }
    };

    return descriptor;
}

class UserService {
    @LogMethod
    getUserById(id: number): { id: number; name: string } {
        if (id <= 0) {
            throw new Error("Invalid User ID");
        }
        return { id, name: `User ${id}` };
    }

    @LogMethod
    createUser(name: string, email: string): { id: number; name: string; email: string } {
        return { id: Math.floor(Math.random() * 1000), name, email };
    }
}

const userService = new UserService();
userService.getUserById(1);
userService.createUser("Alice", "alice@example.com");
try {
    userService.getUserById(0);
} catch (e) {
    // Error handled
}

این دکوراتور به راحتی به متدهای مختلف اعمال می‌شود و نگرانی لاگ‌برداری را از منطق اصلی کسب‌وکار جدا می‌کند.

۳. Property Decorators (دکوراتورهای خصوصیت)

دکوراتورهای خصوصیت به تعریف یک خصوصیت در یک کلاس اعمال می‌شوند. آن‌ها دو آرگومان دریافت می‌کنند:

  1. target: اگر خصوصیت یک خصوصیت ایستا باشد، تابع سازنده کلاس است؛ در غیر این صورت، پروتوتایپ کلاس است.
  2. propertyKey: نام خصوصیت (به صورت string یا symbol).

ساختار و کاربرد


function PropertyDecorator(target: Object, propertyKey: string | symbol) {
    console.log(`Decorating property: ${String(propertyKey)}`);
    // target: پروتوتایپ کلاس یا سازنده
    // propertyKey: نام خصوصیت
}

class Product {
    @PropertyDecorator
    productId: string;

    constructor(id: string) {
        this.productId = id;
    }
}

const product = new Product("PROD-001");

برخلاف دکوراتورهای متد، دکوراتورهای خصوصیت هیچ PropertyDescriptor را دریافت نمی‌کنند و نمی‌توانند آن را برگردانند. این بدان معناست که شما نمی‌توانید مستقیماً نحوه دسترسی یا تغییر یک خصوصیت را با استفاده از دکوراتور خصوصیت تغییر دهید. کاربرد اصلی آن‌ها برای تزریق فراداده (metadata) یا ثبت خصوصیت برای پردازش‌های بعدی (مانند اعتبار‌سنجی یا ORM) است.

مثال کاربردی: دکوراتور برای اعتبار‌سنجی (Validation Decorator)

فرض کنید می‌خواهیم یک دکوراتور برای علامت‌گذاری خصوصیاتی که باید خالی نباشند (@Required) ایجاد کنیم. این دکوراتور به خودی خود اعتبار‌سنجی را انجام نمی‌دهد، بلکه فراداده‌ای را اضافه می‌کند که توسط یک سیستم اعتبار‌سنجی جداگانه در زمان اجرا استفاده می‌شود.


const requiredMetadataKey = Symbol("required");

function Required(target: Object, propertyKey: string | symbol) {
    // اضافه کردن فراداده به خصوصیت
    let requiredProperties: string[] = Reflect.getMetadata(requiredMetadataKey, target) || [];
    requiredProperties.push(String(propertyKey));
    Reflect.defineMetadata(requiredMetadataKey, requiredProperties, target);
}

// یک تابع اعتبار‌سنجی کلی که از فراداده استفاده می‌کند
function validate(target: any) {
    let requiredProperties: string[] = Reflect.getMetadata(requiredMetadataKey, target.constructor.prototype);

    if (requiredProperties) {
        for (let propertyName of requiredProperties) {
            if (target[propertyName] === undefined || target[propertyName] === null || target[propertyName] === '') {
                throw new Error(`Property '${propertyName}' is required.`);
            }
        }
    }
}

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);
validate(user1); // بدون خطا

try {
    const user2 = new UserProfile("Jane", "", 25);
    validate(user2); // خطا: Property 'lastName' is required.
} catch (e: any) {
    console.error(e.message);
}

// نکته: برای استفاده از Reflect.defineMetadata و Reflect.getMetadata، باید:
// 1. npm install reflect-metadata --save
// 2. import "reflect-metadata"; در بالای فایل خود
// 3. "emitDecoratorMetadata": true در tsconfig.json

این مثال اهمیت reflect-metadata را نشان می‌دهد که برای تزریق و بازیابی فراداده در زمان اجرا استفاده می‌شود. بدون آن، دکوراتورهای خصوصیت کاربرد بسیار محدودی خواهند داشت.

۴. Parameter Decorators (دکوراتورهای پارامتر)

دکوراتورهای پارامتر به پارامترهای یک متد در یک کلاس اعمال می‌شوند. آن‌ها سه آرگومان دریافت می‌کنند:

  1. target: اگر متد یک متد ایستا باشد، تابع سازنده کلاس است؛ در غیر این صورت، پروتوتایپ کلاس است.
  2. propertyKey: نام متد (به صورت string یا symbol).
  3. parameterIndex: ایندکس (موقعیت) پارامتر در لیست آرگومان‌های متد (شروع از 0).

ساختار و کاربرد


function ParameterDecorator(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) {
    console.log(`Decorating parameter at index ${parameterIndex} of method: ${String(propertyKey)}`);
    // target: پروتوتایپ کلاس یا سازنده
    // propertyKey: نام متد
    // parameterIndex: ایندکس پارامتر
}

class Greeter {
    greet(@ParameterDecorator name: string, @ParameterDecorator greeting: string) {
        console.log(`${greeting}, ${name}!`);
    }
}

const greeter = new Greeter();
greeter.greet("Alice", "Hello");

دکوراتورهای پارامتر هیچ مقداری را برنمی‌گردانند و نمی‌توانند رفتار پارامتر را تغییر دهند. کاربرد اصلی آن‌ها نیز مانند دکوراتورهای خصوصیت، برای تزریق فراداده به منظور استفاده توسط فریم‌ورک‌ها (مانند تزریق وابستگی در Angular یا NestJS) است.

مثال کاربردی: تزریق وابستگی (Dependency Injection)

این یک مثال ساده و مفهومی برای نشان دادن کاربرد دکوراتور پارامتر در تزریق وابستگی است. فریم‌ورک‌هایی مانند Angular از این الگو برای شناسایی وابستگی‌هایی که باید به پارامترهای سازنده یا متد تزریق شوند، استفاده می‌کنند.


import "reflect-metadata";

const injectMetadataKey = Symbol("inject");

// دکوراتور برای علامت‌گذاری پارامترهایی که باید تزریق شوند
function Inject(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) {
    // ذخیره اطلاعات مربوط به ایندکس پارامتر برای تزریق
    let injectableParameters: number[] = Reflect.getOwnMetadata(injectMetadataKey, target, propertyKey || 'constructor') || [];
    injectableParameters.push(parameterIndex);
    Reflect.defineMetadata(injectMetadataKey, injectableParameters, target, propertyKey || 'constructor');
}

class DatabaseService {
    getData(): string {
        return "Data from database";
    }
}

class AuthService {
    authenticate(): string {
        return "User authenticated";
    }
}

class AppService {
    private dbService: DatabaseService;
    private authService: AuthService;

    // دکوراتور Inject به فریم‌ورک (یا کانتینر DI) می‌گوید چه سرویس‌هایی باید تزریق شوند
    constructor(
        @Inject dbService: DatabaseService,
        @Inject authService: AuthService
    ) {
        this.dbService = dbService;
        this.authService = authService;
    }

    runApplication() {
        console.log(this.authService.authenticate());
        console.log(this.dbService.getData());
    }
}

// یک "کانتینر DI" ساده (برای نمایش مفهوم)
class DependencyInjector {
    private static instances = new Map<any, any>();

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

        const injectableParameters: number[] = Reflect.getMetadata(injectMetadataKey, constructor.prototype, 'constructor') || [];
        
        // اینجا باید Type را از MetadataType API بگیریم (Reflect.getMetadata("design:paramtypes", constructor))
        // برای سادگی، فرض می‌کنیم سرویس‌ها از پیش شناخته شده‌اند
        const paramTypes = Reflect.getMetadata("design:paramtypes", constructor); // برای گرفتن نوع پارامترها

        const dependencies = injectableParameters.map(index => {
            const dependencyType = paramTypes[index];
            if (!dependencyType) {
                throw new Error(`Could not resolve dependency type for parameter at index ${index}.`);
            }
            // اینجا به صورت بازگشتی وابستگی‌ها را حل می‌کنیم
            return DependencyInjector.resolve(dependencyType);
        });

        const instance = new constructor(...dependencies);
        this.instances.set(constructor, instance);
        return instance;
    }

    static register<T>(constructor: { new (...args: any[]): T }) {
        this.instances.set(constructor, new constructor()); // برای سادگی، فوراً نمونه‌سازی می‌کنیم
    }
}

// ثبت سرویس‌ها
DependencyInjector.register(DatabaseService);
DependencyInjector.register(AuthService);

// حل کردن AppService
const app = DependencyInjector.resolve(AppService);
app.runApplication();

این مثال بسیار ساده‌شده است، اما نشان می‌دهد که چگونه @Inject به یک سیستم DI (مانند DependencyInjector در اینجا) اجازه می‌دهد تا بداند چه وابستگی‌هایی باید به سازنده AppService تزریق شوند. ویژگی "emitDecoratorMetadata": true در tsconfig.json برای تولید فراداده "design:paramtypes" ضروری است که تایپ اسکریپت آن را به صورت خودکار برای ما ایجاد می‌کند و به کانتینر DI اجازه می‌دهد نوع پارامتر را در زمان اجرا تشخیص دهد.

۵. Accessor Decorators (دکوراتورهای Accessor)

دکوراتورهای Accessor به متدهای `get` و `set` یک خصوصیت اعمال می‌شوند. آن‌ها نیز سه آرگومان مشابه دکوراتورهای متد دریافت می‌کنند:

  1. target: اگر accessor ایستا باشد، تابع سازنده کلاس است؛ در غیر این صورت، پروتوتایپ کلاس است.
  2. propertyKey: نام accessor (نام خصوصیت).
  3. descriptor: شیء PropertyDescriptor برای accessor.

ساختار و کاربرد


function AccessorDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    console.log(`Decorating accessor: ${String(propertyKey)}`);
    // target: پروتوتایپ کلاس یا سازنده
    // propertyKey: نام خصوصیت
    // descriptor: شیء PropertyDescriptor accessor

    // مثلاً، می‌توانید رفتار get/set را تغییر دهید
    const originalGet = descriptor.get;
    if (originalGet) {
        descriptor.get = function () {
            console.log(`Getting value for ${String(propertyKey)}`);
            return originalGet.apply(this);
        };
    }

    const originalSet = descriptor.set;
    if (originalSet) {
        descriptor.set = function (value: any) {
            console.log(`Setting value for ${String(propertyKey)} to ${value}`);
            originalSet.apply(this, [value]);
        };
    }

    return descriptor; // باید descriptor تغییر یافته را برگرداند
}

class User {
    private _name: string;

    constructor(name: string) {
        this._name = name;
    }

    @AccessorDecorator
    get name(): string {
        return this._name;
    }

    set name(value: string) {
        this._name = value;
    }
}

const user = new User("Alice");
console.log(user.name); // فراخوانی get
user.name = "Bob";     // فراخوانی set
console.log(user.name); // فراخوانی get

مثال کاربردی: اعتبارسنجی مقادیر در زمان تنظیم (Setter Validation)

می‌توانید از دکوراتورهای accessor برای اعتبارسنجی مقادیر در زمان تنظیم یک خصوصیت استفاده کنید:


function MinLength(length: number) {
    return function (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
        const originalSet = descriptor.set;

        descriptor.set = function (value: string) {
            if (typeof value === 'string' && value.length < length) {
                throw new Error(`Error: ${String(propertyKey)} must be at least ${length} characters long.`);
            }
            if (originalSet) {
                originalSet.apply(this, [value]);
            }
        };

        return descriptor;
    };
}

class Post {
    private _title: string;
    private _content: string;

    constructor(title: string, content: string) {
        this._title = title;
        this._content = content;
    }

    @MinLength(5)
    get title(): string {
        return this._title;
    }

    set title(value: string) {
        this._title = value;
    }

    @MinLength(20)
    get content(): string {
        return this._content;
    }

    set content(value: string) {
        this._content = value;
    }
}

const post = new Post("Initial Title", "This is some initial content for the post.");
console.log(post.title); // Initial Title

try {
    post.title = "New"; // خطا: title must be at least 5 characters long.
} catch (e: any) {
    console.error(e.message);
}

post.title = "New Valid Title";
console.log(post.title); // New Valid Title

try {
    post.content = "Short text"; // خطا: content must be at least 20 characters long.
} catch (e: any) {
    console.error(e.message);
}

در این مثال، @MinLength یک دکوراتور accessor factory است که اعتبارسنجی طول رشته را در زمان تلاش برای تنظیم خصوصیت اعمال می‌کند. این الگو برای اعتبارسنجی داده‌ها در مدل‌ها بسیار مفید است.

Decorator Factories (کارخانه‌های دکوراتور)

تا کنون، ما دکوراتورهایی را دیدیم که مستقیماً به عنوان تابع اعمال می‌شوند. اما در بسیاری از موارد، شما نیاز دارید که دکوراتور خود را پیکربندی کنید. به عنوان مثال، یک دکوراتور لاگ ممکن است نیاز به تعیین سطح لاگ (debug, info, warn) داشته باشد. در اینجا است که کارخانه‌های دکوراتور (Decorator Factories) وارد می‌شوند.

یک کارخانه دکوراتور یک تابع است که یک دکوراتور را برمی‌گرداند. این تابع بیرونی است که آرگومان‌های پیکربندی را می‌پذیرد و تابع داخلی که دکوراتور واقعی است، به کلاس، متد، خصوصیت یا پارامتر اعمال می‌شود.

ساختار


function DecoratorFactory(configArgument: any) {
    // این تابع بیرونی است که آرگومان‌های پیکربندی را می‌پذیرد.
    console.log(`Decorator factory configured with: ${configArgument}`);

    return function (target: any, propertyKey?: string | symbol, descriptor?: PropertyDescriptor | number) {
        // این تابع داخلی است که دکوراتور واقعی است.
        // آرگومان‌های آن بستگی به نوع دکوراتور (کلاس، متد، خصوصیت، پارامتر) دارد.
        console.log(`Decorator executed on: ${String(propertyKey || target.name)}`);
        // اینجا منطق دکوراتور اجرا می‌شود
    };
}

@DecoratorFactory('config for class')
class MyConfigurableClass {
    @DecoratorFactory('config for method')
    myMethod() {
        // ...
    }
}

مثال کاربردی: دکوراتور با قابلیت کشینگ (Cache Decorator)

فرض کنید می‌خواهیم یک دکوراتور ایجاد کنیم که نتایج یک متد را برای مدت زمان مشخصی کش (cache) کند تا از فراخوانی‌های مکرر به یک منبع گران‌قیمت (مانند یک API یا پایگاه داده) جلوگیری شود.


interface CacheEntry {
    value: any;
    expiration: number;
}

const methodCache = new Map<string, CacheEntry>();

function Cache(ttlSeconds: number = 60) { // ttlSeconds: Time To Live in seconds
    return function (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;

        descriptor.value = function (...args: any[]) {
            const cacheKey = `${String(propertyKey)}_${JSON.stringify(args)}`;
            const cached = methodCache.get(cacheKey);

            if (cached && Date.now() < cached.expiration) {
                console.log(`[Cache] Cache hit for ${String(propertyKey)} with key: ${cacheKey}`);
                return cached.value;
            }

            console.log(`[Cache] Cache miss for ${String(propertyKey)} with key: ${cacheKey}. Executing original method.`);
            const result = originalMethod.apply(this, args);

            methodCache.set(cacheKey, {
                value: result,
                expiration: Date.now() + ttlSeconds * 1000
            });

            return result;
        };

        return descriptor;
    };
}

class DataService {
    private callCount = 0;

    @Cache(10) // Cache results for 10 seconds
    getExpensiveData(id: number): string {
        this.callCount++;
        console.log(`Fetching expensive data for ID ${id}. Call count: ${this.callCount}`);
        // Simulate a delay
        for (let i = 0; i < 1000000; i++) {}
        return `Data for ${id} (fetched at ${new Date().toLocaleTimeString()})`;
    }

    @Cache(5) // Cache results for 5 seconds
    getLessExpensiveData(param: string): string {
        this.callCount++;
        console.log(`Fetching less expensive data for param '${param}'. Call count: ${this.callCount}`);
        return `Less expensive data for '${param}' (fetched at ${new Date().toLocaleTimeString()})`;
    }
}

const service = new DataService();

console.log("--- First calls ---");
console.log(service.getExpensiveData(1));
console.log(service.getExpensiveData(2));
console.log(service.getExpensiveData(1)); // Should be cached

console.log("\n--- Second calls after a short delay (within cache TTL) ---");
setTimeout(() => {
    console.log(service.getExpensiveData(1)); // Still cached
    console.log(service.getExpensiveData(2)); // Still cached
    console.log(service.getLessExpensiveData("test"));
}, 2000);

console.log("\n--- Third calls after cache expiration ---");
setTimeout(() => {
    console.log(service.getExpensiveData(1)); // Should re-fetch
    console.log(service.getExpensiveData(2)); // Should re-fetch
    console.log(service.getLessExpensiveData("test")); // Should re-fetch
}, 12000); // 12 seconds later (after 10s and 5s TTLs expire)

این دکوراتور @Cache یک کارخانه دکوراتور است که پارامتر ttlSeconds را برای تعیین مدت زمان کش می‌پذیرد. این یک مثال قدرتمند از چگونگی استفاده از دکوراتورها برای افزودن قابلیت‌های غیر هسته‌ای (cross-cutting concerns) به متدهای موجود بدون تغییر در منطق اصلی آن‌ها است.

ترکیب دکوراتورها و ترتیب اجرا

شما می‌توانید چندین دکوراتور را به یک هدف (کلاس، متد، خصوصیت یا پارامتر) اعمال کنید. درک ترتیب اجرای این دکوراتورها بسیار مهم است.

ترتیب ارزیابی (Evaluation Order)

هنگامی که چندین دکوراتور به یک عنصر اعمال می‌شوند، تایپ اسکریپت آن‌ها را به ترتیب خاصی ارزیابی می‌کند:

  1. **از بالا به پایین:** دکوراتورهای بالای لیست در ابتدا ارزیابی می‌شوند. (این فقط در مورد فراخوانی تابع سازنده دکوراتور صدق می‌کند.)
  2. **از راست به چپ:** اگر چندین دکوراتور در یک خط قرار بگیرند (مثلاً @decorator1 @decorator2 method() {})، دکوراتور سمت راست ابتدا ارزیابی می‌شود.

این ارزیابی به معنای فراخوانی تابع کارخانه دکوراتور است. اما زمانی که خود دکوراتورها اجرا می‌شوند و هدف را دست‌کاری می‌کنند، ترتیب متفاوت است.

ترتیب اجرا (Execution Order)

هنگامی که دکوراتورها برای دست‌کاری تعریف (class, method, etc.) اجرا می‌شوند، ترتیب به صورت برعکس ارزیابی است:

  • **از پایین به بالا:** دکوراتوری که نزدیک‌تر به تعریف عنصر است، ابتدا اجرا می‌شود و سپس به سمت بالا می‌رود.

این ترتیب به ویژه برای دکوراتورهای متد و accessor که می‌توانند PropertyDescriptor را تغییر دهند، مهم است. دکوراتور بیرونی می‌تواند خروجی دکوراتور داخلی را بپیچد یا تغییر دهد.

مثال ترکیب دکوراتورهای متد


function First() {
    console.log("FIRST: factory evaluated");
    return function (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
        console.log("FIRST: decorator executed");
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            console.log("FIRST: before original method");
            const result = originalMethod.apply(this, args);
            console.log("FIRST: after original method");
            return result;
        };
    };
}

function Second() {
    console.log("SECOND: factory evaluated");
    return function (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
        console.log("SECOND: decorator executed");
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            console.log("SECOND: before original method");
            const result = originalMethod.apply(this, args);
            console.log("SECOND: after original method");
            return result;
        };
    };
}

class MyComponent {
    @First()
    @Second()
    myAction(message: string) {
        console.log(`Original method executed with: ${message}`);
    }
}

const component = new MyComponent();
component.myAction("Hello Decorators!");

خروجی این کد به صورت زیر خواهد بود:


FIRST: factory evaluated
SECOND: factory evaluated
SECOND: decorator executed
FIRST: decorator executed
SECOND: before original method
FIRST: before original method
Original method executed with: Hello Decorators!
FIRST: after original method
SECOND: after original method

همانطور که مشاهده می‌کنید:

  • کارخانه‌های دکوراتور به ترتیب بالا به پایین ارزیابی می‌شوند (FIRST سپس SECOND).
  • خود دکوراتورها (یعنی توابع داخلی که PropertyDescriptor را دستکاری می‌کنند) به ترتیب پایین به بالا اجرا می‌شوند (SECOND سپس FIRST). این بدان معناست که FIRST دکوراتور SECOND را می‌پیچد.
  • در زمان فراخوانی متد (myAction)، wrappers به ترتیب بیرون به داخل اجرا می‌شوند (ابتدا SECOND‘s wrapper، سپس FIRST‘s wrapper، سپس متد اصلی). سپس نتایج به ترتیب داخل به بیرون بازگردانده می‌شوند.

درک این ترتیب برای پیاده‌سازی صحیح دکوراتورهای پیچیده و اجتناب از رفتارهای غیرمنتظره ضروری است.

کاربردهای دکوراتورها در دنیای واقعی و الگوهای طراحی

دکوراتورها تنها یک ویژگی سینتکسی جذاب نیستند، بلکه ابزاری قدرتمند برای پیاده‌سازی الگوهای طراحی پیچیده و افزایش ماژولاریتی در برنامه‌های بزرگ محسوب می‌شوند. در ادامه به برخی از مهمترین کاربردها و الگوها اشاره می‌کنیم:

۱. برنامه‌نویسی جنبه‌گرا (Aspect-Oriented Programming – AOP)

AOP یک پارادایم برنامه‌نویسی است که هدف آن افزایش ماژولاریتی با امکان جدا کردن نگرانی‌های متقاطع است. نگرانی‌های متقاطع (cross-cutting concerns) شامل قابلیت‌هایی مانند لاگ‌برداری، کشینگ، امنیت، مدیریت تراکنش‌ها و اعتبار‌سنجی هستند که در سراسر بخش‌های مختلف یک برنامه پخش شده‌اند. دکوراتورها راهی عالی برای پیاده‌سازی جنبه‌ها (aspects) در تایپ اسکریپت فراهم می‌کنند. به جای پراکنده کردن کد لاگ‌برداری در هر متد، می‌توانیم یک دکوراتور @Log ایجاد کنیم و آن را به صورت اعلانی به متدهای مورد نظر اعمال کنیم. مثال‌های @LogMethod و @Cache که قبلاً دیدیم، نمونه‌های بارزی از AOP با استفاده از دکوراتورها هستند.

۲. تزریق وابستگی (Dependency Injection – DI)

تزریق وابستگی یکی از اصول کلیدی طراحی در فریم‌ورک‌های مدرن مانند Angular و NestJS است. دکوراتورها نقش محوری در پیاده‌سازی DI دارند. دکوراتورهایی مانند @Injectable() (در Angular) یا @Inject() به یک کانتینر DI (IoC Container) اطلاع می‌دهند که یک کلاس یا یک پارامتر نیاز به تزریق وابستگی دارد.
با استفاده از "emitDecoratorMetadata": true در tsconfig.json، تایپ اسکریپت اطلاعات نوع پارامترها را به فراداده اضافه می‌کند که کانتینر DI می‌تواند از آن برای حل و تزریق وابستگی‌های صحیح استفاده کند. این باعث می‌شود که کد بسیار ماژولار و قابل تست باشد.


// مثال مفهومی DI با دکوراتورها
import 'reflect-metadata';

// @Injectable()
function Injectable() {
    return function (constructor: Function) {
        // می‌توانید اینجا منطق ثبت کلاس در کانتینر DI را اضافه کنید
        console.log(`Class ${constructor.name} is injectable.`);
    };
}

// @Service('userRepository')
function Service(serviceName: string) {
    return function (constructor: Function) {
        // منطق برای ثبت سرویس با یک نام خاص
        console.log(`Service '${serviceName}' registered for class ${constructor.name}.`);
    };
}

// @Inject('database')
function Inject(token: string) {
    return function (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) {
        // ذخیره token برای تزریق به این پارامتر
        let injectionTokens: string[] = Reflect.getOwnMetadata('injectionTokens', target, propertyKey || 'constructor') || [];
        injectionTokens[parameterIndex] = token; // در ایندکس مربوطه
        Reflect.defineMetadata('injectionTokens', injectionTokens, target, propertyKey || 'constructor');
        console.log(`Parameter at index ${parameterIndex} of ${String(propertyKey || target.name)} requires '${token}'.`);
    };
}

// فرض کنید یک کانتینر DI ساده داریم
class DIContainer {
    private static services = new Map<string, any>();

    static register(name: string, instance: any) {
        DIContainer.services.set(name, instance);
    }

    static resolve<T>(constructor: { new (...args: any[]): T }): T {
        const injectionTokens: string[] = Reflect.getMetadata('injectionTokens', constructor.prototype, 'constructor') || [];
        const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", constructor) || [];

        const dependencies = paramTypes.map((paramType, index) => {
            const token = injectionTokens[index];
            if (token && DIContainer.services.has(token)) {
                return DIContainer.services.get(token);
            }
            // اگر دکوراتور Inject استفاده نشده باشد، سعی کنید خود Type را حل کنید
            if (DIContainer.services.has(paramType.name)) {
                return DIContainer.services.get(paramType.name);
            }
            throw new Error(`Cannot resolve dependency for ${paramType.name} or token '${token}'.`);
        });

        return new constructor(...dependencies);
    }
}

// نمونه‌هایی از سرویس‌ها
@Injectable()
class Database {
    getConnection() { return "DB Connection"; }
}

@Injectable()
@Service('userRepository')
class UserRepository {
    constructor(@Inject('database') private db: Database) {} // استفاده از دکوراتور @Inject
    getUser(id: number) {
        console.log(`Fetching user ${id} using ${this.db.getConnection()}`);
        return { id, name: "Test User" };
    }
}

// ثبت در کانتینر
DIContainer.register('database', new Database());
DIContainer.register('userRepository', new UserRepository(DIContainer.services.get('database'))); // یا می‌توانید UserRepository را نیز از طریق resolve حل کنید

const userRepo = DIContainer.resolve(UserRepository); // حل خودکار وابستگی‌ها
userRepo.getUser(1);

۳. ORM و نقشه‌برداری داده (Object-Relational Mapping)

فریم‌ورک‌های ORM مانند TypeORM و MikroORM به شدت از دکوراتورها برای تعریف مدل‌های داده و ارتباط آن‌ها با جداول پایگاه داده استفاده می‌کنند. دکوراتورهایی مانند @Entity()، @Column()، @PrimaryGeneratedColumn()، @OneToMany() و @ManyToOne() به توسعه‌دهندگان اجازه می‌دهند تا شمای پایگاه داده را به صورت اعلانی در کلاس‌های تایپ اسکریپت خود تعریف کنند. این کار خوانایی را به شدت افزایش می‌دهد و کد SQL را از منطق کسب‌وکار جدا می‌کند.


// مثال مفهومی TypeORM-like
import 'reflect-metadata';

function Entity(tableName?: string) {
    return function (constructor: Function) {
        Reflect.defineMetadata('entity:name', tableName || constructor.name, constructor);
        console.log(`Entity '${constructor.name}' mapped to table '${tableName || constructor.name}'.`);
    };
}

function Column(options?: { type?: string; default?: any }) {
    return function (target: Object, propertyKey: string | symbol) {
        let columns: { name: string, options: any }[] = Reflect.getMetadata('entity:columns', target) || [];
        columns.push({ name: String(propertyKey), options });
        Reflect.defineMetadata('entity:columns', columns, target);
        console.log(`Column '${String(propertyKey)}' defined.`);
    };
}

function PrimaryGeneratedColumn() {
    return function (target: Object, propertyKey: string | symbol) {
        Reflect.defineMetadata('entity:primaryColumn', String(propertyKey), target);
        console.log(`Primary column '${String(propertyKey)}' defined.`);
    };
}

@Entity('users')
class User {
    @PrimaryGeneratedColumn()
    id: number;

    @Column({ type: 'varchar' })
    firstName: string;

    @Column({ type: 'varchar' })
    lastName: string;

    @Column({ type: 'int', default: 0 })
    age: number;

    constructor(firstName: string, lastName: string, age: number) {
        this.id = 0; // مقداردهی اولیه
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
}

// در زمان اجرا، یک ORM واقعی این metadata را می‌خواند
const userProto = User.prototype;
console.log(Reflect.getMetadata('entity:name', User));
console.log(Reflect.getMetadata('entity:primaryColumn', userProto));
console.log(Reflect.getMetadata('entity:columns', userProto));

// خروجی چیزی شبیه به این خواهد بود:
// Entity 'User' mapped to table 'users'.
// Column 'id' defined.
// Column 'firstName' defined.
// Column 'lastName' defined.
// Column 'age' defined.
// users
// id
// [
//   { name: 'id', options: undefined },
//   { name: 'firstName', options: { type: 'varchar' } },
//   { name: 'lastName', options: { type: 'varchar' } },
//   { name: 'age', options: { type: 'int', default: 0 } }
// ]

۴. روترها و کنترلرها در وب‌فریم‌ورک‌ها

فریم‌ورک‌هایی مانند NestJS از دکوراتورها برای تعریف روترها و کنترلرها استفاده می‌کنند. دکوراتورهایی مانند @Controller()، @Get()، @Post()، @Body() و @Param() به شما اجازه می‌دهند تا API Endpoints را به صورت اعلانی تعریف کنید و پارامترهای درخواست را به متدهای کنترلر تزریق کنید. این الگو کد مربوط به routing و parsing درخواست را از منطق کسب‌وکار جدا می‌کند و باعث می‌شود کنترلرها بسیار خواناتر و تمیزتر باشند.

۵. مدیریت وضعیت و الگوهای Observer

در برخی کتابخانه‌های مدیریت وضعیت یا الگوهای Observer، دکوراتورها می‌توانند برای علامت‌گذاری Observableها، Actions یا Mutations استفاده شوند. به عنوان مثال، می‌توانید دکوراتوری برای علامت‌گذاری خصوصیاتی ایجاد کنید که باید قابل رصد (observable) باشند و هر زمان که مقدار آن‌ها تغییر کرد، eventهایی را منتشر کنند.

به طور کلی، هر جا که نیاز به اضافه کردن رفتار یا فراداده به صورت اعلانی، جدا کردن نگرانی‌های متقاطع، یا پیکربندی یک بخش از کد بدون دست‌کاری مستقیم آن داشته باشید، دکوراتورها می‌توانند ابزاری فوق‌العاده مفید باشند. آن‌ها به توسعه‌دهندگان امکان می‌دهند تا کدی ماژولارتر، قابل نگهداری‌تر و مقیاس‌پذیرتر بنویسند.

ملاحظات پیشرفته و نکات مهم

با وجود قدرت و انعطاف‌پذیری دکوراتورها، درک برخی ملاحظات پیشرفته و نکات مهم برای استفاده مؤثر و جلوگیری از مشکلات احتمالی ضروری است.

۱. Reflect Metadata API

همانطور که در بخش دکوراتورهای خصوصیت و پارامتر دیدیم، دکوراتورها به تنهایی نمی‌توانند فراداده را “ذخیره” کنند. در اینجا reflect-metadata وارد می‌شود. این یک کتابخانه (و پیشنهاد ES) است که یک API برای افزودن و خواندن فراداده در زمان اجرا فراهم می‌کند. تایپ اسکریپت با فعال کردن گزینه "emitDecoratorMetadata": true در tsconfig.json، به طور خودکار از این کتابخانه برای تزریق فراداده نوع (مانند design:type، design:paramtypes و design:returntype) استفاده می‌کند.
برای استفاده دستی از آن (مثلاً برای تعریف فراداده‌های سفارشی مانند @Required یا @Inject)، باید:

  1. پکیج reflect-metadata را نصب کنید: npm install reflect-metadata --save
  2. آن را در نقطه ورودی برنامه یا در فایل‌هایی که از آن استفاده می‌کنید، import کنید: import "reflect-metadata";
  3. گزینه "emitDecoratorMetadata": true را در tsconfig.json فعال کنید.

این API شامل توابعی مانند Reflect.defineMetadata()، Reflect.getMetadata()، Reflect.getOwnMetadata() و … است که به شما امکان می‌دهد فراداده را به یک هدف (مثلاً یک کلاس یا یک متد) و یک خصوصیت (اختیاری) مرتبط کنید.

۲. ترتیب ارزیابی و اجرا (Recap)

یادآوری ترتیب ارزیابی و اجرا بسیار مهم است:

  • **ارزیابی کارخانه‌های دکوراتور:** از بالا به پایین (برای دکوراتورهای روی یک خط) یا از راست به چپ (برای دکوراتورهای روی یک عنصر).
  • **اجرای دکوراتورها (دستکاری):** از پایین به بالا (نزدیک‌ترین به تعریف تا دورترین).
  • **اجرای متد/Accessor (هنگام فراخوانی):** از بیرون به داخل (اگر دکوراتورها رفتار را بسته‌بندی کرده باشند).

این پیچیدگی می‌تواند منجر به رفتارهای غیرمنتظره شود اگر به درستی درک نشود، به ویژه هنگام کار با PropertyDescriptorها.

۳. محدودیت‌های دکوراتورها

  • **فقط برای کلاس‌ها و اعضای کلاس:** دکوراتورها را نمی‌توان به توابع مستقل، متغیرها یا بلاک‌های کد اعمال کرد. آن‌ها صرفاً برای ساختارهای کلاس محور طراحی شده‌اند.
  • **تغییرات در زمان طراحی:** دکوراتورها در زمان کامپایل (یا بارگذاری اولیه برنامه) اجرا می‌شوند. این بدان معناست که آن‌ها نمی‌توانند در زمان اجرای برنامه و به صورت پویا اضافه یا حذف شوند (مگر اینکه دکوراتورها کدی را برای دست‌کاری‌های زمان اجرا تولید کنند).
  • **عدم پشتیبانی کامل در جاوا اسکریپت استاندارد:** همانطور که اشاره شد، دکوراتورها هنوز یک ویژگی کاملاً استاندارد شده در ECMAScript نیستند (اگرچه در مرحله 3 قرار دارند). این بدان معناست که نحو و رفتار آن‌ها ممکن است در آینده تغییر کند، هرچند بعید است تغییرات اساسی صورت گیرد که کاربردهای فعلی را از بین ببرد. وابستگی به "experimentalDecorators" در tsconfig.json همواره یادآور این موضوع است.

۴. ابزارهای رفع اشکال (Debugging)

دیباگ کردن دکوراتورها می‌تواند چالش‌برانگیز باشد زیرا آن‌ها در مراحل اولیه بارگذاری برنامه اجرا می‌شوند. استفاده از console.log و debugger در داخل توابع دکوراتور و کارخانه‌های دکوراتور می‌تواند به درک ترتیب و جریان اجرا کمک کند.

۵. استفاده صحیح در مقابل استفاده بیش از حد

همانند هر ابزار قدرتمندی، دکوراتورها نیز در صورت استفاده نادرست می‌توانند باعث پیچیدگی بیش از حد یا ابهام شوند. از دکوراتورها در مواقعی استفاده کنید که واقعاً به آن‌ها نیاز دارید:

  • زمانی که می‌خواهید یک نگرانی متقاطع را به صورت اعلانی به چندین مکان اعمال کنید (AOP).
  • زمانی که فریم‌ورک شما از آن‌ها برای پیکربندی یا تزریق وابستگی استفاده می‌کند (مانند Angular، NestJS).
  • زمانی که می‌خواهید رفتار پیش‌فرض یک کلاس یا متد را بدون تغییر مستقیم کد آن، گسترش دهید یا اصلاح کنید.

از استفاده از دکوراتورها برای منطق ساده‌ای که می‌تواند به راحتی در خود متد یا کلاس مدیریت شود، خودداری کنید. همیشه هدف این است که کد را خواناتر و قابل نگهداری‌تر کنید، نه پیچیده‌تر.

۶. عملکرد (Performance)

دکوراتورها به خودی خود تأثیر عملکردی ناچیزی دارند زیرا در زمان بارگذاری کد (یا کامپایل) اجرا می‌شوند. هرگونه تأثیر عملکردی احتمالی ناشی از کدی است که دکوراتورها در زمان اجرا تزریق می‌کنند. به عنوان مثال، یک دکوراتور لاگ که در هر فراخوانی متد، عملیات I/O سنگینی انجام می‌دهد، می‌تواند بر عملکرد تأثیر بگذارد، اما این تأثیر مربوط به منطق داخل دکوراتور است، نه مکانیسم دکوراتور.

در نهایت، دکوراتورها یک لایه قدرتمند از انتزاع و ماژولاریتی را به تایپ اسکریپت اضافه می‌کنند. با درک عمیق نحوه کار آن‌ها و آگاهی از بهترین شیوه‌ها، می‌توانید از این ویژگی به طور مؤثر برای ساخت برنامه‌های قوی‌تر و قابل نگهداری‌تر استفاده کنید.

نتیجه‌گیری

دکوراتورها در تایپ اسکریپت، ابزاری بی‌نظیر برای افزایش انعطاف‌پذیری، خوانایی و ماژولاریتی کد هستند. با فراهم کردن امکان اضافه کردن رفتار یا فراداده به کلاس‌ها، متدها، خصوصیات و پارامترها به صورت اعلانی، دکوراتورها راه را برای پیاده‌سازی الگوهای طراحی پیشرفته مانند برنامه‌نویسی جنبه‌گرا (AOP) و تزریق وابستگی (DI) هموار می‌کنند. این قابلیت، به توسعه‌دهندگان کمک می‌کند تا نگرانی‌های متقاطع (cross-cutting concerns) را از منطق اصلی کسب‌وکار جدا کرده و کدی تمیزتر، قابل نگهداری‌تر و قابل تست‌تر بنویسند.

از دکوراتورهای کلاس برای تغییرات جامع در رفتار کلاس گرفته تا دکوراتورهای متد برای افزودن لاگ‌برداری، کشینگ یا اعتبارسنجی قبل و بعد از اجرای متد، و از دکوراتورهای خصوصیت و پارامتر برای تزریق فراداده‌ای که توسط فریم‌ورک‌ها استفاده می‌شود، هر نوع دکوراتور کاربردهای منحصر به فرد خود را دارد. توانایی ایجاد کارخانه‌های دکوراتور (Decorator Factories) نیز انعطاف‌پذیری را به اوج می‌رساند و امکان پیکربندی دکوراتورها را در زمان استفاده فراهم می‌آورد.

با این حال، مانند هر ابزار قدرتمندی، استفاده از دکوراتورها نیازمند درک صحیح از مکانیسم عملکردی آن‌ها، به ویژه در مورد ترتیب ارزیابی و اجرا، و همچنین آگاهی از محدودیت‌ها و بهترین شیوه‌ها است. استفاده هوشمندانه و هدفمند از دکوراتورها می‌تواند به شما در ساخت سیستم‌هایی مقیاس‌پذیر و با کد سازمان‌یافته کمک کند، در حالی که استفاده بی‌رویه یا نادرست ممکن است به پیچیدگی‌های غیرضروری منجر شود.

در دنیای مدرن توسعه وب، با فریم‌ورک‌هایی مانند Angular و NestJS که به شدت بر دکوراتورها تکیه دارند، تسلط بر این ویژگی تایپ اسکریپت دیگر یک امتیاز نیست، بلکه یک ضرورت محسوب می‌شود. با به کارگیری صحیح این قابلیت، می‌توانید به یک توسعه‌دهنده تایپ اسکریپت قدرتمندتر تبدیل شوید و پروژه‌های خود را به سطح بالاتری از کیفیت و نگهداری‌پذیری ارتقا دهید.

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

قیمت اصلی 2.290.000 ریال بود.قیمت فعلی 1.590.000 ریال است.

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

"با شرکت در این دوره جامع و کاربردی، به راحتی مهارت‌های برنامه‌نویسی پایتون را از سطح مبتدی تا پیشرفته با کمک هوش مصنوعی ChatGPT بیاموزید. این دوره، با بیش از 6 ساعت محتوای آموزشی، شما را قادر می‌سازد تا به سرعت الگوریتم‌های پیچیده را درک کرده و اپلیکیشن‌های هوشمند ایجاد کنید. مناسب برای تمامی سطوح با زیرنویس فارسی حرفه‌ای و امکان دانلود و تماشای آنلاین."

ویژگی‌های کلیدی:

بدون نیاز به تجربه قبلی برنامه‌نویسی

زیرنویس فارسی با ترجمه حرفه‌ای

۳۰ ٪ تخفیف ویژه برای دانشجویان و دانش آموزان