با دکوراتورهای جاوا اسکریپت آشنا شوید: فراداده اضافه کنید، کلاسها/متدها را تبدیل کنید و عملکرد کد خود را به روشی تمیز و اعلانی بهبود بخشید.
دکوراتورهای جاوا اسکریپت: فراداده و تبدیل
دکوراتورهای جاوا اسکریپت، ویژگیای الهام گرفته از زبانهایی مانند پایتون و جاوا، روشی قدرتمند و گویا برای افزودن فراداده و تبدیل کلاسها، متدها، ویژگیها و پارامترها فراهم میکنند. آنها سینتکس تمیز و اعلانی برای بهبود عملکرد کد و ترویج جداسازی مسئولیتها (separation of concerns) ارائه میدهند. در حالی که دکوراتورها هنوز یک افزودنی نسبتاً جدید به اکوسیستم جاوا اسکریپت هستند، محبوبیت آنها در حال افزایش است، به ویژه در فریمورکهایی مانند Angular و کتابخانههایی که از فراداده برای تزریق وابستگی و سایر ویژگیهای پیشرفته استفاده میکنند. این مقاله به بررسی اصول اولیه دکوراتورهای جاوا اسکریپت، کاربرد آنها و پتانسیل آنها برای ایجاد پایگاهکدهای قابل نگهداری و توسعهپذیرتر میپردازد.
دکوراتورهای جاوا اسکریپت چه هستند؟
در هسته خود، دکوراتورها نوع خاصی از اعلانها هستند که میتوانند به کلاسها، متدها، اکسسورها، پراپرتیها یا پارامترها متصل شوند. آنها از سینتکس @expression
استفاده میکنند، جایی که expression
باید به یک تابع ارزیابی شود که در زمان اجرا با اطلاعاتی در مورد اعلان تزئین شده فراخوانی میشود. دکوراتورها اساساً به عنوان توابعی عمل میکنند که رفتار عنصر تزئین شده را تغییر داده یا گسترش میدهند.
دکوراتورها را به عنوان راهی برای بستهبندی یا تقویت کد موجود بدون تغییر مستقیم آن در نظر بگیرید. این اصل، که در طراحی نرمافزار به عنوان الگوی دکوراتور شناخته میشود، به شما امکان میدهد تا عملکرد را به صورت پویا به یک شیء اضافه کنید.
فعالسازی دکوراتورها
در حالی که دکوراتورها بخشی از استاندارد ECMAScript هستند، در اکثر محیطهای جاوا اسکریپت به طور پیشفرض فعال نیستند. برای استفاده از آنها، معمولاً باید ابزارهای ساخت (build tools) خود را پیکربندی کنید. در اینجا نحوه فعال کردن دکوراتورها در برخی از محیطهای رایج آورده شده است:
- تایپاسکریپت: دکوراتورها به صورت بومی در تایپاسکریپت پشتیبانی میشوند. اطمینان حاصل کنید که گزینه کامپایلر
experimentalDecorators
در فایلtsconfig.json
شما رویtrue
تنظیم شده باشد:
{
"compilerOptions": {
"target": "esnext",
"experimentalDecorators": true,
"emitDecoratorMetadata": true, // Optional, but often useful
"module": "commonjs", // Or another module system like "es6" or "esnext"
"moduleResolution": "node"
}
}
- Babel: اگر از Babel استفاده میکنید، باید پلاگین
@babel/plugin-proposal-decorators
را نصب و پیکربندی کنید:
npm install --save-dev @babel/plugin-proposal-decorators
سپس، پلاگین را به پیکربندی Babel خود (مثلاً .babelrc
یا babel.config.js
) اضافه کنید:
{
"plugins": [["@babel/plugin-proposal-decorators", { "version": "2023-05" }]]
}
گزینه version
مهم است و باید با نسخه پروپوزال دکوراتورهایی که هدف قرار دادهاید مطابقت داشته باشد. برای آخرین نسخه پیشنهادی، به مستندات پلاگین Babel مراجعه کنید.
انواع دکوراتورها
انواع مختلفی از دکوراتورها وجود دارد که هر کدام برای عناصر خاصی طراحی شدهاند:
- دکوراتورهای کلاس: روی کلاسها اعمال میشوند.
- دکوراتورهای متد: روی متدهای درون یک کلاس اعمال میشوند.
- دکوراتورهای اکسسور: روی اکسسورهای getter یا setter اعمال میشوند.
- دکوراتورهای پراپرتی: روی پراپرتیهای یک کلاس اعمال میشوند.
- دکوراتورهای پارامتر: روی پارامترهای یک متد یا سازنده اعمال میشوند.
دکوراتورهای کلاس
دکوراتورهای کلاس به سازنده (constructor) یک کلاس اعمال میشوند و میتوانند برای مشاهده، تغییر یا جایگزینی تعریف یک کلاس استفاده شوند. آنها سازنده کلاس را به عنوان تنها آرگومان خود دریافت میکنند.
مثال:
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
// Attempting to add properties to the sealed class or its prototype will fail
در این مثال، دکوراتور @sealed
از تغییرات بیشتر در کلاس Greeter
و پروتوتایپ آن جلوگیری میکند. این میتواند برای اطمینان از تغییرناپذیری یا جلوگیری از تغییرات تصادفی مفید باشد.
دکوراتورهای متد
دکوراتورهای متد به متدهای درون یک کلاس اعمال میشوند. آنها سه آرگومان دریافت میکنند:
target
: پروتوتایپ کلاس (برای متدهای نمونه) یا سازنده کلاس (برای متدهای استاتیک).propertyKey
: نام متدی که تزئین میشود.descriptor
: توصیفگر پراپرتی (property descriptor) برای متد.
مثال:
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with arguments: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`Method ${propertyKey} returned: ${result}`);
return result;
};
return descriptor;
}
class Calculator {
@log
add(x: number, y: number) {
return x + y;
}
}
const calculator = new Calculator();
calculator.add(2, 3); // Output: Calling add with arguments: [2,3]
// Method add returned: 5
دکوراتور @log
آرگومانها و مقدار بازگشتی متد add
را ثبت میکند. این یک مثال ساده از نحوه استفاده از دکوراتورهای متد برای لاگگیری، پروفایلسازی یا سایر دغدغههای فراگیر (cross-cutting concerns) است.
دکوراتورهای اکسسور (Accessor)
دکوراتورهای اکسسور شبیه به دکوراتورهای متد هستند اما به اکسسورهای getter یا setter اعمال میشوند. آنها نیز همان سه آرگومان را دریافت میکنند: target
، propertyKey
و descriptor
.
مثال:
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() {
return this._x;
}
set x(value: number) {
this._x = value;
}
}
const point = new Point(1, 2);
// Object.defineProperty(point, 'x', { configurable: true }); // Would throw an error because 'x' is not configurable
دکوراتور @configurable(false)
از پیکربندی مجدد getter x
جلوگیری میکند و آن را غیرقابل پیکربندی (non-configurable) میسازد.
دکوراتورهای پراپرتی (Property)
دکوراتورهای پراپرتی به پراپرتیهای یک کلاس اعمال میشوند. آنها دو آرگومان دریافت میکنند:
target
: پروتوتایپ کلاس (برای پراپرتیهای نمونه) یا سازنده کلاس (برای پراپرتیهای استاتیک).propertyKey
: نام پراپرتی که تزئین میشود.
مثال:
function readonly(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
writable: false,
});
}
class Person {
@readonly
name: string;
constructor(name: string) {
this.name = name;
}
}
const person = new Person("Alice");
// person.name = "Bob"; // This will cause an error in strict mode because 'name' is readonly
دکوراتور @readonly
پراپرتی name
را فقط-خواندنی (read-only) میکند و از تغییر آن پس از مقداردهی اولیه جلوگیری میکند.
دکوراتورهای پارامتر
دکوراتورهای پارامتر به پارامترهای یک متد یا سازنده اعمال میشوند. آنها سه آرگومان دریافت میکنند:
target
: پروتوتایپ کلاس (برای متدهای نمونه) یا سازنده کلاس (برای متدهای استاتیک یا سازندهها).propertyKey
: نام متد یا سازنده.parameterIndex
: ایندکس پارامتر در لیست پارامترها.
دکوراتورهای پارامتر اغلب با reflection برای ذخیره فراداده در مورد پارامترهای یک تابع استفاده میشوند. این فراداده سپس میتواند در زمان اجرا برای تزریق وابستگی یا اهداف دیگر استفاده شود. برای اینکه این کار به درستی انجام شود، باید گزینه کامپایلر emitDecoratorMetadata
را در فایل tsconfig.json
خود فعال کنید.
مثال (با استفاده از reflect-metadata
):
import 'reflect-metadata';
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
let existingRequiredParameters: number[] = Reflect.getOwnMetadata("required", target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata("required", existingRequiredParameters, target, propertyKey);
}
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
let method = descriptor.value!;
descriptor.value = function (...args: any[]) {
let requiredParameters: number[] = Reflect.getOwnMetadata("required", target, propertyName);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (args[parameterIndex] === null || args[parameterIndex] === undefined) {
throw new Error(`Missing required argument at index ${parameterIndex}`);
}
}
}
return method.apply(this, args);
};
}
class User {
name: string;
age: number;
constructor(@required name: string, public surname: string, @required age: number) {
this.name = name;
this.age = age;
}
@validate
greet(prefix: string, @required salutation: string): string {
return `${prefix} ${salutation} ${this.name}`;
}
}
// Usage
try {
const user1 = new User("John", "Doe", 30);
console.log(user1.greet("Mr.", "Hello"));
const user2 = new User(undefined as any, "Doe", null as any);
} catch (error) {
console.error(error.message);
}
try {
const user = new User("John", "Doe", 30);
console.log(user.greet("Mr.", undefined as any));
} catch (error) {
console.error(error.message);
}
در این مثال، دکوراتور @required
پارامترها را به عنوان الزامی علامتگذاری میکند. دکوراتور @validate
سپس از reflection (از طریق reflect-metadata
) برای بررسی حضور پارامترهای الزامی قبل از فراخوانی متد استفاده میکند. این مثال کاربرد اولیه را نشان میدهد و توصیه میشود که در یک سناریوی تولیدی، اعتبارسنجی پارامتر قویتری ایجاد کنید.
برای نصب reflect-metadata
:
npm install reflect-metadata --save
استفاده از دکوراتورها برای فراداده
یکی از کاربردهای اصلی دکوراتورها، پیوست کردن فراداده به کلاسها و اعضای آنهاست. این فراداده میتواند در زمان اجرا برای اهداف مختلفی مانند تزریق وابستگی، سریالسازی و اعتبارسنجی استفاده شود. کتابخانه reflect-metadata
یک روش استاندارد برای ذخیره و بازیابی فراداده فراهم میکند.
مثال:
import 'reflect-metadata';
const TYPE_KEY = "design:type";
const PARAMTYPES_KEY = "design:paramtypes";
const RETURNTYPE_KEY = "design:returntype";
function Type(type: any) {
return Reflect.metadata(TYPE_KEY, type);
}
function LogType(target: any, propertyKey: string) {
const t = Reflect.getMetadata(TYPE_KEY, target, propertyKey);
console.log(`${target.constructor.name}.${propertyKey} type: ${t.name}`);
}
class Demo {
@LogType
public name: string;
constructor(name: string){
this.name = name;
}
}
فکتوریهای دکوراتور (Decorator Factories)
فکتوریهای دکوراتور توابعی هستند که یک دکوراتور را برمیگردانند. آنها به شما اجازه میدهند تا آرگومانهایی را به دکوراتور ارسال کنید، که آن را انعطافپذیرتر و قابل استفاده مجدد میکند.
مثال:
function deprecated(deprecationReason: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.warn(`Method ${propertyKey} is deprecated: ${deprecationReason}`);
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class LegacyComponent {
@deprecated("Use the newMethod instead.")
oldMethod() {
console.log("Old method called");
}
newMethod() {
console.log("New method called");
}
}
const component = new LegacyComponent();
component.oldMethod(); // Output: Method oldMethod is deprecated: Use the newMethod instead.
// Old method called
فکتوری دکوراتور @deprecated
یک پیام منسوخ شدن را به عنوان آرگومان میگیرد و هنگام فراخوانی متد تزئین شده، یک هشدار ثبت میکند. این به شما امکان میدهد تا متدها را به عنوان منسوخ شده علامتگذاری کرده و راهنماییهایی برای توسعهدهندگان در مورد چگونگی مهاجرت به جایگزینهای جدیدتر ارائه دهید.
کاربردهای واقعی
دکوراتورها طیف گستردهای از کاربردها را در توسعه مدرن جاوا اسکریپت دارند:
- تزریق وابستگی: فریمورکهایی مانند Angular به شدت برای تزریق وابستگی به دکوراتورها متکی هستند.
- مسیریابی (Routing): در برنامههای وب، دکوراتورها میتوانند برای تعریف مسیرها برای کنترلرها و متدها استفاده شوند.
- اعتبارسنجی (Validation): دکوراتورها میتوانند برای اعتبارسنجی دادههای ورودی و اطمینان از مطابقت آنها با معیارهای خاص استفاده شوند.
- مجوزدهی (Authorization): دکوراتورها میتوانند برای اجرای سیاستهای امنیتی و محدود کردن دسترسی به متدها یا منابع خاص استفاده شوند.
- لاگگیری و پروفایلسازی: همانطور که در مثالهای بالا نشان داده شد، دکوراتورها میتوانند برای لاگگیری و پروفایلسازی اجرای کد استفاده شوند.
- مدیریت وضعیت (State Management): دکوراتورها میتوانند با کتابخانههای مدیریت وضعیت ادغام شوند تا کامپوننتها را به طور خودکار هنگام تغییر وضعیت بهروز کنند.
مزایای استفاده از دکوراتورها
- خوانایی بهتر کد: دکوراتورها یک سینتکس اعلانی برای افزودن عملکرد فراهم میکنند که درک و نگهداری کد را آسانتر میکند.
- جداسازی مسئولیتها: دکوراتورها به شما امکان میدهند دغدغههای فراگیر (مانند لاگگیری، اعتبارسنجی، مجوزدهی) را از منطق اصلی کسبوکار جدا کنید.
- قابلیت استفاده مجدد: دکوراتورها میتوانند در چندین کلاس و متد مجدداً استفاده شوند و از تکرار کد بکاهند.
- توسعهپذیری: دکوراتورها توسعه عملکرد کد موجود را بدون تغییر مستقیم آن آسان میکنند.
چالشها و ملاحظات
- منحنی یادگیری: دکوراتورها یک ویژگی نسبتاً جدید هستند و ممکن است یادگیری نحوه استفاده موثر از آنها زمانبر باشد.
- سازگاری: اطمینان حاصل کنید که محیط هدف شما از دکوراتورها پشتیبانی میکند و ابزارهای ساخت خود را به درستی پیکربندی کردهاید.
- دیباگ کردن: دیباگ کردن کدی که از دکوراتورها استفاده میکند میتواند چالشبرانگیزتر از دیباگ کردن کد معمولی باشد، به خصوص اگر دکوراتورها پیچیده باشند.
- استفاده بیش از حد: از استفاده بیش از حد از دکوراتورها خودداری کنید، زیرا این کار میتواند درک و نگهداری کد شما را دشوارتر کند. از آنها به صورت استراتژیک برای اهداف خاص استفاده کنید.
- سربار زمان اجرا: دکوراتورها میتوانند مقداری سربار زمان اجرا ایجاد کنند، به خصوص اگر عملیات پیچیدهای انجام دهند. هنگام استفاده از دکوراتورها در برنامههای حساس به عملکرد، پیامدهای عملکردی را در نظر بگیرید.
نتیجهگیری
دکوراتورهای جاوا اسکریپت ابزاری قدرتمند برای بهبود عملکرد کد و ترویج جداسازی مسئولیتها هستند. با ارائه یک سینتکس تمیز و اعلانی برای افزودن فراداده و تبدیل کلاسها، متدها، پراپرتیها و پارامترها، دکوراتورها میتوانند به شما در ایجاد پایگاهکدهای قابل نگهداری، قابل استفاده مجدد و توسعهپذیرتر کمک کنند. در حالی که آنها با یک منحنی یادگیری و برخی چالشهای بالقوه همراه هستند، مزایای استفاده از دکوراتورها در زمینه مناسب میتواند قابل توجه باشد. با ادامه تکامل اکوسیستم جاوا اسکریپت، دکوراتورها احتمالاً به بخش مهمی از توسعه مدرن جاوا اسکریپت تبدیل خواهند شد.
بررسی کنید که چگونه دکوراتورها میتوانند کد موجود شما را ساده کنند یا شما را قادر به نوشتن برنامههای گویاتر و قابل نگهداریتر کنند. با برنامهریزی دقیق و درک قوی از قابلیتهای آنها، میتوانید از دکوراتورها برای ایجاد راهحلهای جاوا اسکریپت قویتر و مقیاسپذیرتر استفاده کنید.