دکوراتورها (Decorators)
دکوراتور (Decorator) یعنی برچسب هوشمند روی کلاس یا عضو. این برچسب در زمان تعریف، اطلاعات اضافه می کند یا رفتار را می پیچد. توی فریم ورک هایی مثل Angular و NestJS خیلی استفاده می شود. اول بیایید با مفهوم «دکوراتور تایپ اسکریپت» آشنا شویم و آرام جلو برویم.
فعال سازی دکوراتورها
برای استفاده، باید در tsconfig.json گزینه ها را روشن کنی. experimentalDecorators یعنی اجازه استفاده از سینتکس دکوراتور. emitDecoratorMetadata یعنی خروجی نوع ها برای کتابخانه ها.
{\n "compilerOptions": {\n "target": "ES2020",\n "module": "commonjs",\n "experimentalDecorators": true,\n "emitDecoratorMetadata": true,\n "strictPropertyInitialization": false\n },\n "include": ["src/**/*.ts"]\n}\n
نکته: emitDecoratorMetadata برای کتابخانه هایی مثل class-validator مفید است.
انواع دکوراتور
چهار نوع اصلی داریم: کلاس، متد، ویژگی، پارامتر. هرکدام ورودی متفاوتی می گیرند و هنگام «تعریف» اجرا می شوند، نه هنگام «فراخوانی».
دکوراتور کلاس: مشاهده و تغییر سازنده
این دکوراتور روی سازنده می نشیند. می تواند کلاس را لاگ کند یا جایگزین کند. مثل برچسب نظم انضباطی روی پوشه مدرسه.
function logClass(constructor: Function) {\n console.log(`Class ${constructor.name} was defined at ${new Date().toISOString()}`);\n}\n\n@logClass\nclass UserService {\n getUsers() {\n return ['Alice', 'Bob', 'Charlie'];\n }\n}\n
دکوراتور کلاس: افزودن نسخه و پیچیدن سازنده
می خواهی نسخه بگذاری و هنگام ساخت شیء پیام بدهی؟ یک دکوراتور کارخانه ای بساز.
function versioned(version: string) {\n return function (constructor: Function) {\n constructor.prototype.version = version;\n const original = constructor;\n const newConstructor: any = function (...args: any[]) {\n console.log(`Creating instance of ${original.name} v${version}`);\n return new original(...args);\n };\n newConstructor.prototype = original.prototype;\n return newConstructor;\n };\n}\n\n@versioned('1.0.0')\nclass ApiClient {\n fetchData() {\n console.log('Fetching data...');\n }\n}\n\nconst client = new ApiClient();\nconsole.log((client as any).version);\nclient.fetchData();\n
دکوراتور کلاس: مهر و موم کردن
با sealed جلوی افزودن ویژگی های جدید را بگیر. مثل لاک کردن دفتر نمره.
function sealed(constructor: Function) {\n console.log(`Sealing ${constructor.name}...`);\n Object.seal(constructor);\n Object.seal(constructor.prototype);\n}\n\n@sealed\nclass Greeter {\n greeting: string;\n constructor(message: string) {\n this.greeting = message;\n }\n greet() {\n return `Hello, ${this.greeting}`;\n }\n}\n
دکوراتور متد: زمان سنجی و دسترسی
دکوراتور متد می تواند رفتار را بپیچد. مثل بادیگارد کنار در کلاس.
اندازه گیری زمان اجرا
function measureTime(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalMethod = descriptor.value;\n descriptor.value = function (...args: any[]) {\n const start = performance.now();\n const result = originalMethod.apply(this, args);\n const end = performance.now();\n console.log(`${propertyKey} executed in ${(end - start).toFixed(2)}ms`);\n return result;\n };\n return descriptor;\n}\n\nclass DataProcessor {\n @measureTime\n processData(data: number[]): number[] {\n for (let i = 0; i < 100000000; i++) {\n /* processing */\n }\n return data.map(x => x * 2);\n }\n}\n\nconst processor = new DataProcessor();\nprocessor.processData([1, 2, 3, 4, 5]);\n
کنترل دسترسی با نقش
type UserRole = 'admin' | 'editor' | 'viewer';\n\nconst currentUser = {\n id: 1,\n name: 'John Doe',\n roles: ['viewer'] as UserRole[]\n};\n\nfunction AllowedRoles(...allowedRoles: UserRole[]) {\n return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalMethod = descriptor.value;\n descriptor.value = function (...args: any[]) {\n const hasPermission = allowedRoles.some(role => currentUser.roles.includes(role));\n if (!hasPermission) {\n throw new Error(`User ${currentUser.name} is not authorized to call ${propertyKey}`);\n }\n return originalMethod.apply(this, args);\n };\n return descriptor;\n };\n}\n\nclass DocumentService {\n @AllowedRoles('admin', 'editor')\n deleteDocument(id: string) {\n console.log(`Document ${id} deleted`);\n }\n\n @AllowedRoles('admin', 'editor', 'viewer')\n viewDocument(id: string) {\n console.log(`Viewing document ${id}`);\n }\n}\n\nconst docService = new DocumentService();\ntry {\n docService.viewDocument('doc123');\n docService.deleteDocument('doc123');\n} catch (error: any) {\n console.error(error.message);\n}\ncurrentUser.roles = ['admin'];\ndocService.deleteDocument('doc123');\n
هشدار منسوخ شدن
function deprecated(message: string) {\n return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalMethod = descriptor.value;\n descriptor.value = function (...args: any[]) {\n console.warn(`Warning: ${propertyKey} is deprecated. ${message}`);\n return originalMethod.apply(this, args);\n };\n return descriptor;\n };\n}\n\nclass PaymentService {\n @deprecated('Use processPaymentV2 instead')\n processPayment(amount: number, currency: string) {\n console.log(`Processing payment of ${amount} ${currency}`);\n }\n\n processPaymentV2(amount: number, currency: string) {\n console.log(`Processing payment v2 of ${amount} ${currency}`);\n }\n}\n\nconst payment = new PaymentService();\npayment.processPayment(100, 'USD');\npayment.processPaymentV2(100, 'USD');\n
دکوراتور ویژگی: قالب دهی و لاگ
می خواهی هنگام set شدن، مقدار قالب بگیرد؟ یا روی get/set لاگ بگیری؟ این ها نمونه های ساده هستند.
قالب دهی خودکار
function format(formatString: string) {\n return function (target: any, propertyKey: string) {\n let value: string;\n const getter = () => value;\n const setter = (newVal: string) => {\n value = formatString.replace('{}', newVal);\n };\n Object.defineProperty(target, propertyKey, {\n get: getter,\n set: setter,\n enumerable: true,\n configurable: true\n });\n };\n}\n\nclass Greeter {\n @format('Hello, {}!')\n greeting: string;\n}\n\nconst greeter = new Greeter();\ngreeter.greeting = 'World';\nconsole.log(greeter.greeting);\n
ثبت دسترسی ها
function logProperty(target: any, propertyKey: string) {\n let value: any;\n const getter = function () {\n console.log(`Getting ${propertyKey}: ${value}`);\n return value;\n };\n const setter = function (newVal: any) {\n console.log(`Setting ${propertyKey} from ${value} to ${newVal}`);\n value = newVal;\n };\n Object.defineProperty(target, propertyKey, {\n get: getter,\n set: setter,\n enumerable: true,\n configurable: true\n });\n}\n\nclass Product {\n @logProperty\n name: string;\n @logProperty\n price: number;\n constructor(name: string, price: number) {\n this.name = name;\n this.price = price;\n }\n}\n\nconst product = new Product('Laptop', 999.99);\nproduct.price = 899.99;\nconsole.log(product.name);\n
اجباری کردن مقدار
function required(target: any, propertyKey: string) {\n let value: any;\n const getter = function () {\n if (value === undefined) {\n throw new Error(`Property ${propertyKey} is required`);\n }\n return value;\n };\n const setter = function (newVal: any) {\n value = newVal;\n };\n Object.defineProperty(target, propertyKey, {\n get: getter,\n set: setter,\n enumerable: true,\n configurable: true\n });\n}\n\nclass User {\n @required\n username: string;\n @required\n email: string;\n age?: number;\n constructor(username: string, email: string) {\n this.username = username;\n this.email = email;\n }\n}\n\nconst user1 = new User('johndoe', 'john@example.com');\n
دکوراتور پارامتر و متد راستی آزمایی
می خواهی ورودی ها سالم باشند؟ با متادیتا روی پارامتر علامت بزن. بعد متد را قبل از اجرا بررسی کن. مثل چک لیست ورود به آزمون.
function validateParam(type: 'string' | 'number' | 'boolean') {\n return function (target: any, propertyKey: string | symbol, parameterIndex: number) {\n const existingValidations: any[] = Reflect.getOwnMetadata('validations', target, propertyKey) || [];\n existingValidations.push({ index: parameterIndex, type });\n Reflect.defineMetadata('validations', existingValidations, target, propertyKey);\n };\n}\n\nfunction validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalMethod = descriptor.value;\n descriptor.value = function (...args: any[]) {\n const validations: Array<{ index: number, type: string }> = Reflect.getOwnMetadata('validations', target, propertyKey) || [];\n for (const validation of validations) {\n const { index, type } = validation;\n const param = args[index];\n let isValid = false;\n switch (type) {\n case 'string': {\n isValid = typeof param === 'string' && param.length > 0;\n break;\n }\n case 'number': {\n isValid = typeof param === 'number' && !isNaN(param as any);\n break;\n }\n case 'boolean': {\n isValid = typeof param === 'boolean';\n break;\n }\n }\n if (!isValid) {\n throw new Error(`Parameter at index ${index} failed ${type} validation`);\n }\n }\n return originalMethod.apply(this, args);\n };\n return descriptor;\n}\n\nclass UserService {\n @validate\n createUser(@validateParam('string') name: string, @validateParam('number') age: number, @validateParam('boolean') isActive: boolean) {\n console.log(`Creating user: ${name}, ${age}, ${isActive}`);\n }\n}\n\nconst service = new UserService();\nservice.createUser('John', 30, true);\n
کارخانه دکوراتور و ترتیب اجرا
کارخانه دکوراتور (Decorator Factory) تابعی است که دکوراتور می سازد. ترتیب اجرا هم مهم است؛ معمولاً از پایین به بالا.
لاگ قابل پیکربندی
function logWithConfig(config: { level: 'log' | 'warn' | 'error', message?: string }) {\n return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalMethod = descriptor.value;\n descriptor.value = function (...args: any[]) {\n const { level = 'log', message = 'Executing method' } = config;\n console[level](`${message}: ${propertyKey}`, { arguments: args });\n const result = originalMethod.apply(this, args);\n console[level](`${propertyKey} completed`);\n return result;\n };\n return descriptor;\n };\n}\n\nclass PaymentService {\n @logWithConfig({ level: 'log', message: 'Processing payment' })\n processPayment(amount: number) {\n console.log(`Processing payment of $${amount}`);\n }\n}\n
نمونه ترتیب ارزیابی
function first() {\n console.log('first(): factory evaluated');\n return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n console.log('first(): called');\n };\n}\n\nfunction second() {\n console.log('second(): factory evaluated');\n return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n console.log('second(): called');\n };\n}\n\nclass ExampleClass {\n @first()\n @second()\n method() {}\n}\n
نمونه واقعی: کنترلر API ساده
با چند دکوراتور ساده می توانیم مسیردهی شبیه فریم ورک ها بسازیم. این تنها تمرین آموزشی است.
const ROUTES: any[] = [];\n\nfunction Controller(prefix: string = '') {\n return function (constructor: Function) {\n constructor.prototype.prefix = prefix;\n };\n}\n\nfunction Get(path: string = '') {\n return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n ROUTES.push({\n method: 'get',\n path,\n handler: descriptor.value,\n target: target.constructor\n });\n };\n}\n\n@Controller('/users')\nclass UserController {\n @Get('/')\n getAllUsers() {\n return { users: [{ id: 1, name: 'John' }] };\n }\n\n @Get('/:id')\n getUserById(id: string) {\n return { id, name: 'John' };\n }\n}\n\nfunction registerRoutes() {\n ROUTES.forEach(route => {\n const prefix = route.target.prototype.prefix || '';\n console.log(`Registered ${route.method.toUpperCase()} ${prefix}${route.path}`);\n });\n}\n\nregisterRoutes();\n
بهترین تجربه و خطاهای رایج
نکته: هر دکوراتور یک مسئولیت داشته باشد. مستند کن. حواست به کارایی باشد. برای سناریوهای پیشرفته، از متادیتا کمک بگیر.
هشدار: فعال سازی را فراموش نکن. ترتیب ارزیابی را بشناس. ویژگی ها قبل از مقداردهی اجرا می شوند. اگر با متادیتا کار می کنی، import را یادت نرود.
ادامه مسیر
برای مفاهیم تکمیلی به نوع های پیشرفته سر بزن. همچنین کنار دکوراتور تایپ اسکریپت می توانی با JSDoc کد را خواناتر کنی.
جمع بندی سریع
- دکوراتور هنگام «تعریف» اجرا می شود.
- کلاس، متد، ویژگی، پارامتر را پوشش می دهد.
- با کارخانه ها، دکوراتور را قابل تنظیم کن.
- ترتیب اجرا اهمیت دارد.
- اول
experimentalDecoratorsرا فعال کن.