สำรวจพลังของ TypeScript Decorators สำหรับการเขียนโปรแกรมเมทาดาทา, การเขียนโปรแกรมเชิงลักษณะ (AOP) และการปรับปรุงโค้ดด้วยรูปแบบ declarative คู่มือฉบับสมบูรณ์สำหรับนักพัฒนาทั่วโลก
TypeScript Decorators: การเรียนรู้รูปแบบการเขียนโปรแกรมเมทาดาทาเพื่อแอปพลิเคชันที่แข็งแกร่ง
ในวงการการพัฒนาซอฟต์แวร์สมัยใหม่ที่กว้างใหญ่ การดูแลรักษาโค้ดเบสให้สะอาด ขยายขนาดได้ และจัดการได้ง่ายเป็นสิ่งสำคัญยิ่ง TypeScript ซึ่งมีระบบไทป์ที่ทรงพลังและคุณสมบัติขั้นสูง ได้มอบเครื่องมือให้นักพัฒนาเพื่อให้บรรลุเป้าหมายนี้ หนึ่งในคุณสมบัติที่น่าสนใจและเปลี่ยนแปลงวงการมากที่สุดคือ Decorators แม้ว่าในขณะที่เขียนบทความนี้จะยังเป็นคุณสมบัติทดลอง (ข้อเสนอ Stage 3 สำหรับ ECMAScript) แต่ decorators ก็ถูกใช้อย่างแพร่หลายในเฟรมเวิร์กอย่าง Angular และ TypeORM ซึ่งเปลี่ยนแปลงวิธีการที่เราเข้าถึงรูปแบบการออกแบบ, การเขียนโปรแกรมเมทาดาทา และการเขียนโปรแกรมเชิงลักษณะ (AOP) โดยพื้นฐาน
คู่มือฉบับสมบูรณ์นี้จะเจาะลึกเกี่ยวกับ TypeScript decorators โดยสำรวจกลไก, ประเภทต่างๆ, การประยุกต์ใช้จริง และแนวทางปฏิบัติที่ดีที่สุด ไม่ว่าคุณจะกำลังสร้างแอปพลิเคชันระดับองค์กรขนาดใหญ่, ไมโครเซอร์วิส หรือเว็บอินเทอร์เฟซฝั่งไคลเอนต์ การทำความเข้าใจ decorators จะช่วยให้คุณสามารถเขียนโค้ด TypeScript ที่เป็นแบบ declarative, บำรุงรักษาง่าย และมีประสิทธิภาพมากขึ้น
ทำความเข้าใจแนวคิดหลัก: Decorator คืออะไร?
โดยพื้นฐานแล้ว decorator คือการประกาศชนิดพิเศษที่สามารถแนบไปกับการประกาศคลาส, เมธอด, accessor, property หรือพารามิเตอร์ได้ Decorators คือฟังก์ชันที่คืนค่าใหม่ (หรือแก้ไขค่าที่มีอยู่) สำหรับเป้าหมายที่มันตกแต่ง วัตถุประสงค์หลักของมันคือการเพิ่มเมทาดาทาหรือเปลี่ยนแปลงพฤติกรรมของการประกาศที่มันแนบอยู่ โดยไม่ต้องแก้ไขโครงสร้างโค้ดพื้นฐานโดยตรง วิธีการเสริมโค้ดจากภายนอกแบบ declarative นี้มีประสิทธิภาพอย่างเหลือเชื่อ
ลองนึกภาพ decorators เป็นเหมือนคำอธิบายประกอบ (annotations) หรือป้ายกำกับ (labels) ที่คุณนำไปใช้กับส่วนต่างๆ ของโค้ดของคุณ จากนั้นป้ายกำกับเหล่านี้สามารถถูกอ่านหรือดำเนินการโดยส่วนอื่นๆ ของแอปพลิเคชันหรือโดยเฟรมเวิร์ก ซึ่งมักจะเกิดขึ้นในขณะรันไทม์ เพื่อให้ฟังก์ชันการทำงานหรือการกำหนดค่าเพิ่มเติม
ไวยากรณ์ของ Decorator
Decorators จะมีสัญลักษณ์ @
นำหน้า ตามด้วยชื่อฟังก์ชันของ decorator และจะถูกวางไว้ก่อนหน้าการประกาศที่มันจะตกแต่งทันที
@MyDecorator\nclass MyClass {\n @AnotherDecorator\n myMethod() {\n // ...\n }\n}
การเปิดใช้งาน Decorators ใน TypeScript
ก่อนที่คุณจะสามารถใช้ decorators ได้ คุณต้องเปิดใช้งานตัวเลือกคอมไพเลอร์ experimentalDecorators
ในไฟล์ tsconfig.json
ของคุณ นอกจากนี้ สำหรับความสามารถในการสะท้อนเมทาดาทาขั้นสูง (ซึ่งมักใช้โดยเฟรมเวิร์ก) คุณจะต้องใช้ emitDecoratorMetadata
และ polyfill reflect-metadata
ด้วย
// tsconfig.json\n{\n "compilerOptions": {\n "target": "ES2017",\n "module": "commonjs",\n "experimentalDecorators": true,\n "emitDecoratorMetadata": true,\n "outDir": "./dist",\n "strict": true,\n "esModuleInterop": true,\n "skipLibCheck": true,\n "forceConsistentCasingInFileNames": true\n }\n}
คุณยังต้องติดตั้ง reflect-metadata
:
npm install reflect-metadata --save\n# or\nyarn add reflect-metadata
และนำเข้า (import) ที่ส่วนบนสุดของไฟล์เริ่มต้นของแอปพลิเคชัน (เช่น main.ts
หรือ app.ts
):
import "reflect-metadata";\n// โค้ดแอปพลิเคชันของคุณตามมา
Decorator Factories: การปรับแต่งที่ปลายนิ้วของคุณ
แม้ว่า decorator พื้นฐานจะเป็นฟังก์ชัน แต่บ่อยครั้งที่คุณจำเป็นต้องส่งอาร์กิวเมนต์ไปยัง decorator เพื่อกำหนดค่าพฤติกรรมของมัน ซึ่งทำได้โดยใช้ decorator factory Decorator factory คือฟังก์ชันที่คืนค่าฟังก์ชัน decorator ที่แท้จริง เมื่อคุณใช้ decorator factory คุณจะเรียกมันพร้อมกับอาร์กิวเมนต์ของมัน และมันจะคืนค่าฟังก์ชัน decorator ที่ TypeScript นำไปใช้กับโค้ดของคุณ
ตัวอย่างการสร้าง Decorator Factory แบบง่าย
ลองสร้าง factory สำหรับ Logger
decorator ที่สามารถบันทึกข้อความด้วยคำนำหน้าที่แตกต่างกันได้
function Logger(prefix: string) {\n return function (target: Function) {\n console.log(`[${prefix}] Class ${target.name} has been defined.`);\n };\n}\n\n@Logger("APP_INIT")\nclass ApplicationBootstrap {\n constructor() {\n console.log("Application is starting...");\n }\n}\n\nconst app = new ApplicationBootstrap();\n// ผลลัพธ์:\n// [APP_INIT] Class ApplicationBootstrap has been defined.\n// Application is starting...
ในตัวอย่างนี้ Logger("APP_INIT")
คือการเรียก decorator factory ซึ่งจะคืนค่าฟังก์ชัน decorator ที่แท้จริงที่รับ target: Function
(constructor ของคลาส) เป็นอาร์กิวเมนต์ ซึ่งช่วยให้สามารถกำหนดค่าพฤติกรรมของ decorator แบบไดนามิกได้
ประเภทของ Decorators ใน TypeScript
TypeScript รองรับ decorators 5 ประเภทที่แตกต่างกัน โดยแต่ละประเภทจะใช้กับการประกาศชนิดเฉพาะ รูปแบบ (signature) ของฟังก์ชัน decorator จะแตกต่างกันไปตามบริบทที่นำไปใช้
1. Class Decorators
Class decorators ถูกนำไปใช้กับการประกาศคลาส ฟังก์ชัน decorator จะได้รับ constructor ของคลาสเป็นอาร์กิวเมนต์เพียงตัวเดียว class decorator สามารถสังเกต, แก้ไข หรือแม้กระทั่งแทนที่นิยามของคลาสได้
Signature:
function ClassDecorator(target: Function) { ... }
Return Value:
หาก class decorator คืนค่ากลับมา ค่านั้นจะมาแทนที่การประกาศคลาสด้วยฟังก์ชัน constructor ที่ให้มา นี่เป็นคุณสมบัติที่ทรงพลัง ซึ่งมักใช้สำหรับ mixins หรือการเสริมคลาส หากไม่มีการคืนค่า คลาสเดิมจะถูกใช้งาน
Use Cases:
- การลงทะเบียนคลาสใน dependency injection container
- การใช้ mixins หรือฟังก์ชันเพิ่มเติมกับคลาส
- การกำหนดค่าเฉพาะของเฟรมเวิร์ก (เช่น การกำหนดเส้นทางในเว็บเฟรมเวิร์ก)
- การเพิ่ม lifecycle hooks ให้กับคลาส
ตัวอย่าง Class Decorator: การฉีด Service
ลองจินตนาการถึงสถานการณ์ dependency injection แบบง่ายๆ ที่คุณต้องการทำเครื่องหมายคลาสว่าเป็น "injectable" และอาจระบุชื่อสำหรับมันใน container
const InjectableServiceRegistry = new Map();\n\nfunction Injectable(name?: string) {\n return function(constructor: T) {\n const serviceName = name || constructor.name;\n InjectableServiceRegistry.set(serviceName, constructor);\n console.log(`Registered service: ${serviceName}`);\n\n // คุณสามารถคืนค่าคลาสใหม่ที่นี่เพื่อเพิ่มพฤติกรรมได้\n return class extends constructor {\n createdAt = new Date();\n // คุณสมบัติหรือเมธอดเพิ่มเติมสำหรับ services ที่ถูกฉีดทั้งหมด\n };\n };\n}\n\n@Injectable("UserService")\nclass UserDataService {\n getUsers() {\n return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];\n }\n}\n\n@Injectable()\nclass ProductDataService {\n getProducts() {\n return [{ id: 101, name: "Laptop" }, { id: 102, name: "Mouse" }];\n }\n}\n\nconsole.log("--- Services Registered ---");\nconsole.log(Array.from(InjectableServiceRegistry.keys()));\n\nconst userServiceConstructor = InjectableServiceRegistry.get("UserService");\nif (userServiceConstructor) {\n const userServiceInstance = new userServiceConstructor();\n console.log("Users:", userServiceInstance.getUsers());\n // console.log("User Service Created At:", userServiceInstance.createdAt); // หากใช้คลาสที่คืนค่ากลับมา\n}
ตัวอย่างนี้แสดงให้เห็นว่า class decorator สามารถลงทะเบียนคลาสและแม้กระทั่งแก้ไข constructor ของมันได้ Injectable
decorator ทำให้คลาสสามารถถูกค้นพบได้โดยระบบ dependency injection ตามทฤษฎี
2. Method Decorators
Method decorators ถูกนำไปใช้กับการประกาศเมธอด มันจะได้รับอาร์กิวเมนต์สามตัว: target object (สำหรับสมาชิก static จะเป็นฟังก์ชัน constructor; สำหรับสมาชิก instance จะเป็น prototype ของคลาส), ชื่อของเมธอด และ property descriptor ของเมธอด
Signature:
function MethodDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
Return Value:
Method decorator สามารถคืนค่า PropertyDescriptor
ใหม่ได้ หากทำเช่นนั้น descriptor นี้จะถูกใช้เพื่อกำหนดเมธอด ซึ่งช่วยให้คุณสามารถแก้ไขหรือแทนที่การทำงานของเมธอดเดิมได้ ทำให้มีประสิทธิภาพอย่างยิ่งสำหรับ AOP
Use Cases:
- การบันทึกการเรียกเมธอดและอาร์กิวเมนต์/ผลลัพธ์ของมัน
- การแคชผลลัพธ์ของเมธอดเพื่อปรับปรุงประสิทธิภาพ
- การใช้การตรวจสอบสิทธิ์ก่อนการทำงานของเมธอด
- การวัดเวลาการทำงานของเมธอด
- การทำ Debouncing หรือ throttling การเรียกเมธอด
ตัวอย่าง Method Decorator: การตรวจสอบประสิทธิภาพ
ลองสร้าง MeasurePerformance
decorator เพื่อบันทึกเวลาการทำงานของเมธอด
function MeasurePerformance(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalMethod = descriptor.value;\n\n descriptor.value = function(...args: any[]) {\n const start = process.hrtime.bigint();\n const result = originalMethod.apply(this, args);\n const end = process.hrtime.bigint();\n const duration = Number(end - start) / 1_000_000;\n console.log(`Method "${propertyKey}" executed in ${duration.toFixed(2)} ms`);\n return result;\n };\n\n return descriptor;\n}\n\nclass DataProcessor {\n @MeasurePerformance\n processData(data: number[]): number[] {\n // จำลองการทำงานที่ซับซ้อนและใช้เวลานาน\n for (let i = 0; i < 1_000_000; i++) {\n Math.sin(i);\n }\n return data.map(n => n * 2);\n }\n\n @MeasurePerformance\n fetchRemoteData(id: string): Promise {\n return new Promise(resolve => {\n setTimeout(() => {\n resolve(`Data for ID: ${id}`);\n }, 500);\n });\n }\n}\n\nconst processor = new DataProcessor();\nprocessor.processData([1, 2, 3]);\nprocessor.fetchRemoteData("abc").then(result => console.log(result));
MeasurePerformance
decorator จะห่อหุ้มเมธอดเดิมด้วยตรรกะการจับเวลา โดยพิมพ์ระยะเวลาการทำงานออกมาโดยไม่ทำให้ตรรกะทางธุรกิจภายในเมธอดรก นี่เป็นตัวอย่างคลาสสิกของ Aspect-Oriented Programming (AOP)
3. Accessor Decorators
Accessor decorators ถูกนำไปใช้กับการประกาศ accessor (get
และ set
) คล้ายกับ method decorators มันจะได้รับ target object, ชื่อของ accessor และ property descriptor ของมัน
Signature:
function AccessorDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
Return Value:
Accessor decorator สามารถคืนค่า PropertyDescriptor
ใหม่ ซึ่งจะถูกใช้เพื่อกำหนด accessor
Use Cases:
- การตรวจสอบความถูกต้องเมื่อตั้งค่า property
- การแปลงค่าก่อนที่จะถูกตั้งค่าหรือหลังจากที่ถูกดึงข้อมูล
- การควบคุมสิทธิ์การเข้าถึงสำหรับ properties
ตัวอย่าง Accessor Decorator: การแคช Getters
ลองสร้าง decorator ที่แคชผลลัพธ์ของการคำนวณ getter ที่มีค่าใช้จ่ายสูง
function CachedGetter(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalGetter = descriptor.get;\n const cacheKey = `_cached_${String(propertyKey)}`;\n\n if (originalGetter) {\n descriptor.get = function() {\n if (this[cacheKey] === undefined) {\n console.log(`[Cache Miss] Computing value for ${String(propertyKey)}`);\n this[cacheKey] = originalGetter.apply(this);\n } else {\n console.log(`[Cache Hit] Using cached value for ${String(propertyKey)}`);\n }\n return this[cacheKey];\n };\n }\n return descriptor;\n}\n\nclass ReportGenerator {\n private data: number[];\n\n constructor(data: number[]) {\n this.data = data;\n }\n\n // จำลองการคำนวณที่มีค่าใช้จ่ายสูง\n @CachedGetter\n get expensiveSummary(): number {\n console.log("Performing expensive summary calculation...");\n return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;\n }\n}\n\nconst generator = new ReportGenerator([10, 20, 30, 40, 50]);\n\nconsole.log("First access:", generator.expensiveSummary);\nconsole.log("Second access:", generator.expensiveSummary);\nconsole.log("Third access:", generator.expensiveSummary);
decorator นี้ทำให้แน่ใจว่าการคำนวณของ getter expensiveSummary
จะทำงานเพียงครั้งเดียว การเรียกครั้งต่อๆ ไปจะคืนค่าที่แคชไว้ รูปแบบนี้มีประโยชน์มากสำหรับการเพิ่มประสิทธิภาพในกรณีที่การเข้าถึง property เกี่ยวข้องกับการคำนวณที่หนักหน่วงหรือการเรียกจากภายนอก
4. Property Decorators
Property decorators ถูกนำไปใช้กับการประกาศ property มันจะได้รับอาร์กิวเมนต์สองตัว: target object (สำหรับสมาชิก static จะเป็นฟังก์ชัน constructor; สำหรับสมาชิก instance จะเป็น prototype ของคลาส) และชื่อของ property
Signature:
function PropertyDecorator(target: Object, propertyKey: string | symbol) { ... }
Return Value:
Property decorators ไม่สามารถคืนค่าใดๆ ได้ การใช้งานหลักของมันคือการลงทะเบียนเมทาดาทาเกี่ยวกับ property มันไม่สามารถเปลี่ยนแปลงค่าของ property หรือ descriptor ของมันโดยตรงในขณะที่ตกแต่งได้ เนื่องจาก descriptor สำหรับ property ยังไม่ถูกกำหนดอย่างสมบูรณ์เมื่อ property decorators ทำงาน
Use Cases:
- การลงทะเบียน properties สำหรับ serialization/deserialization
- การใช้กฎการตรวจสอบความถูกต้องกับ properties
- การตั้งค่าเริ่มต้นหรือการกำหนดค่าสำหรับ properties
- การแมปคอลัมน์ ORM (Object-Relational Mapping) (เช่น
@Column()
ใน TypeORM)
ตัวอย่าง Property Decorator: การตรวจสอบฟิลด์ที่จำเป็น
ลองสร้าง decorator เพื่อทำเครื่องหมาย property ว่า "จำเป็น" แล้วตรวจสอบในขณะรันไทม์
interface ValidationRule {\n property: string | symbol;\n validate: (value: any) => boolean;\n message: string;\n}\n\nconst validationRules: Map = new Map();\n\nfunction Required(target: Object, propertyKey: string | symbol) {\n const rules = validationRules.get(target.constructor) || [];\n rules.push({\n property: propertyKey,\n validate: (value: any) => value !== null && value !== undefined && value !== "",\n message: `${String(propertyKey)} is required.`\n });\n validationRules.set(target.constructor, rules);\n}\n\nfunction validate(instance: any): string[] {\n const classRules = validationRules.get(instance.constructor) || [];\n const errors: string[] = [];\n\n for (const rule of classRules) {\n if (!rule.validate(instance[rule.property])) {\n errors.push(rule.message);\n }\n }\n return errors;\n}\n\nclass UserProfile {\n @Required\n firstName: string;\n\n @Required\n lastName: string;\n\n age?: number;\n\n constructor(firstName: string, lastName: string, age?: number) {\n this.firstName = firstName;\n this.lastName = lastName;\n this.age = age;\n }\n}\n\nconst user1 = new UserProfile("John", "Doe", 30);\nconsole.log("User 1 validation errors:", validate(user1)); // []\n\nconst user2 = new UserProfile("", "Smith");\nconsole.log("User 2 validation errors:", validate(user2)); // ["firstName is required."]\n\nconst user3 = new UserProfile("Alice", "");\nconsole.log("User 3 validation errors:", validate(user3)); // ["lastName is required."]
Required
decorator เพียงแค่ลงทะเบียนกฎการตรวจสอบกับ map กลาง validationRules
จากนั้นฟังก์ชัน validate
ที่แยกต่างหากจะใช้เมทาดาทานี้เพื่อตรวจสอบ instance ในขณะรันไทม์ รูปแบบนี้แยกตรรกะการตรวจสอบออกจากนิยามข้อมูล ทำให้สามารถนำกลับมาใช้ใหม่ได้และสะอาด
5. Parameter Decorators
Parameter decorators ถูกนำไปใช้กับพารามิเตอร์ภายใน constructor ของคลาสหรือเมธอด มันจะได้รับอาร์กิวเมนต์สามตัว: target object (สำหรับสมาชิก static จะเป็นฟังก์ชัน constructor; สำหรับสมาชิก instance จะเป็น prototype ของคลาส), ชื่อของเมธอด (หรือ undefined
สำหรับพารามิเตอร์ของ constructor) และดัชนีลำดับของพารามิเตอร์ในรายการพารามิเตอร์ของฟังก์ชัน
Signature:
function ParameterDecorator(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) { ... }
Return Value:
Parameter decorators ไม่สามารถคืนค่าใดๆ ได้ เช่นเดียวกับ property decorators บทบาทหลักของมันคือการเพิ่มเมทาดาทาเกี่ยวกับพารามิเตอร์
Use Cases:
- การลงทะเบียนประเภทพารามิเตอร์สำหรับ dependency injection (เช่น
@Inject()
ใน Angular) - การใช้การตรวจสอบหรือการแปลงกับพารามิเตอร์เฉพาะ
- การดึงเมทาดาทาเกี่ยวกับพารามิเตอร์ของคำขอ API ในเว็บเฟรมเวิร์ก
ตัวอย่าง Parameter Decorator: การฉีดข้อมูลคำขอ
ลองจำลองว่าเว็บเฟรมเวิร์กอาจใช้ parameter decorators เพื่อฉีดข้อมูลเฉพาะเข้าไปในพารามิเตอร์ของเมธอด เช่น ID ผู้ใช้จากคำขอ
interface ParameterMetadata {\n index: number;\n key: string | symbol;\n resolver: (request: any) => any;\n}\n\nconst parameterResolvers: Map> = new Map();\n\nfunction RequestParam(paramName: string) {\n return function (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) {\n const targetKey = propertyKey || "constructor";\n let methodResolvers = parameterResolvers.get(target.constructor);\n if (!methodResolvers) {\n methodResolvers = new Map();\n parameterResolvers.set(target.constructor, methodResolvers);\n }\n const paramMetadata = methodResolvers.get(targetKey) || [];\n paramMetadata.push({\n index: parameterIndex,\n key: targetKey,\n resolver: (request: any) => request[paramName]\n });\n methodResolvers.set(targetKey, paramMetadata);\n };\n}\n\n// ฟังก์ชันสมมติของเฟรมเวิร์กเพื่อเรียกเมธอดพร้อมพารามิเตอร์ที่แก้ไขแล้ว\nfunction executeWithParams(instance: any, methodName: string, request: any) {\n const classResolvers = parameterResolvers.get(instance.constructor);\n if (!classResolvers) {\n return (instance[methodName] as Function).apply(instance, []);\n }\n const methodParamMetadata = classResolvers.get(methodName);\n if (!methodParamMetadata) {\n return (instance[methodName] as Function).apply(instance, []);\n }\n\n const args: any[] = Array(methodParamMetadata.length);\n for (const meta of methodParamMetadata) {\n args[meta.index] = meta.resolver(request);\n }\n return (instance[methodName] as Function).apply(instance, args);\n}\n\nclass UserController {\n getUser(@RequestParam("id") userId: string, @RequestParam("token") authToken?: string) {\n console.log(`Fetching user with ID: ${userId}, Token: ${authToken || "N/A"}`);\n return { id: userId, name: "Jane Doe" };\n }\n\n deleteUser(@RequestParam("id") userId: string) {\n console.log(`Deleting user with ID: ${userId}`);\n return { status: "deleted", id: userId };\n }\n}\n\nconst userController = new UserController();\n\n// จำลองคำขอที่เข้ามา\nconst mockRequest = {\n id: "user123",\n token: "abc-123",\n someOtherProp: "xyz"\n};\n\nconsole.log("\n--- Executing getUser ---");\nexecuteWithParams(userController, "getUser", mockRequest);\n\nconsole.log("\n--- Executing deleteUser ---");\nexecuteWithParams(userController, "deleteUser", { id: "user456" });
ตัวอย่างนี้แสดงให้เห็นว่า parameter decorators สามารถรวบรวมข้อมูลเกี่ยวกับพารามิเตอร์ที่จำเป็นของเมธอดได้อย่างไร จากนั้นเฟรมเวิร์กสามารถใช้เมทาดาทาที่รวบรวมมานี้เพื่อแก้ไขและฉีดค่าที่เหมาะสมโดยอัตโนมัติเมื่อเมธอดถูกเรียก ซึ่งช่วยลดความซับซ้อนของตรรกะของ controller หรือ service ได้อย่างมาก
การประสมและการจัดลำดับการทำงานของ Decorator
Decorators สามารถนำมาใช้ร่วมกันได้หลายรูปแบบ และการทำความเข้าใจลำดับการทำงานของมันเป็นสิ่งสำคัญในการคาดการณ์พฤติกรรมและหลีกเลี่ยงปัญหาที่ไม่คาดคิด
Decorator หลายตัวบนเป้าหมายเดียว
เมื่อมี decorator หลายตัวถูกนำไปใช้กับการประกาศเดียว (เช่น คลาส, เมธอด หรือ property) มันจะทำงานตามลำดับที่เฉพาะเจาะจง: จากล่างขึ้นบน หรือจากขวาไปซ้าย สำหรับการประเมินค่าของมัน อย่างไรก็ตาม ผลลัพธ์ของมันจะถูกนำไปใช้ในลำดับที่ตรงกันข้าม
@DecoratorA\n@DecoratorB\nclass MyClass {\n // ...\n}\n
ในที่นี้ DecoratorB
จะถูกประเมินค่าก่อน แล้วจึงเป็น DecoratorA
หากมันแก้ไขคลาส (เช่น โดยการคืนค่า constructor ใหม่) การแก้ไขจาก DecoratorA
จะห่อหุ้มหรือทับซ้อนการแก้ไขจาก DecoratorB
ตัวอย่าง: การเชื่อมโยง Method Decorators
พิจารณา method decorators สองตัว: LogCall
และ Authorization
function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalMethod = descriptor.value;\n descriptor.value = function (...args: any[]) {\n console.log(`[LOG] Calling ${String(propertyKey)} with args:`, args);\n const result = originalMethod.apply(this, args);\n console.log(`[LOG] Method ${String(propertyKey)} returned:`, result);\n return result;\n };\n return descriptor;\n}\n\nfunction Authorization(roles: string[]) {\n return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalMethod = descriptor.value;\n descriptor.value = function (...args: any[]) {\n const currentUserRoles = ["admin"]; // จำลองการดึงบทบาทของผู้ใช้ปัจจุบัน\n const authorized = roles.some(role => currentUserRoles.includes(role));\n if (!authorized) {\n console.warn(`[AUTH] Access denied for ${String(propertyKey)}. Required roles: ${roles.join(", ")}`);\n throw new Error("Unauthorized access");\n }\n console.log(`[AUTH] Access granted for ${String(propertyKey)}`);\n return originalMethod.apply(this, args);\n };\n return descriptor;\n };\n}\n\nclass SecureService {\n @LogCall\n @Authorization(["admin"])\n deleteSensitiveData(id: string) {\n console.log(`Deleting sensitive data for ID: ${id}`);\n return `Data ID ${id} deleted.`;\n }\n\n @Authorization(["user"])\n @LogCall // เปลี่ยนลำดับที่นี่\n fetchPublicData(query: string) {\n console.log(`Fetching public data with query: ${query}`);\n return `Public data for query: ${query}`; \n }\n}\n\nconst service = new SecureService();\n\ntry {\n console.log("\n--- Calling deleteSensitiveData (Admin User) ---");\n service.deleteSensitiveData("record123");\n} catch (error: any) {\n console.error(error.message);\n}\n\ntry {\n console.log("\n--- Calling fetchPublicData (Non-Admin User) ---");\n // จำลองผู้ใช้ที่ไม่ใช่ admin พยายามเข้าถึง fetchPublicData ซึ่งต้องการบทบาท 'user'\n const mockUserRoles = ["guest"]; // นี่จะทำให้การตรวจสอบสิทธิ์ล้มเหลว\n // เพื่อให้เป็นไดนามิก คุณจะต้องมีระบบ DI หรือบริบทแบบสแตติกสำหรับบทบาทผู้ใช้ปัจจุบัน\n // เพื่อความง่าย เราจะสมมติว่า Authorization decorator สามารถเข้าถึงบริบทผู้ใช้ปัจจุบันได้\n // เรามาปรับ Authorization decorator ให้ถือว่าเป็น 'admin' เสมอเพื่อการสาธิต, \n // เพื่อให้การเรียกครั้งแรกสำเร็จและการเรียกครั้งที่สองล้มเหลวเพื่อแสดงเส้นทางที่แตกต่างกัน\n \n // รันอีกครั้งด้วยบทบาทผู้ใช้เพื่อให้ fetchPublicData สำเร็จ\n // ลองจินตนาการว่า currentUserRoles ใน Authorization กลายเป็น: ['user']\n // สำหรับตัวอย่างนี้ ให้เรียบง่ายและแสดงผลกระทบของลำดับ\n service.fetchPublicData("search term"); // นี่จะทำงาน Auth -> Log\n} catch (error: any) {\n console.error(error.message);\n}\n\n/* ผลลัพธ์ที่คาดหวังสำหรับ deleteSensitiveData:\n[AUTH] Access granted for deleteSensitiveData\n[LOG] Calling deleteSensitiveData with args: [ 'record123' ]\nDeleting sensitive data for ID: record123\n[LOG] Method deleteSensitiveData returned: Data ID record123 deleted.\n*/\n\n/* ผลลัพธ์ที่คาดหวังสำหรับ fetchPublicData (หากผู้ใช้มีบทบาท 'user'):\n[LOG] Calling fetchPublicData with args: [ 'search term' ]\n[AUTH] Access granted for fetchPublicData\nFetching public data with query: search term\n[LOG] Method fetchPublicData returned: Public data for query: search term\n*/
สังเกตลำดับ: สำหรับ deleteSensitiveData
, Authorization
(ด้านล่าง) ทำงานก่อน จากนั้น LogCall
(ด้านบน) จะห่อหุ้มมัน ตรรกะภายในของ Authorization
ทำงานก่อน สำหรับ fetchPublicData
, LogCall
(ด้านล่าง) ทำงานก่อน จากนั้น Authorization
(ด้านบน) จะห่อหุ้มมัน ซึ่งหมายความว่า aspect ของ LogCall
จะอยู่นอก aspect ของ Authorization
ความแตกต่างนี้สำคัญอย่างยิ่งสำหรับ cross-cutting concerns เช่น การบันทึกหรือการจัดการข้อผิดพลาด ซึ่งลำดับการทำงานสามารถส่งผลกระทบต่อพฤติกรรมได้อย่างมีนัยสำคัญ
ลำดับการทำงานสำหรับเป้าหมายที่แตกต่างกัน
เมื่อคลาส, สมาชิกของมัน และพารามิเตอร์ทั้งหมดมี decorators ลำดับการทำงานจะถูกกำหนดไว้อย่างชัดเจน:
- Parameter Decorators จะถูกนำไปใช้ก่อน สำหรับแต่ละพารามิเตอร์ โดยเริ่มจากพารามิเตอร์สุดท้ายไปจนถึงตัวแรก
- จากนั้น Method, Accessor, หรือ Property Decorators จะถูกนำไปใช้สำหรับแต่ละสมาชิก
- สุดท้าย Class Decorators จะถูกนำไปใช้กับคลาสเอง
ภายในแต่ละหมวดหมู่ decorator หลายตัวบนเป้าหมายเดียวกันจะถูกนำไปใช้จากล่างขึ้นบน (หรือจากขวาไปซ้าย)
ตัวอย่าง: ลำดับการทำงานแบบเต็ม
function log(message: string) {\n return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {\n if (typeof descriptorOrIndex === 'number') {\n console.log(`Param Decorator: ${message} on parameter #${descriptorOrIndex} of ${String(propertyKey || "constructor")}`);\n } else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {\n if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {\n console.log(`Method/Accessor Decorator: ${message} on ${String(propertyKey)}`);\n } else {\n console.log(`Property Decorator: ${message} on ${String(propertyKey)}`);\n }\n } else {\n console.log(`Class Decorator: ${message} on ${target.name}`);\n }\n return descriptorOrIndex; // คืนค่า descriptor สำหรับ method/accessor, undefined สำหรับอื่นๆ\n };\n}\n\n@log("Class Level D")\n@log("Class Level C")\nclass MyDecoratedClass {\n @log("Static Property A")\n static staticProp: string = "";\n\n @log("Instance Property B")\n instanceProp: number = 0;\n\n @log("Method D")\n @log("Method C")\n myMethod(\n @log("Parameter Z") paramZ: string,\n @log("Parameter Y") paramY: number\n ) {\n console.log("Method myMethod executed.");\n }\n\n @log("Getter/Setter F")\n get myAccessor() {\n return "";\n }\n\n set myAccessor(value: string) {\n //...\n }\n\n constructor() {\n console.log("Constructor executed.");\n }\n}\n\nnew MyDecoratedClass();\n// เรียกเมธอดเพื่อทริกเกอร์ method decorator\nnew MyDecoratedClass().myMethod("hello", 123);\n\n/* ลำดับผลลัพธ์ที่คาดการณ์ (โดยประมาณ ขึ้นอยู่กับเวอร์ชัน TypeScript และการคอมไพล์ที่เฉพาะเจาะจง):\nParam Decorator: Parameter Y on parameter #1 of myMethod\nParam Decorator: Parameter Z on parameter #0 of myMethod\nProperty Decorator: Static Property A on staticProp\nProperty Decorator: Instance Property B on instanceProp\nMethod/Accessor Decorator: Getter/Setter F on myAccessor\nMethod/Accessor Decorator: Method C on myMethod\nMethod/Accessor Decorator: Method D on myMethod\nClass Decorator: Class Level C on MyDecoratedClass\nClass Decorator: Class Level D on MyDecoratedClass\nConstructor executed.\nMethod myMethod executed.\n*/
เวลาที่ console log แสดงผลอาจแตกต่างกันเล็กน้อยขึ้นอยู่กับว่า constructor หรือเมธอดถูกเรียกเมื่อใด แต่ลำดับที่ฟังก์ชัน decorator เองถูกดำเนินการ (และดังนั้นผลข้างเคียงหรือค่าที่คืนกลับมาถูกนำไปใช้) จะเป็นไปตามกฎข้างต้น
การประยุกต์ใช้จริงและรูปแบบการออกแบบด้วย Decorators
Decorators โดยเฉพาะอย่างยิ่งเมื่อใช้ร่วมกับ polyfill reflect-metadata
จะเปิดโลกใหม่ของการเขียนโปรแกรมที่ขับเคลื่อนด้วยเมทาดาทา ซึ่งช่วยให้เกิดรูปแบบการออกแบบที่ทรงพลังที่สามารถลด boilerplate และ cross-cutting concerns ได้
1. Dependency Injection (DI)
หนึ่งในการใช้งานที่โดดเด่นที่สุดของ decorators คือในเฟรมเวิร์ก Dependency Injection (เช่น @Injectable()
, @Component()
ของ Angular หรือการใช้ DI อย่างกว้างขวางของ NestJS) Decorators ช่วยให้คุณสามารถประกาศ dependencies ได้โดยตรงบน constructors หรือ properties ทำให้เฟรมเวิร์กสามารถสร้างและจัดหา services ที่ถูกต้องได้โดยอัตโนมัติ
ตัวอย่าง: การฉีด Service แบบง่าย
import "reflect-metadata"; // จำเป็นสำหรับ emitDecoratorMetadata\n\nconst INJECTABLE_METADATA_KEY = Symbol("injectable");\nconst INJECT_METADATA_KEY = Symbol("inject");\n\nfunction Injectable() {\n return function (target: Function) {\n Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);\n };\n}\n\nfunction Inject(token: any) {\n return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {\n const existingInjections: any[] = Reflect.getOwnMetadata(INJECT_METADATA_KEY, target, propertyKey) || [];\n existingInjections[parameterIndex] = token;\n Reflect.defineMetadata(INJECT_METADATA_KEY, existingInjections, target, propertyKey);\n };\n}\n\nclass Container {\n private static instances = new Map();\n\n static resolve(target: { new (...args: any[]): T }): T {\n if (Container.instances.has(target)) {\n return Container.instances.get(target);\n }\n\n const isInjectable = Reflect.getMetadata(INJECTABLE_METADATA_KEY, target);\n if (!isInjectable) {\n throw new Error(`Class ${target.name} is not marked as @Injectable.`);\n }\n\n // รับประเภทพารามิเตอร์ของ constructor (ต้องการ emitDecoratorMetadata)\n const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", target) || [];\n const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];\n\n const dependencies = paramTypes.map((paramType, index) => {\n // ใช้ token จาก @Inject ที่ระบุไว้ หากไม่มีให้ใช้ประเภทที่อนุมานได้\n const token = explicitInjections[index] || paramType;\n if (token === undefined) {\n throw new Error(`Cannot resolve parameter at index ${index} for ${target.name}. It might be a circular dependency or primitive type without explicit @Inject.`);\n }\n return Container.resolve(token);\n });\n\n const instance = new target(...dependencies);\n Container.instances.set(target, instance);\n return instance;\n }\n}\n\n// กำหนด services\n@Injectable()\nclass DatabaseService {\n connect() {\n console.log("Connecting to database...");\n return "DB Connection";\n }\n}\n\n@Injectable()\nclass AuthService {\n private db: DatabaseService;\n\n constructor(db: DatabaseService) {\n this.db = db;\n }\n\n login() {\n console.log(`AuthService: Authenticating using ${this.db.connect()}`);\n return "User logged in";\n }\n}\n\n@Injectable()\nclass UserService {\n private authService: AuthService;\n private dbService: DatabaseService; // ตัวอย่างการฉีดผ่าน property โดยใช้ decorator ที่กำหนดเองหรือคุณสมบัติของเฟรมเวิร์ก\n\n constructor(@Inject(AuthService) authService: AuthService,\n @Inject(DatabaseService) dbService: DatabaseService) {\n this.authService = authService;\n this.dbService = dbService;\n }\n\n getUserProfile() {\n this.authService.login();\n this.dbService.connect();\n console.log("UserService: Fetching user profile...");\n return { id: 1, name: "Global User" };\n }\n}\n\n// แก้ไข (Resolve) service หลัก\nconsole.log("--- Resolving UserService ---");\nconst userService = Container.resolve(UserService);\nconsole.log(userService.getUserProfile());\n\nconsole.log("\n--- Resolving AuthService (should be cached) ---");\nconst authService = Container.resolve(AuthService);\nauthService.login();
ตัวอย่างที่ซับซ้อนนี้แสดงให้เห็นว่า @Injectable
และ @Inject
decorators เมื่อรวมกับ reflect-metadata
ช่วยให้ Container
ที่กำหนดเองสามารถแก้ไขและจัดหา dependencies ได้โดยอัตโนมัติ เมทาดาทา design:paramtypes
ที่ TypeScript ปล่อยออกมาโดยอัตโนมัติ (เมื่อ emitDecoratorMetadata
เป็น true) มีความสำคัญอย่างยิ่งในที่นี้
2. Aspect-Oriented Programming (AOP)
AOP มุ่งเน้นไปที่การจัดระเบียบ cross-cutting concerns (เช่น การบันทึก, ความปลอดภัย, ธุรกรรม) ที่ตัดผ่านหลายคลาสและโมดูล Decorators เหมาะอย่างยิ่งสำหรับการนำแนวคิด AOP ไปใช้ใน TypeScript
ตัวอย่าง: การบันทึกด้วย Method Decorator
กลับมาที่ LogCall
decorator อีกครั้ง มันเป็นตัวอย่างที่สมบูรณ์แบบของ AOP มันเพิ่มพฤติกรรมการบันทึกให้กับเมธอดใดๆ โดยไม่ต้องแก้ไขโค้ดดั้งเดิมของเมธอด ซึ่งเป็นการแยก "สิ่งที่ต้องทำ" (ตรรกะทางธุรกิจ) ออกจาก "วิธีการทำ" (การบันทึก, การตรวจสอบประสิทธิภาพ เป็นต้น)
function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalMethod = descriptor.value;\n descriptor.value = function (...args: any[]) {\n console.log(`[LOG AOP] Entering method: ${String(propertyKey)} with args:`, args);\n try {\n const result = originalMethod.apply(this, args);\n console.log(`[LOG AOP] Exiting method: ${String(propertyKey)} with result:`, result);\n return result;\n } catch (error: any) {\n console.error(`[LOG AOP] Error in method ${String(propertyKey)}:`, error.message);\n throw error;\n }\n };\n return descriptor;\n}\n\nclass PaymentProcessor {\n @LogMethod\n processPayment(amount: number, currency: string) {\n if (amount <= 0) {\n throw new Error("Payment amount must be positive.");\n }\n console.log(`Processing payment of ${amount} ${currency}...`);\n return `Payment of ${amount} ${currency} processed successfully.`;\n }\n\n @LogMethod\n refundPayment(transactionId: string) {\n console.log(`Refunding payment for transaction ID: ${transactionId}...`);\n return `Refund initiated for ${transactionId}.`;\n }\n}\n\nconst processor = new PaymentProcessor();\nprocessor.processPayment(100, "USD");\ntry {\n processor.processPayment(-50, "EUR");\n} catch (error: any) {\n console.error("Caught error:", error.message);\n}
แนวทางนี้ทำให้คลาส PaymentProcessor
มุ่งเน้นไปที่ตรรกะการชำระเงินเพียงอย่างเดียว ในขณะที่ LogMethod
decorator จัดการกับ cross-cutting concern ของการบันทึก
3. การตรวจสอบความถูกต้องและการแปลงข้อมูล
Decorators มีประโยชน์อย่างเหลือเชื่อสำหรับการกำหนดกฎการตรวจสอบความถูกต้องโดยตรงบน properties หรือสำหรับการแปลงข้อมูลระหว่าง serialization/deserialization
ตัวอย่าง: การตรวจสอบข้อมูลด้วย Property Decorators
ตัวอย่าง @Required
ก่อนหน้านี้ได้แสดงให้เห็นแล้ว นี่คืออีกตัวอย่างหนึ่งที่มีการตรวจสอบช่วงตัวเลข
interface FieldValidationRule {\n property: string | symbol;\n validator: (value: any) => boolean;\n message: string;\n}\n\nconst fieldValidationRules = new Map();\n\nfunction addValidationRule(target: Object, propertyKey: string | symbol, validator: (value: any) => boolean, message: string) {\n const rules = fieldValidationRules.get(target.constructor) || [];\n rules.push({ property: propertyKey, validator, message });\n fieldValidationRules.set(target.constructor, rules);\n}\n\nfunction IsPositive(target: Object, propertyKey: string | symbol) {\n addValidationRule(target, propertyKey, (value: number) => value > 0, `${String(propertyKey)} must be a positive number.`);\n}\n\nfunction MaxLength(maxLength: number) {\n return function (target: Object, propertyKey: string | symbol) {\n addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} must be at most ${maxLength} characters long.`);\n };\n}\n\nclass Product {\n @MaxLength(50)\n name: string;\n\n @IsPositive\n price: number;\n\n constructor(name: string, price: number) {\n this.name = name;\n this.price = price;\n }\n\n static validate(instance: any): string[] {\n const errors: string[] = [];\n const rules = fieldValidationRules.get(instance.constructor) || [];\n for (const rule of rules) {\n if (!rule.validator(instance[rule.property])) {\n errors.push(rule.message);\n }\n }\n return errors;\n }\n}\n\nconst product1 = new Product("Laptop", 1200);\nconsole.log("Product 1 errors:", Product.validate(product1)); // []\n\nconst product2 = new Product("Very long product name that exceeds fifty characters limit for testing purpose", 50);\nconsole.log("Product 2 errors:", Product.validate(product2)); // ["name must be at most 50 characters long."]\n\nconst product3 = new Product("Book", -10);\nconsole.log("Product 3 errors:", Product.validate(product3)); // ["price must be a positive number."]
การตั้งค่านี้ช่วยให้คุณสามารถกำหนดกฎการตรวจสอบความถูกต้องบน properties ของโมเดลของคุณในรูปแบบ declarative ทำให้โมเดลข้อมูลของคุณสามารถอธิบายข้อจำกัดของตัวเองได้
แนวทางปฏิบัติที่ดีที่สุดและข้อควรพิจารณา
แม้ว่า decorators จะทรงพลัง แต่ก็ควรใช้อย่างรอบคอบ การใช้ในทางที่ผิดอาจนำไปสู่โค้ดที่ยากต่อการดีบักหรือทำความเข้าใจ
เมื่อใดควรใช้ Decorators (และเมื่อใดไม่ควร)
- ใช้สำหรับ:
- Cross-cutting concerns: การบันทึก, การแคช, การอนุญาต, การจัดการธุรกรรม
- การประกาศเมทาดาทา: การกำหนดสคีมาสำหรับ ORMs, กฎการตรวจสอบความถูกต้อง, การกำหนดค่า DI
- การรวมเข้ากับเฟรมเวิร์ก: เมื่อสร้างหรือใช้เฟรมเวิร์กที่ใช้ประโยชน์จากเมทาดาทา
- การลด boilerplate: การสรุปรูปแบบโค้ดที่ซ้ำซ้อน
- หลีกเลี่ยงสำหรับ:
- การเรียกฟังก์ชันอย่างง่าย: หากการเรียกฟังก์ชันธรรมดาสามารถให้ผลลัพธ์เดียวกันได้อย่างชัดเจน ให้เลือกใช้วิธีนั้น
- ตรรกะทางธุรกิจ: Decorators ควรเสริม ไม่ใช่กำหนด ตรรกะทางธุรกิจหลัก
- ความซับซ้อนเกินไป: หากการใช้ decorator ทำให้โค้ดอ่านยากขึ้นหรือทดสอบยากขึ้น ให้พิจารณาใหม่
ผลกระทบต่อประสิทธิภาพ
Decorators ทำงานในเวลาคอมไพล์ (หรือเวลาที่กำหนดใน JavaScript runtime หากถูกแปลง) การแปลงหรือการรวบรวมเมทาดาทาเกิดขึ้นเมื่อคลาส/เมธอดถูกกำหนด ไม่ใช่ทุกครั้งที่เรียก ดังนั้น ผลกระทบต่อประสิทธิภาพในรันไทม์ของ *การใช้* decorators จึงมีน้อยมาก อย่างไรก็ตาม *ตรรกะภายใน* decorators ของคุณอาจมีผลกระทบต่อประสิทธิภาพได้ โดยเฉพาะอย่างยิ่งหากมันทำการทำงานที่มีค่าใช้จ่ายสูงทุกครั้งที่เรียกเมธอด (เช่น การคำนวณที่ซับซ้อนภายใน method decorator)
การบำรุงรักษาและความสามารถในการอ่าน
Decorators เมื่อใช้อย่างถูกต้อง สามารถปรับปรุงความสามารถในการอ่านได้อย่างมากโดยการย้ายโค้ด boilerplate ออกจากตรรกะหลัก อย่างไรก็ตาม หากมันทำการแปลงที่ซับซ้อนและซ่อนเร้น การดีบักอาจกลายเป็นเรื่องท้าทาย ตรวจสอบให้แน่ใจว่า decorators ของคุณมีเอกสารประกอบที่ดีและพฤติกรรมของมันสามารถคาดเดาได้
สถานะการทดลองและอนาคตของ Decorators
สิ่งสำคัญที่ต้องย้ำคือ TypeScript decorators นั้นอิงตามข้อเสนอ Stage 3 TC39 ซึ่งหมายความว่าข้อกำหนดนั้นมีความเสถียรเป็นส่วนใหญ่ แต่อาจมีการเปลี่ยนแปลงเล็กน้อยก่อนที่จะเป็นส่วนหนึ่งของมาตรฐาน ECMAScript อย่างเป็นทางการ เฟรมเวิร์กอย่าง Angular ได้นำไปใช้แล้ว โดยคาดการณ์ว่าในที่สุดมันจะกลายเป็นมาตรฐาน ซึ่งหมายถึงความเสี่ยงในระดับหนึ่ง แม้ว่าด้วยการยอมรับอย่างกว้างขวาง การเปลี่ยนแปลงที่ส่งผลกระทบอย่างมีนัยสำคัญจึงไม่น่าจะเกิดขึ้น
ข้อเสนอ TC39 ได้มีการพัฒนา การใช้งานปัจจุบันของ TypeScript อิงตามเวอร์ชันเก่าของข้อเสนอ มีความแตกต่างระหว่าง "Legacy Decorators" กับ "Standard Decorators" เมื่อมาตรฐานอย่างเป็นทางการมาถึง TypeScript น่าจะอัปเดตการใช้งานของตน สำหรับนักพัฒนาส่วนใหญ่ที่ใช้เฟรมเวิร์ก การเปลี่ยนแปลงนี้จะถูกจัดการโดยเฟรมเวิร์กเอง สำหรับผู้เขียนไลบรารี การทำความเข้าใจความแตกต่างเล็กน้อยระหว่าง legacy และ standard decorators ในอนาคตอาจมีความจำเป็น
ตัวเลือกคอมไพเลอร์ emitDecoratorMetadata
ตัวเลือกนี้ เมื่อตั้งค่าเป็น true
ใน tsconfig.json
จะสั่งให้คอมไพเลอร์ TypeScript ปล่อยเมทาดาทาประเภท ณ เวลาออกแบบ (design-time type metadata) บางอย่างเข้าไปใน JavaScript ที่คอมไพล์แล้ว เมทาดาทานี้รวมถึงประเภทของพารามิเตอร์ของ constructor (design:paramtypes
), ประเภทการคืนค่าของเมธอด (design:returntype
) และประเภทของ properties (design:type
)
เมทาดาทาที่ปล่อยออกมานี้ไม่ได้เป็นส่วนหนึ่งของ JavaScript runtime มาตรฐาน โดยปกติจะถูกใช้โดย polyfill reflect-metadata
ซึ่งจะทำให้สามารถเข้าถึงได้ผ่านฟังก์ชัน Reflect.getMetadata()
สิ่งนี้มีความสำคัญอย่างยิ่งสำหรับรูปแบบขั้นสูงเช่น Dependency Injection ซึ่ง container จำเป็นต้องทราบประเภทของ dependencies ที่คลาสต้องการโดยไม่ต้องกำหนดค่าอย่างชัดเจน
รูปแบบขั้นสูงด้วย Decorators
Decorators สามารถนำมารวมและขยายเพื่อสร้างรูปแบบที่ซับซ้อนยิ่งขึ้นได้
1. การตกแต่ง Decorators (Higher-Order Decorators)
คุณสามารถสร้าง decorators ที่แก้ไขหรือประกอบ decorators อื่นๆ ได้ สิ่งนี้ไม่ค่อยพบบ่อย แต่แสดงให้เห็นถึงลักษณะการทำงานแบบฟังก์ชันของ decorators
// decorator ที่ทำให้แน่ใจว่าเมธอดถูกบันทึกและยังต้องการบทบาท admin\nfunction AdminAndLoggedMethod() {\n return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n // ใช้ Authorization ก่อน (ด้านใน)\n Authorization(["admin"])(target, propertyKey, descriptor);\n // จากนั้นใช้ LogCall (ด้านนอก)\n LogCall(target, propertyKey, descriptor);\n\n return descriptor; // คืนค่า descriptor ที่แก้ไขแล้ว\n };\n}\n\nclass AdminPanel {\n @AdminAndLoggedMethod()\n deleteUserAccount(userId: string) {\n console.log(`Deleting user account: ${userId}`);\n return `User ${userId} deleted.`;\n }\n}\n\nconst adminPanel = new AdminPanel();\nadminPanel.deleteUserAccount("user007");\n/* ผลลัพธ์ที่คาดหวัง (สมมติว่ามีบทบาท admin):\n[AUTH] Access granted for deleteUserAccount\n[LOG] Calling deleteUserAccount with args: [ 'user007' ]\nDeleting user account: user007\n[LOG] Method deleteUserAccount returned: User user007 deleted.\n*/
ในที่นี้ AdminAndLoggedMethod
เป็น factory ที่คืนค่า decorator และภายใน decorator นั้น มันจะใช้ decorators อื่นอีกสองตัว รูปแบบนี้สามารถห่อหุ้มการประกอบ decorator ที่ซับซ้อนได้
2. การใช้ Decorators สำหรับ Mixins
แม้ว่า TypeScript จะมีวิธีอื่นในการใช้งาน mixins แต่ decorators สามารถใช้เพื่อฉีดความสามารถเข้าไปในคลาสในรูปแบบ declarative ได้
function ApplyMixins(constructors: Function[]) {\n return function (derivedConstructor: Function) {\n constructors.forEach(baseConstructor => {\n Object.getOwnPropertyNames(baseConstructor.prototype).forEach(name => {\n Object.defineProperty(\n derivedConstructor.prototype,\n name,\n Object.getOwnPropertyDescriptor(baseConstructor.prototype, name) || Object.create(null)\n );\n });\n });\n };\n}\n\nclass Disposable {\n isDisposed: boolean = false;\n dispose() {\n this.isDisposed = true;\n console.log("Object disposed.");\n }\n}\n\nclass Loggable {\n log(message: string) {\n console.log(`[Loggable] ${message}`);\n }\n}\n\n@ApplyMixins([Disposable, Loggable])\nclass MyResource implements Disposable, Loggable {\n // properties/methods เหล่านี้ถูกฉีดโดย decorator\n isDisposed!: boolean;\n dispose!: () => void;\n log!: (message: string) => void;\n\n constructor(public name: string) {\n this.log(`Resource ${this.name} created.`);\n }\n\n cleanUp() {\n this.dispose();\n this.log(`Resource ${this.name} cleaned up.`);\n }\n}\n\nconst resource = new MyResource("NetworkConnection");\nconsole.log(`Is disposed: ${resource.isDisposed}`);\nresource.cleanUp();\nconsole.log(`Is disposed: ${resource.isDisposed}`);
@ApplyMixins
decorator นี้จะคัดลอกเมธอดและ properties จาก base constructors ไปยัง prototype ของคลาสที่สืบทอดมาแบบไดนามิก ซึ่งเป็นการ "ผสม" ฟังก์ชันการทำงานเข้าไปอย่างมีประสิทธิภาพ
สรุป: การเสริมพลังให้กับการพัฒนา TypeScript สมัยใหม่
TypeScript decorators เป็นคุณสมบัติที่ทรงพลังและสื่อความหมายได้ดี ซึ่งเปิดใช้งานกระบวนทัศน์ใหม่ของการเขียนโปรแกรมที่ขับเคลื่อนด้วยเมทาดาทาและเชิงลักษณะ ช่วยให้นักพัฒนาสามารถปรับปรุง, แก้ไข และเพิ่มพฤติกรรมแบบ declarative ให้กับคลาส, เมธอด, properties, accessors และพารามิเตอร์ได้โดยไม่ต้องเปลี่ยนแปลงตรรกะหลักของมัน การแยกส่วนที่เกี่ยวข้องนี้ส่งผลให้โค้ดสะอาดขึ้น, บำรุงรักษาง่ายขึ้น และสามารถนำกลับมาใช้ใหม่ได้อย่างมีประสิทธิภาพ
ตั้งแต่การทำให้ dependency injection ง่ายขึ้นและการสร้างระบบการตรวจสอบความถูกต้องที่แข็งแกร่ง ไปจนถึงการเพิ่ม cross-cutting concerns เช่น การบันทึกและการตรวจสอบประสิทธิภาพ decorators มอบโซลูชันที่สวยงามสำหรับความท้าทายในการพัฒนาทั่วไปมากมาย แม้ว่าสถานะการทดลองของมันจะทำให้ต้องตระหนัก แต่การยอมรับอย่างกว้างขวางในเฟรมเวิร์กหลักๆ ก็บ่งบอกถึงคุณค่าในทางปฏิบัติและความเกี่ยวข้องในอนาคต
ด้วยการเรียนรู้ TypeScript decorators คุณจะได้รับเครื่องมือที่สำคัญในคลังแสงของคุณ ซึ่งช่วยให้คุณสามารถสร้างแอปพลิเคชันที่แข็งแกร่ง, ขยายขนาดได้ และชาญฉลาดมากขึ้น จงนำไปใช้อย่างมีความรับผิดชอบ, ทำความเข้าใจกลไกของมัน และปลดล็อกพลังแห่ง declarative ในโปรเจกต์ TypeScript ของคุณ