قدرت دکوراتورهای تایپاسکریپت را برای برنامهنویسی فراداده، برنامهنویسی جنبهگرا و بهبود کد با الگوهای اعلانی کشف کنید. راهنمای جامع برای توسعهدهندگان جهانی.
دکوراتورهای تایپاسکریپت: تسلط بر الگوهای برنامهنویسی فراداده برای اپلیکیشنهای قدرتمند
در چشمانداز گسترده توسعه نرمافزار مدرن، حفظ پایگاههای کد تمیز، مقیاسپذیر و قابل مدیریت از اهمیت بالایی برخوردار است. تایپاسکریپت، با سیستم نوعدهی قدرتمند و ویژگیهای پیشرفتهاش، ابزارهایی را برای دستیابی به این هدف در اختیار توسعهدهندگان قرار میدهد. در میان جذابترین و تحولآفرینترین ویژگیهای آن، دکوراتورها (Decorators) قرار دارند. اگرچه در زمان نگارش این مطلب هنوز یک ویژگی آزمایشی (پیشنهاد مرحله ۳ برای ECMAScript) محسوب میشوند، دکوراتورها به طور گسترده در فریمورکهایی مانند Angular و TypeORM استفاده میشوند و اساساً نحوه رویکرد ما به الگوهای طراحی، برنامهنویسی فراداده و برنامهنویسی جنبهگرا (AOP) را تغییر دادهاند.
این راهنمای جامع به عمق دکوراتورهای تایپاسکریپت میپردازد و مکانیک، انواع مختلف، کاربردهای عملی و بهترین شیوههای استفاده از آنها را بررسی میکند. چه در حال ساخت اپلیکیشنهای سازمانی در مقیاس بزرگ، میکروسرویسها یا رابطهای وب سمت کلاینت باشید، درک دکوراتورها به شما قدرت میدهد تا کد تایپاسکریپت اعلانیتر، قابل نگهداریتر و قدرتمندتری بنویسید.
درک مفهوم اصلی: دکوراتور چیست؟
در قلب خود، دکوراتور نوع خاصی از اعلان است که میتواند به تعریف یک کلاس، متد، accessor، پراپرتی یا پارامتر متصل شود. دکوراتورها توابعی هستند که یک مقدار جدید را برای هدفی که تزئین میکنند برمیگردانند (یا مقدار موجود را تغییر میدهند). هدف اصلی آنها افزودن فراداده یا تغییر رفتار اعلانی است که به آن متصل شدهاند، بدون اینکه ساختار کد زیربنایی را مستقیماً تغییر دهند. این روش خارجی و اعلانی برای تقویت کد، فوقالعاده قدرتمند است.
دکوراتورها را مانند حاشیهنویسیها یا برچسبهایی در نظر بگیرید که به بخشهایی از کد خود اعمال میکنید. این برچسبها سپس میتوانند توسط بخشهای دیگر اپلیکیشن شما یا توسط فریمورکها، اغلب در زمان اجرا، خوانده یا بر اساس آنها عمل شود تا عملکرد یا پیکربندی اضافی فراهم کنند.
سینتکس یک دکوراتور
دکوراتورها با یک نماد @
پیشوند میگیرند و به دنبال آن نام تابع دکوراتور میآید. آنها بلافاصله قبل از اعلانی که تزئین میکنند قرار میگیرند.
@MyDecorator
class MyClass {
@AnotherDecorator
myMethod() {
// ...
}
}
فعال کردن دکوراتورها در تایپاسکریپت
قبل از اینکه بتوانید از دکوراتورها استفاده کنید، باید گزینه کامپایلر 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
# or
yarn add reflect-metadata
و آن را در بالاترین قسمت نقطه ورودی اپلیکیشن خود (مانند main.ts
یا app.ts
) ایمپورت کنید:
import "reflect-metadata";
// Your application code follows
فکتوریهای دکوراتور: سفارشیسازی در دستان شما
در حالی که یک دکوراتور پایه یک تابع است، اغلب نیاز دارید که آرگومانهایی را به یک دکوراتور ارسال کنید تا رفتار آن را پیکربندی کنید. این کار با استفاده از یک فکتوری دکوراتور (decorator factory) انجام میشود. فکتوری دکوراتور تابعی است که تابع دکوراتور واقعی را برمیگرداند. هنگامی که یک فکتوری دکوراتور را اعمال میکنید، آن را با آرگومانهایش فراخوانی میکنید، و سپس تابع دکوراتوری را که تایپاسکریپت به کد شما اعمال میکند، برمیگرداند.
ایجاد یک مثال ساده از فکتوری دکوراتور
بیایید یک فکتوری برای دکوراتور Logger
بسازیم که بتواند پیامها را با پیشوندهای مختلف ثبت کند.
function Logger(prefix: string) {
return function (target: Function) {
console.log(`[${prefix}] Class ${target.name} has been defined.`);
};
}
@Logger("APP_INIT")
class ApplicationBootstrap {
constructor() {
console.log("Application is starting...");
}
}
const app = new ApplicationBootstrap();
// Output:
// [APP_INIT] Class ApplicationBootstrap has been defined.
// Application is starting...
در این مثال، Logger("APP_INIT")
فراخوانی فکتوری دکوراتور است. این فراخوانی تابع دکوراتور واقعی را برمیگرداند که target: Function
(سازنده کلاس) را به عنوان آرگومان خود میگیرد. این امکان پیکربندی پویا رفتار دکوراتور را فراهم میکند.
انواع دکوراتورها در تایپاسکریپت
تایپاسکریپت از پنج نوع متمایز دکوراتور پشتیبانی میکند که هر کدام برای نوع خاصی از اعلان قابل استفاده هستند. امضای تابع دکوراتور بسته به زمینهای که در آن اعمال میشود، متفاوت است.
۱. دکوراتورهای کلاس
دکوراتورهای کلاس به اعلانهای کلاس اعمال میشوند. تابع دکوراتور، سازنده کلاس را به عنوان تنها آرگومان خود دریافت میکند. یک دکوراتور کلاس میتواند تعریف یک کلاس را مشاهده، اصلاح یا حتی جایگزین کند.
امضا:
function ClassDecorator(target: Function) { ... }
مقدار بازگشتی:
اگر دکوراتور کلاس مقداری را برگرداند، اعلان کلاس را با تابع سازنده ارائه شده جایگزین میکند. این یک ویژگی قدرتمند است که اغلب برای mixinها یا تقویت کلاس استفاده میشود. اگر هیچ مقداری برنگردانده شود، از کلاس اصلی استفاده میشود.
موارد استفاده:
- ثبت کلاسها در یک کانتینر تزریق وابستگی.
- اعمال mixinها یا قابلیتهای اضافی به یک کلاس.
- پیکربندیهای مخصوص فریمورک (مانند مسیریابی در یک فریمورک وب).
- افزودن هوکهای چرخه حیات به کلاسها.
مثال دکوراتور کلاس: تزریق یک سرویس
یک سناریوی ساده تزریق وابستگی را تصور کنید که در آن میخواهید یک کلاس را به عنوان «قابل تزریق» (injectable) علامتگذاری کنید و به صورت اختیاری نامی برای آن در یک کانتینر فراهم کنید.
const InjectableServiceRegistry = new Map<string, Function>();
function Injectable(name?: string) {
return function<T extends { new(...args: any[]): {} }>(constructor: T) {
const serviceName = name || constructor.name;
InjectableServiceRegistry.set(serviceName, constructor);
console.log(`Registered service: ${serviceName}`);
// Optionally, you could return a new class here to augment behavior
return class extends constructor {
createdAt = new Date();
// Additional properties or methods for all injected services
};
};
}
@Injectable("UserService")
class UserDataService {
getUsers() {
return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];
}
}
@Injectable()
class ProductDataService {
getProducts() {
return [{ id: 101, name: "Laptop" }, { id: 102, name: "Mouse" }];
}
}
console.log("--- Services Registered ---");
console.log(Array.from(InjectableServiceRegistry.keys()));
const userServiceConstructor = InjectableServiceRegistry.get("UserService");
if (userServiceConstructor) {
const userServiceInstance = new userServiceConstructor();
console.log("Users:", userServiceInstance.getUsers());
// console.log("User Service Created At:", userServiceInstance.createdAt); // If the returned class is used
}
این مثال نشان میدهد که چگونه یک دکوراتور کلاس میتواند یک کلاس را ثبت کرده و حتی سازنده آن را تغییر دهد. دکوراتور Injectable
کلاس را برای یک سیستم تزریق وابستگی فرضی قابل کشف میکند.
۲. دکوراتورهای متد
دکوراتورهای متد به اعلانهای متد اعمال میشوند. آنها سه آرگومان دریافت میکنند: شیء هدف (برای اعضای استاتیک، تابع سازنده؛ برای اعضای نمونه، پروتوتایپ کلاس)، نام متد و توصیفگر پراپرتی (property descriptor) متد.
امضا:
function MethodDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
مقدار بازگشتی:
یک دکوراتور متد میتواند یک PropertyDescriptor
جدید برگرداند. اگر این کار را انجام دهد، این توصیفگر برای تعریف متد استفاده خواهد شد. این به شما امکان میدهد پیادهسازی متد اصلی را تغییر داده یا جایگزین کنید، که آن را برای AOP فوقالعاده قدرتمند میکند.
موارد استفاده:
- ثبت فراخوانیهای متد و آرگومانها/نتایج آنها.
- کش کردن نتایج متد برای بهبود عملکرد.
- اعمال بررسیهای احراز هویت قبل از اجرای متد.
- اندازهگیری زمان اجرای متد.
- Debounce یا throttle کردن فراخوانیهای متد.
مثال دکوراتور متد: نظارت بر عملکرد
بیایید یک دکوراتور MeasurePerformance
برای ثبت زمان اجرای یک متد ایجاد کنیم.
function MeasurePerformance(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
const start = process.hrtime.bigint();
const result = originalMethod.apply(this, args);
const end = process.hrtime.bigint();
const duration = Number(end - start) / 1_000_000;
console.log(`Method "${propertyKey}" executed in ${duration.toFixed(2)} ms`);
return result;
};
return descriptor;
}
class DataProcessor {
@MeasurePerformance
processData(data: number[]): number[] {
// Simulate a complex, time-consuming operation
for (let i = 0; i < 1_000_000; i++) {
Math.sin(i);
}
return data.map(n => n * 2);
}
@MeasurePerformance
fetchRemoteData(id: string): Promise<string> {
return new Promise(resolve => {
setTimeout(() => {
resolve(`Data for ID: ${id}`);
}, 500);
});
}
}
const processor = new DataProcessor();
processor.processData([1, 2, 3]);
processor.fetchRemoteData("abc").then(result => console.log(result));
دکوراتور MeasurePerformance
متد اصلی را با منطق زمانبندی بستهبندی میکند و مدت زمان اجرا را بدون شلوغ کردن منطق کسب و کار در داخل خود متد چاپ میکند. این یک مثال کلاسیک از برنامهنویسی جنبهگرا (AOP) است.
۳. دکوراتورهای Accessor
دکوراتورهای Accessor به اعلانهای accessor (get
و set
) اعمال میشوند. مشابه دکوراتورهای متد، آنها شیء هدف، نام accessor و توصیفگر پراپرتی آن را دریافت میکنند.
امضا:
function AccessorDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
مقدار بازگشتی:
یک دکوراتور accessor میتواند یک PropertyDescriptor
جدید برگرداند که برای تعریف accessor استفاده خواهد شد.
موارد استفاده:
- اعتبارسنجی هنگام تنظیم یک پراپرتی.
- تبدیل یک مقدار قبل از تنظیم یا پس از بازیابی آن.
- کنترل مجوزهای دسترسی برای پراپرتیها.
مثال دکوراتور Accessor: کش کردن Getterها
بیایید یک دکوراتور ایجاد کنیم که نتیجه یک محاسبه گرانقیمت getter را کش کند.
function CachedGetter(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalGetter = descriptor.get;
const cacheKey = `_cached_${String(propertyKey)}`;
if (originalGetter) {
descriptor.get = function() {
if (this[cacheKey] === undefined) {
console.log(`[Cache Miss] Computing value for ${String(propertyKey)}`);
this[cacheKey] = originalGetter.apply(this);
} else {
console.log(`[Cache Hit] Using cached value for ${String(propertyKey)}`);
}
return this[cacheKey];
};
}
return descriptor;
}
class ReportGenerator {
private data: number[];
constructor(data: number[]) {
this.data = data;
}
// Simulates an expensive computation
@CachedGetter
get expensiveSummary(): number {
console.log("Performing expensive summary calculation...");
return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;
}
}
const generator = new ReportGenerator([10, 20, 30, 40, 50]);
console.log("First access:", generator.expensiveSummary);
console.log("Second access:", generator.expensiveSummary);
console.log("Third access:", generator.expensiveSummary);
این دکوراتور تضمین میکند که محاسبه getter expensiveSummary
فقط یک بار اجرا شود، فراخوانیهای بعدی مقدار کش شده را برمیگردانند. این الگو برای بهینهسازی عملکرد در جایی که دسترسی به پراپرتی شامل محاسبات سنگین یا فراخوانیهای خارجی است بسیار مفید است.
۴. دکوراتورهای پراپرتی
دکوراتورهای پراپرتی به اعلانهای پراپرتی اعمال میشوند. آنها دو آرگومان دریافت میکنند: شیء هدف (برای اعضای استاتیک، تابع سازنده؛ برای اعضای نمونه، پروتوتایپ کلاس) و نام پراپرتی.
امضا:
function PropertyDecorator(target: Object, propertyKey: string | symbol) { ... }
مقدار بازگشتی:
دکوراتورهای پراپرتی نمیتوانند هیچ مقداری را برگردانند. استفاده اصلی آنها ثبت فراداده در مورد پراپرتی است. آنها نمیتوانند مستقیماً مقدار پراپرتی یا توصیفگر آن را در زمان تزئین تغییر دهند، زیرا توصیفگر یک پراپرتی هنوز در زمان اجرای دکوراتورهای پراپرتی به طور کامل تعریف نشده است.
موارد استفاده:
- ثبت پراپرتیها برای سریالسازی/دیسریالسازی.
- اعمال قوانین اعتبارسنجی به پراپرتیها.
- تنظیم مقادیر پیشفرض یا پیکربندی برای پراپرتیها.
- نگاشت ستون ORM (Object-Relational Mapping) (مانند
@Column()
در TypeORM).
مثال دکوراتور پراپرتی: اعتبارسنجی فیلد الزامی
بیایید یک دکوراتور برای علامتگذاری یک پراپرتی به عنوان «الزامی» ایجاد کنیم و سپس آن را در زمان اجرا اعتبارسنجی کنیم.
interface ValidationRule {
property: string | symbol;
validate: (value: any) => boolean;
message: string;
}
const validationRules: Map<Function, ValidationRule[]> = new Map();
function Required(target: Object, propertyKey: string | symbol) {
const rules = validationRules.get(target.constructor) || [];
rules.push({
property: propertyKey,
validate: (value: any) => value !== null && value !== undefined && value !== "",
message: `${String(propertyKey)} is required.`
});
validationRules.set(target.constructor, rules);
}
function validate(instance: any): string[] {
const classRules = validationRules.get(instance.constructor) || [];
const errors: string[] = [];
for (const rule of classRules) {
if (!rule.validate(instance[rule.property])) {
errors.push(rule.message);
}
}
return errors;
}
class UserProfile {
@Required
firstName: string;
@Required
lastName: string;
age?: number;
constructor(firstName: string, lastName: string, age?: number) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
}
const user1 = new UserProfile("John", "Doe", 30);
console.log("User 1 validation errors:", validate(user1)); // []
const user2 = new UserProfile("", "Smith");
console.log("User 2 validation errors:", validate(user2)); // ["firstName is required."]
const user3 = new UserProfile("Alice", "");
console.log("User 3 validation errors:", validate(user3)); // ["lastName is required."]
دکوراتور Required
به سادگی قانون اعتبارسنجی را در یک نقشه مرکزی validationRules
ثبت میکند. سپس یک تابع جداگانه validate
از این فراداده برای بررسی نمونه در زمان اجرا استفاده میکند. این الگو منطق اعتبارسنجی را از تعریف داده جدا میکند و آن را قابل استفاده مجدد و تمیز میسازد.
۵. دکوراتورهای پارامتر
دکوراتورهای پارامتر به پارامترهای درون سازنده کلاس یا یک متد اعمال میشوند. آنها سه آرگومان دریافت میکنند: شیء هدف (برای اعضای استاتیک، تابع سازنده؛ برای اعضای نمونه، پروتوتایپ کلاس)، نام متد (یا undefined
برای پارامترهای سازنده)، و شاخص ترتیبی پارامتر در لیست پارامترهای تابع.
امضا:
function ParameterDecorator(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) { ... }
مقدار بازگشتی:
دکوراتورهای پارامتر نمیتوانند هیچ مقداری را برگردانند. مانند دکوراتورهای پراپرتی، نقش اصلی آنها افزودن فراداده در مورد پارامتر است.
موارد استفاده:
- ثبت انواع پارامتر برای تزریق وابستگی (مانند
@Inject()
در Angular). - اعمال اعتبارسنجی یا تبدیل به پارامترهای خاص.
- استخراج فراداده در مورد پارامترهای درخواست API در فریمورکهای وب.
مثال دکوراتور پارامتر: تزریق دادههای درخواست
بیایید شبیهسازی کنیم که چگونه یک فریمورک وب ممکن است از دکوراتورهای پارامتر برای تزریق دادههای خاص به یک پارامتر متد استفاده کند، مانند شناسه کاربر از یک درخواست.
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);
};
}
// A hypothetical framework function to invoke a method with resolved parameters
function executeWithParams(instance: any, methodName: string, request: any) {
const classResolvers = parameterResolvers.get(instance.constructor);
if (!classResolvers) {
return (instance[methodName] as Function).apply(instance, []);
}
const methodParamMetadata = classResolvers.get(methodName);
if (!methodParamMetadata) {
return (instance[methodName] as Function).apply(instance, []);
}
const args: any[] = Array(methodParamMetadata.length);
for (const meta of methodParamMetadata) {
args[meta.index] = meta.resolver(request);
}
return (instance[methodName] as Function).apply(instance, args);
}
class UserController {
getUser(@RequestParam("id") userId: string, @RequestParam("token") authToken?: string) {
console.log(`Fetching user with ID: ${userId}, Token: ${authToken || "N/A"}`);
return { id: userId, name: "Jane Doe" };
}
deleteUser(@RequestParam("id") userId: string) {
console.log(`Deleting user with ID: ${userId}`);
return { status: "deleted", id: userId };
}
}
const userController = new UserController();
// Simulate an incoming request
const mockRequest = {
id: "user123",
token: "abc-123",
someOtherProp: "xyz"
};
console.log("\n--- Executing getUser ---");
executeWithParams(userController, "getUser", mockRequest);
console.log("\n--- Executing deleteUser ---");
executeWithParams(userController, "deleteUser", { id: "user456" });
این مثال نشان میدهد که چگونه دکوراتورهای پارامتر میتوانند اطلاعاتی در مورد پارامترهای مورد نیاز متد جمعآوری کنند. سپس یک فریمورک میتواند از این فراداده جمعآوری شده برای حل و فصل خودکار و تزریق مقادیر مناسب هنگام فراخوانی متد استفاده کند، که به طور قابل توجهی منطق کنترلر یا سرویس را ساده میکند.
ترکیب و ترتیب اجرای دکوراتورها
دکوراتورها میتوانند در ترکیبهای مختلفی اعمال شوند و درک ترتیب اجرای آنها برای پیشبینی رفتار و جلوگیری از مشکلات غیرمنتظره حیاتی است.
چندین دکوراتور بر روی یک هدف واحد
هنگامی که چندین دکوراتور به یک اعلان واحد (مانند یک کلاس، متد یا پراپرتی) اعمال میشوند، آنها به ترتیب خاصی اجرا میشوند: از پایین به بالا، یا از راست به چپ برای ارزیابی آنها. با این حال، نتایج آنها به ترتیب معکوس اعمال میشود.
@DecoratorA
@DecoratorB
class MyClass {
// ...
}
در اینجا، ابتدا DecoratorB
ارزیابی میشود، سپس DecoratorA
. اگر آنها کلاس را تغییر دهند (مثلاً با برگرداندن یک سازنده جدید)، تغییر از DecoratorA
تغییر از DecoratorB
را بستهبندی یا روی آن اعمال خواهد کرد.
مثال: زنجیرهای کردن دکوراتورهای متد
دو دکوراتور متد را در نظر بگیرید: LogCall
و Authorization
.
function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[LOG] Calling ${String(propertyKey)} with args:`, args);
const result = originalMethod.apply(this, args);
console.log(`[LOG] Method ${String(propertyKey)} returned:`, result);
return result;
};
return descriptor;
}
function Authorization(roles: string[]) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const currentUserRoles = ["admin"]; // Simulate fetching current user roles
const authorized = roles.some(role => currentUserRoles.includes(role));
if (!authorized) {
console.warn(`[AUTH] Access denied for ${String(propertyKey)}. Required roles: ${roles.join(", ")}`);
throw new Error("Unauthorized access");
}
console.log(`[AUTH] Access granted for ${String(propertyKey)}`);
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class SecureService {
@LogCall
@Authorization(["admin"])
deleteSensitiveData(id: string) {
console.log(`Deleting sensitive data for ID: ${id}`);
return `Data ID ${id} deleted.`;
}
@Authorization(["user"])
@LogCall // Order changed here
fetchPublicData(query: string) {
console.log(`Fetching public data with query: ${query}`);
return `Public data for query: ${query}`;
}
}
const service = new SecureService();
try {
console.log("\n--- Calling deleteSensitiveData (Admin User) ---");
service.deleteSensitiveData("record123");
} catch (error: any) {
console.error(error.message);
}
try {
console.log("\n--- Calling fetchPublicData (Non-Admin User) ---");
// Simulate a non-admin user trying to access fetchPublicData which requires 'user' role
const mockUserRoles = ["guest"]; // This will fail auth
// To make this dynamic, you'd need a DI system or static context for current user roles.
// For simplicity, we assume the Authorization decorator has access to current user context.
// Let's adjust Authorization decorator to always assume 'admin' for demo purposes,
// so the first call succeeds and second fails to show different paths.
// Re-run with user role for fetchPublicData to succeed.
// Imagine currentUserRoles in Authorization becomes: ['user']
// For this example, let's keep it simple and show the order effect.
service.fetchPublicData("search term"); // This will execute Auth -> Log
} catch (error: any) {
console.error(error.message);
}
/* Expected output for deleteSensitiveData:
[AUTH] Access granted for deleteSensitiveData
[LOG] Calling deleteSensitiveData with args: [ 'record123' ]
Deleting sensitive data for ID: record123
[LOG] Method deleteSensitiveData returned: Data ID record123 deleted.
*/
/* Expected output for fetchPublicData (if user has 'user' role):
[LOG] Calling fetchPublicData with args: [ 'search term' ]
[AUTH] Access granted for fetchPublicData
Fetching public data with query: search term
[LOG] Method fetchPublicData returned: Public data for query: search term
*/
به ترتیب توجه کنید: برای deleteSensitiveData
، Authorization
(پایین) اول اجرا میشود، سپس LogCall
(بالا) دور آن را میگیرد. منطق داخلی Authorization
اول اجرا میشود. برای fetchPublicData
، LogCall
(پایین) اول اجرا میشود، سپس Authorization
(بالا) دور آن را میگیرد. این بدان معناست که جنبه LogCall
خارج از جنبه Authorization
خواهد بود. این تفاوت برای دغدغههای فراگیر (cross-cutting concerns) مانند لاگگیری یا مدیریت خطا حیاتی است، جایی که ترتیب اجرا میتواند به طور قابل توجهی بر رفتار تأثیر بگذارد.
ترتیب اجرا برای اهداف مختلف
هنگامی که یک کلاس، اعضای آن و پارامترها همگی دکوراتور دارند، ترتیب اجرا به خوبی تعریف شده است:
- دکوراتورهای پارامتر اول از همه اعمال میشوند، برای هر پارامتر، از آخرین پارامتر به اولین.
- سپس، دکوراتورهای متد، Accessor یا پراپرتی برای هر عضو اعمال میشوند.
- در نهایت، دکوراتورهای کلاس به خود کلاس اعمال میشوند.
در هر دسته، چندین دکوراتور روی یک هدف از پایین به بالا (یا از راست به چپ) اعمال میشوند.
مثال: ترتیب کامل اجرا
function log(message: string) {
return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {
if (typeof descriptorOrIndex === 'number') {
console.log(`Param Decorator: ${message} on parameter #${descriptorOrIndex} of ${String(propertyKey || "constructor")}`);
} else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {
if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {
console.log(`Method/Accessor Decorator: ${message} on ${String(propertyKey)}`);
} else {
console.log(`Property Decorator: ${message} on ${String(propertyKey)}`);
}
} else {
console.log(`Class Decorator: ${message} on ${target.name}`);
}
return descriptorOrIndex; // Return descriptor for method/accessor, undefined for others
};
}
@log("Class Level D")
@log("Class Level C")
class MyDecoratedClass {
@log("Static Property A")
static staticProp: string = "";
@log("Instance Property B")
instanceProp: number = 0;
@log("Method D")
@log("Method C")
myMethod(
@log("Parameter Z") paramZ: string,
@log("Parameter Y") paramY: number
) {
console.log("Method myMethod executed.");
}
@log("Getter/Setter F")
get myAccessor() {
return "";
}
set myAccessor(value: string) {
//...
}
constructor() {
console.log("Constructor executed.");
}
}
new MyDecoratedClass();
// Call method to trigger method decorator
new MyDecoratedClass().myMethod("hello", 123);
/* Predicted Output Order (approximate, depending on specific TypeScript version and compilation):
Param Decorator: Parameter Y on parameter #1 of myMethod
Param Decorator: Parameter Z on parameter #0 of myMethod
Property Decorator: Static Property A on staticProp
Property Decorator: Instance Property B on instanceProp
Method/Accessor Decorator: Getter/Setter F on myAccessor
Method/Accessor Decorator: Method C on myMethod
Method/Accessor Decorator: Method D on myMethod
Class Decorator: Class Level C on MyDecoratedClass
Class Decorator: Class Level D on MyDecoratedClass
Constructor executed.
Method myMethod executed.
*/
زمان دقیق لاگ کنسول ممکن است بسته به زمان فراخوانی سازنده یا متد کمی متفاوت باشد، اما ترتیبی که خود توابع دکوراتور اجرا میشوند (و بنابراین اثرات جانبی یا مقادیر بازگشتی آنها اعمال میشود) از قوانین بالا پیروی میکند.
کاربردهای عملی و الگوهای طراحی با دکوراتورها
دکوراتورها، به ویژه در ترکیب با پلیفیل reflect-metadata
، قلمرو جدیدی از برنامهنویسی مبتنی بر فراداده را باز میکنند. این امکان را برای الگوهای طراحی قدرتمندی فراهم میکند که کدهای تکراری و دغدغههای فراگیر را انتزاعی میکنند.
۱. تزریق وابستگی (DI)
یکی از برجستهترین کاربردهای دکوراتورها در فریمورکهای تزریق وابستگی است (مانند @Injectable()
، @Component()
و غیره در Angular، یا استفاده گسترده از DI در NestJS). دکوراتورها به شما اجازه میدهند وابستگیها را مستقیماً روی سازندهها یا پراپرتیها اعلام کنید، که به فریمورک امکان میدهد به طور خودکار سرویسهای صحیح را نمونهسازی و فراهم کند.
مثال: تزریق سرویس سادهشده
import "reflect-metadata"; // Essential for emitDecoratorMetadata
const INJECTABLE_METADATA_KEY = Symbol("injectable");
const INJECT_METADATA_KEY = Symbol("inject");
function Injectable() {
return function (target: Function) {
Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);
};
}
function Inject(token: any) {
return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {
const existingInjections: any[] = Reflect.getOwnMetadata(INJECT_METADATA_KEY, target, propertyKey) || [];
existingInjections[parameterIndex] = token;
Reflect.defineMetadata(INJECT_METADATA_KEY, existingInjections, target, propertyKey);
};
}
class Container {
private static instances = new Map<any, any>();
static resolve<T>(target: { new (...args: any[]): T }): T {
if (Container.instances.has(target)) {
return Container.instances.get(target);
}
const isInjectable = Reflect.getMetadata(INJECTABLE_METADATA_KEY, target);
if (!isInjectable) {
throw new Error(`Class ${target.name} is not marked as @Injectable.`);
}
// Get constructor parameters' types (requires emitDecoratorMetadata)
const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", target) || [];
const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];
const dependencies = paramTypes.map((paramType, index) => {
// Use explicit @Inject token if provided, otherwise infer type
const token = explicitInjections[index] || paramType;
if (token === undefined) {
throw new Error(`Cannot resolve parameter at index ${index} for ${target.name}. It might be a circular dependency or primitive type without explicit @Inject.`);
}
return Container.resolve(token);
});
const instance = new target(...dependencies);
Container.instances.set(target, instance);
return instance;
}
}
// Define services
@Injectable()
class DatabaseService {
connect() {
console.log("Connecting to database...");
return "DB Connection";
}
}
@Injectable()
class AuthService {
private db: DatabaseService;
constructor(db: DatabaseService) {
this.db = db;
}
login() {
console.log(`AuthService: Authenticating using ${this.db.connect()}`);
return "User logged in";
}
}
@Injectable()
class UserService {
private authService: AuthService;
private dbService: DatabaseService; // Example of injecting via property using a custom decorator or framework feature
constructor(@Inject(AuthService) authService: AuthService,
@Inject(DatabaseService) dbService: DatabaseService) {
this.authService = authService;
this.dbService = dbService;
}
getUserProfile() {
this.authService.login();
this.dbService.connect();
console.log("UserService: Fetching user profile...");
return { id: 1, name: "Global User" };
}
}
// Resolve the main service
console.log("--- Resolving UserService ---");
const userService = Container.resolve(UserService);
console.log(userService.getUserProfile());
console.log("\n--- Resolving AuthService (should be cached) ---");
const authService = Container.resolve(AuthService);
authService.login();
این مثال پیچیده نشان میدهد که چگونه دکوراتورهای @Injectable
و @Inject
، در ترکیب با reflect-metadata
، به یک Container
سفارشی اجازه میدهند تا به طور خودکار وابستگیها را حل و فصل و فراهم کند. فراداده design:paramtypes
که به طور خودکار توسط تایپاسکریپت (زمانی که emitDecoratorMetadata
فعال است) منتشر میشود، در اینجا حیاتی است.
۲. برنامهنویسی جنبهگرا (AOP)
AOP بر روی ماژولارسازی دغدغههای فراگیر (مانند لاگگیری، امنیت، تراکنشها) تمرکز دارد که در چندین کلاس و ماژول تکرار میشوند. دکوراتورها برای پیادهسازی مفاهیم AOP در تایپاسکریپت بسیار مناسب هستند.
مثال: لاگگیری با دکوراتور متد
با بازگشت به دکوراتور LogCall
، این یک مثال عالی از AOP است. این دکوراتور رفتار لاگگیری را به هر متدی اضافه میکند بدون اینکه کد اصلی متد را تغییر دهد. این کار «چه کاری باید انجام شود» (منطق کسب و کار) را از «چگونه باید انجام شود» (لاگگیری، نظارت بر عملکرد و غیره) جدا میکند.
function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[LOG AOP] Entering method: ${String(propertyKey)} with args:`, args);
try {
const result = originalMethod.apply(this, args);
console.log(`[LOG AOP] Exiting method: ${String(propertyKey)} with result:`, result);
return result;
} catch (error: any) {
console.error(`[LOG AOP] Error in method ${String(propertyKey)}:`, error.message);
throw error;
}
};
return descriptor;
}
class PaymentProcessor {
@LogMethod
processPayment(amount: number, currency: string) {
if (amount <= 0) {
throw new Error("Payment amount must be positive.");
}
console.log(`Processing payment of ${amount} ${currency}...`);
return `Payment of ${amount} ${currency} processed successfully.`;
}
@LogMethod
refundPayment(transactionId: string) {
console.log(`Refunding payment for transaction ID: ${transactionId}...`);
return `Refund initiated for ${transactionId}.`;
}
}
const processor = new PaymentProcessor();
processor.processPayment(100, "USD");
try {
processor.processPayment(-50, "EUR");
} catch (error: any) {
console.error("Caught error:", error.message);
}
این رویکرد کلاس PaymentProcessor
را صرفاً بر روی منطق پرداخت متمرکز نگه میدارد، در حالی که دکوراتور LogMethod
دغدغه فراگیر لاگگیری را مدیریت میکند.
۳. اعتبارسنجی و تبدیل
دکوراتورها برای تعریف قوانین اعتبارسنجی مستقیماً روی پراپرتیها یا برای تبدیل دادهها در حین سریالسازی/دیسریالسازی فوقالعاده مفید هستند.
مثال: اعتبارسنجی داده با دکوراتورهای پراپرتی
مثال @Required
قبلاً این را نشان داد. در اینجا مثال دیگری با اعتبارسنجی محدوده عددی آورده شده است.
interface FieldValidationRule {
property: string | symbol;
validator: (value: any) => boolean;
message: string;
}
const fieldValidationRules = new Map<Function, FieldValidationRule[]>();
function addValidationRule(target: Object, propertyKey: string | symbol, validator: (value: any) => boolean, message: string) {
const rules = fieldValidationRules.get(target.constructor) || [];
rules.push({ property: propertyKey, validator, message });
fieldValidationRules.set(target.constructor, rules);
}
function IsPositive(target: Object, propertyKey: string | symbol) {
addValidationRule(target, propertyKey, (value: number) => value > 0, `${String(propertyKey)} must be a positive number.`);
}
function MaxLength(maxLength: number) {
return function (target: Object, propertyKey: string | symbol) {
addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} must be at most ${maxLength} characters long.`);
};
}
class Product {
@MaxLength(50)
name: string;
@IsPositive
price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
static validate(instance: any): string[] {
const errors: string[] = [];
const rules = fieldValidationRules.get(instance.constructor) || [];
for (const rule of rules) {
if (!rule.validator(instance[rule.property])) {
errors.push(rule.message);
}
}
return errors;
}
}
const product1 = new Product("Laptop", 1200);
console.log("Product 1 errors:", Product.validate(product1)); // []
const product2 = new Product("Very long product name that exceeds fifty characters limit for testing purpose", 50);
console.log("Product 2 errors:", Product.validate(product2)); // ["name must be at most 50 characters long."]
const product3 = new Product("Book", -10);
console.log("Product 3 errors:", Product.validate(product3)); // ["price must be a positive number."]
این تنظیم به شما امکان میدهد قوانین اعتبارسنجی را به صورت اعلانی روی پراپرتیهای مدل خود تعریف کنید، و مدلهای داده خود را از نظر محدودیتهایشان خود-توصیفگر میسازد.
بهترین شیوهها و ملاحظات
در حالی که دکوراتورها قدرتمند هستند، باید با احتیاط استفاده شوند. استفاده نادرست از آنها میتواند به کدی منجر شود که اشکالزدایی یا درک آن دشوارتر است.
چه زمانی از دکوراتورها استفاده کنیم (و چه زمانی نه)
- برای این موارد استفاده کنید:
- دغدغههای فراگیر: لاگگیری، کش کردن، احراز هویت، مدیریت تراکنش.
- اعلان فراداده: تعریف اسکما برای ORMها، قوانین اعتبارسنجی، پیکربندی DI.
- یکپارچهسازی فریمورک: هنگام ساخت یا استفاده از فریمورکهایی که از فراداده استفاده میکنند.
- کاهش کد تکراری: انتزاعی کردن الگوهای کد تکراری.
- برای این موارد از آنها اجتناب کنید:
- فراخوانیهای ساده تابع: اگر یک فراخوانی تابع ساده میتواند به وضوح همان نتیجه را به دست آورد، آن را ترجیح دهید.
- منطق کسب و کار: دکوراتورها باید منطق اصلی کسب و کار را تقویت کنند، نه اینکه آن را تعریف کنند.
- پیچیدگی بیش از حد: اگر استفاده از یک دکوراتور کد را کمتر خوانا یا تست آن را دشوارتر میکند، دوباره فکر کنید.
پیامدهای عملکرد
دکوراتورها در زمان کامپایل (یا زمان تعریف در زمان اجرای جاوااسکریپت اگر ترنسپایل شوند) اجرا میشوند. تبدیل یا جمعآوری فراداده زمانی اتفاق میافتد که کلاس/متد تعریف میشود، نه در هر فراخوانی. بنابراین، تأثیر عملکرد زمان اجرای *اعمال* دکوراتورها حداقل است. با این حال، *منطق درون* دکوراتورهای شما میتواند تأثیر عملکردی داشته باشد، به خصوص اگر عملیات گرانقیمتی را در هر فراخوانی متد انجام دهند (مانند محاسبات پیچیده در یک دکوراتور متد).
قابلیت نگهداری و خوانایی
دکوراتورها، هنگامی که به درستی استفاده شوند، میتوانند با انتقال کد تکراری به خارج از منطق اصلی، خوانایی را به طور قابل توجهی بهبود بخشند. با این حال، اگر آنها تبدیلهای پیچیده و پنهانی انجام دهند، اشکالزدایی میتواند چالشبرانگیز شود. اطمینان حاصل کنید که دکوراتورهای شما به خوبی مستند شده و رفتار آنها قابل پیشبینی است.
وضعیت آزمایشی و آینده دکوراتورها
مهم است که تکرار کنیم دکوراتورهای تایپاسکریپت بر اساس یک پیشنهاد مرحله ۳ TC39 هستند. این بدان معناست که مشخصات تا حد زیادی پایدار است اما هنوز هم ممکن است قبل از تبدیل شدن به بخشی از استاندارد رسمی ECMAScript تغییرات جزئی داشته باشد. فریمورکهایی مانند Angular آنها را پذیرفتهاند و بر روی استاندارد شدن نهایی آنها شرطبندی کردهاند. این به معنای سطح معینی از ریسک است، هرچند با توجه به پذیرش گسترده آنها، تغییرات شکننده قابل توجهی بعید است.
پیشنهاد TC39 تکامل یافته است. پیادهسازی فعلی تایپاسکریپت بر اساس نسخه قدیمیتری از پیشنهاد است. تمایزی بین «دکوراتورهای قدیمی» (Legacy Decorators) و «دکوراتورهای استاندارد» (Standard Decorators) وجود دارد. هنگامی که استاندارد رسمی منتشر شود، تایپاسکریپت احتمالاً پیادهسازی خود را بهروزرسانی خواهد کرد. برای اکثر توسعهدهندگانی که از فریمورکها استفاده میکنند، این انتقال توسط خود فریمورک مدیریت خواهد شد. برای نویسندگان کتابخانه، درک تفاوتهای ظریف بین دکوراتورهای قدیمی و استاندارد آینده ممکن است ضروری شود.
گزینه کامپایلر emitDecoratorMetadata
این گزینه، هنگامی که در tsconfig.json
روی true
تنظیم شود، به کامپایلر تایپاسکریپت دستور میدهد تا فراداده نوع زمان طراحی خاصی را در جاوااسکریپت کامپایل شده منتشر کند. این فراداده شامل نوع پارامترهای سازنده (design:paramtypes
)، نوع بازگشتی متدها (design:returntype
) و نوع پراپرتیها (design:type
) است.
این فراداده منتشر شده بخشی از زمان اجرای استاندارد جاوااسکریپت نیست. معمولاً توسط پلیفیل reflect-metadata
مصرف میشود که سپس آن را از طریق توابع Reflect.getMetadata()
در دسترس قرار میدهد. این برای الگوهای پیشرفته مانند تزریق وابستگی کاملاً حیاتی است، جایی که یک کانتینر نیاز دارد تا انواع وابستگیهایی را که یک کلاس نیاز دارد بدون پیکربندی صریح بداند.
الگوهای پیشرفته با دکوراتورها
دکوراتورها میتوانند برای ساخت الگوهای حتی پیچیدهتر ترکیب و گسترش یابند.
۱. تزئین دکوراتورها (دکوراتورهای مرتبه بالاتر)
شما میتوانید دکوراتورهایی ایجاد کنید که دکوراتورهای دیگر را تغییر یا ترکیب میکنند. این کمتر رایج است اما ماهیت تابعی دکوراتورها را نشان میدهد.
// A decorator that ensures a method is logged and also requires admin roles
function AdminAndLoggedMethod() {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Apply Authorization first (inner)
Authorization(["admin"])(target, propertyKey, descriptor);
// Then apply LogCall (outer)
LogCall(target, propertyKey, descriptor);
return descriptor; // Return the modified descriptor
};
}
class AdminPanel {
@AdminAndLoggedMethod()
deleteUserAccount(userId: string) {
console.log(`Deleting user account: ${userId}`);
return `User ${userId} deleted.`;
}
}
const adminPanel = new AdminPanel();
adminPanel.deleteUserAccount("user007");
/* Expected Output (assuming admin role):
[AUTH] Access granted for deleteUserAccount
[LOG] Calling deleteUserAccount with args: [ 'user007' ]
Deleting user account: user007
[LOG] Method deleteUserAccount returned: User user007 deleted.
*/
در اینجا، AdminAndLoggedMethod
یک فکتوری است که یک دکوراتور را برمیگرداند و در داخل آن دکوراتور، دو دکوراتور دیگر را اعمال میکند. این الگو میتواند ترکیبات پیچیده دکوراتور را کپسوله کند.
۲. استفاده از دکوراتورها برای Mixinها
در حالی که تایپاسکریپت راههای دیگری برای پیادهسازی mixinها ارائه میدهد، میتوان از دکوراتورها برای تزریق قابلیتها به کلاسها به روشی اعلانی استفاده کرد.
function ApplyMixins(constructors: Function[]) {
return function (derivedConstructor: Function) {
constructors.forEach(baseConstructor => {
Object.getOwnPropertyNames(baseConstructor.prototype).forEach(name => {
Object.defineProperty(
derivedConstructor.prototype,
name,
Object.getOwnPropertyDescriptor(baseConstructor.prototype, name) || Object.create(null)
);
});
});
};
}
class Disposable {
isDisposed: boolean = false;
dispose() {
this.isDisposed = true;
console.log("Object disposed.");
}
}
class Loggable {
log(message: string) {
console.log(`[Loggable] ${message}`);
}
}
@ApplyMixins([Disposable, Loggable])
class MyResource implements Disposable, Loggable {
// These properties/methods are injected by the decorator
isDisposed!: boolean;
dispose!: () => void;
log!: (message: string) => void;
constructor(public name: string) {
this.log(`Resource ${this.name} created.`);
}
cleanUp() {
this.dispose();
this.log(`Resource ${this.name} cleaned up.`);
}
}
const resource = new MyResource("NetworkConnection");
console.log(`Is disposed: ${resource.isDisposed}`);
resource.cleanUp();
console.log(`Is disposed: ${resource.isDisposed}`);
این دکوراتور @ApplyMixins
به صورت پویا متدها و پراپرتیها را از سازندههای پایه به پروتوتایپ کلاس مشتق شده کپی میکند و به طور مؤثری قابلیتها را «مخلوط» میکند.
نتیجهگیری: توانمندسازی توسعه مدرن تایپاسکریپت
دکوراتورهای تایپاسکریپت یک ویژگی قدرتمند و بیانی هستند که پارادایم جدیدی از برنامهنویسی مبتنی بر فراداده و جنبهگرا را امکانپذیر میسازند. آنها به توسعهدهندگان اجازه میدهند تا رفتارهای اعلانی را به کلاسها، متدها، پراپرتیها، accessorها و پارامترها اضافه، اصلاح و تقویت کنند بدون اینکه منطق اصلی آنها را تغییر دهند. این جداسازی دغدغهها به کدی تمیزتر، قابل نگهداریتر و بسیار قابل استفاده مجدد منجر میشود.
از سادهسازی تزریق وابستگی و پیادهسازی سیستمهای اعتبارسنجی قوی گرفته تا افزودن دغدغههای فراگیر مانند لاگگیری و نظارت بر عملکرد، دکوراتورها راهحل ظریفی برای بسیاری از چالشهای رایج توسعه ارائه میدهند. در حالی که وضعیت آزمایشی آنها نیازمند آگاهی است، پذیرش گسترده آنها در فریمورکهای اصلی نشاندهنده ارزش عملی و اهمیت آینده آنهاست.
با تسلط بر دکوراتورهای تایپاسکریپت، ابزار مهمی را به زرادخانه خود اضافه میکنید که شما را قادر میسازد اپلیکیشنهای قویتر، مقیاسپذیرتر و هوشمندتری بسازید. آنها را با مسئولیتپذیری بپذیرید، مکانیک آنها را درک کنید و سطح جدیدی از قدرت اعلانی را در پروژههای تایپاسکریپت خود باز کنید.