สำรวจ JavaScript Decorators: เพิ่ม metadata, แปลงคลาส/เมธอด และปรับปรุงฟังก์ชันการทำงานของโค้ดของคุณด้วยวิธีที่สะอาดและเป็นแบบ declarative
JavaScript Decorators: Metadata และการแปลงข้อมูล
JavaScript Decorators เป็นฟีเจอร์ที่ได้รับแรงบันดาลใจจากภาษาอย่าง Python และ Java ซึ่งเป็นวิธีที่มีประสิทธิภาพและสื่อความหมายได้ดีในการเพิ่ม metadata และแปลงคลาส เมธอด คุณสมบัติ และพารามิเตอร์ โดยมี синтаксис (syntax) ที่สะอาดและเป็นแบบ declarative สำหรับการปรับปรุงฟังก์ชันการทำงานของโค้ดและส่งเสริมการแยกส่วนความรับผิดชอบ (separation of concerns) แม้ว่ายังเป็นส่วนเสริมที่ค่อนข้างใหม่ในระบบนิเวศของ JavaScript แต่ decorators ก็กำลังได้รับความนิยมเพิ่มขึ้น โดยเฉพาะในเฟรมเวิร์กอย่าง Angular และไลบรารีที่ใช้ metadata สำหรับ dependency injection และฟีเจอร์ขั้นสูงอื่นๆ บทความนี้จะสำรวจพื้นฐานของ JavaScript decorators การนำไปใช้ และศักยภาพในการสร้าง codebase ที่สามารถบำรุงรักษาและขยายได้ง่ายขึ้น
JavaScript Decorators คืออะไร?
โดยแก่นแท้แล้ว decorators คือการประกาศชนิดพิเศษที่สามารถแนบไปกับคลาส เมธอด accessors คุณสมบัติ หรือพารามิเตอร์ โดยใช้ синтаксис (syntax) @expression
ซึ่ง expression
จะต้องประเมินค่าเป็นฟังก์ชันที่จะถูกเรียกใช้ในขณะรันไทม์พร้อมข้อมูลเกี่ยวกับการประกาศที่ถูกตกแต่ง Decorators ทำหน้าที่เป็นฟังก์ชันที่แก้ไขหรือขยายพฤติกรรมขององค์ประกอบที่ถูกตกแต่งเป็นหลัก
ลองนึกภาพว่า decorators เป็นวิธีการห่อหุ้มหรือเสริมโค้ดที่มีอยู่โดยไม่ต้องแก้ไขโดยตรง หลักการนี้เรียกว่า Decorator pattern ในการออกแบบซอฟต์แวร์ ซึ่งช่วยให้คุณสามารถเพิ่มฟังก์ชันการทำงานให้กับอ็อบเจกต์แบบไดนามิกได้
การเปิดใช้งาน Decorators
แม้ว่า decorators จะเป็นส่วนหนึ่งของมาตรฐาน ECMAScript แต่ก็ไม่ได้ถูกเปิดใช้งานโดยค่าเริ่มต้นในสภาพแวดล้อม JavaScript ส่วนใหญ่ ในการใช้งาน คุณจะต้องตั้งค่าเครื่องมือ build ของคุณ นี่คือวิธีการเปิดใช้งาน decorators ในสภาพแวดล้อมทั่วไปบางส่วน:
- TypeScript: Decorators ได้รับการสนับสนุนโดยกำเนิดใน TypeScript ตรวจสอบให้แน่ใจว่าตัวเลือกคอมไพเลอร์
experimentalDecorators
ถูกตั้งค่าเป็นtrue
ในไฟล์tsconfig.json
ของคุณ:
{
"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
เป็นสิ่งสำคัญและควรตรงกับเวอร์ชันของ decorators proposal ที่คุณกำลังใช้งาน ควรศึกษาเอกสารของปลั๊กอิน Babel สำหรับเวอร์ชันที่แนะนำล่าสุด
ประเภทของ Decorators
Decorators มีหลายประเภท แต่ละประเภทถูกออกแบบมาสำหรับองค์ประกอบเฉพาะ:
- Class Decorators: ใช้กับคลาส
- Method Decorators: ใช้กับเมธอดภายในคลาส
- Accessor Decorators: ใช้กับ getter หรือ setter accessors
- Property Decorators: ใช้กับคุณสมบัติของคลาส
- Parameter Decorators: ใช้กับพารามิเตอร์ของเมธอดหรือ constructor
Class Decorators
Class decorators ถูกนำไปใช้กับ constructor ของคลาส และสามารถใช้เพื่อสังเกตการณ์ แก้ไข หรือแทนที่คำจำกัดความของคลาส โดยจะได้รับ 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
decorator จะป้องกันการแก้ไขเพิ่มเติมในคลาส Greeter
และ prototype ของมัน ซึ่งมีประโยชน์ในการรับประกันความไม่เปลี่ยนรูป (immutability) หรือป้องกันการเปลี่ยนแปลงโดยไม่ตั้งใจ
Method Decorators
Method decorators ถูกนำไปใช้กับเมธอดภายในคลาส โดยจะได้รับสามอาร์กิวเมนต์:
target
: Prototype ของคลาส (สำหรับเมธอดของ instance) หรือ constructor ของคลาส (สำหรับเมธอด static)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
decorator จะบันทึกอาร์กิวเมนต์และค่าที่ส่งคืนของเมธอด add
นี่เป็นตัวอย่างง่ายๆ ของการใช้ method decorators สำหรับการบันทึก (logging), การวิเคราะห์ประสิทธิภาพ (profiling) หรือข้อกังวลอื่นๆ ที่ครอบคลุมหลายส่วน (cross-cutting concerns)
Accessor Decorators
Accessor decorators คล้ายกับ method decorators แต่ใช้กับ getter หรือ setter accessors และได้รับสามอาร์กิวเมนต์เดียวกันคือ 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)
decorator จะป้องกันไม่ให้ getter x
ถูกกำหนดค่าใหม่ ทำให้มันไม่สามารถกำหนดค่าได้ (non-configurable)
Property Decorators
Property decorators ถูกนำไปใช้กับคุณสมบัติของคลาส โดยจะได้รับสองอาร์กิวเมนต์:
target
: Prototype ของคลาส (สำหรับคุณสมบัติของ instance) หรือ constructor ของคลาส (สำหรับคุณสมบัติ static)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
decorator ทำให้คุณสมบัติ name
เป็นแบบอ่านอย่างเดียว (read-only) ป้องกันไม่ให้ถูกแก้ไขหลังจากการ khởi tạo (initialization)
Parameter Decorators
Parameter decorators ถูกนำไปใช้กับพารามิเตอร์ของเมธอดหรือ constructor โดยจะได้รับสามอาร์กิวเมนต์:
target
: Prototype ของคลาส (สำหรับเมธอดของ instance) หรือ constructor ของคลาส (สำหรับเมธอด static หรือ constructor)propertyKey
: ชื่อของเมธอดหรือ constructorparameterIndex
: ดัชนีของพารามิเตอร์ในรายการพารามิเตอร์
Parameter decorators มักใช้กับ reflection เพื่อเก็บ metadata เกี่ยวกับพารามิเตอร์ของฟังก์ชัน จากนั้น metadata นี้สามารถใช้ในขณะรันไทม์สำหรับ dependency injection หรือวัตถุประสงค์อื่นๆ เพื่อให้ทำงานได้อย่างถูกต้อง คุณต้องเปิดใช้งานตัวเลือกคอมไพเลอร์ 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) {
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
decorator จะทำเครื่องหมายพารามิเตอร์ว่าจำเป็น จากนั้น @validate
decorator จะใช้ reflection (ผ่าน reflect-metadata
) เพื่อตรวจสอบว่ามีพารามิเตอร์ที่จำเป็นอยู่หรือไม่ก่อนที่จะเรียกเมธอด ตัวอย่างนี้แสดงการใช้งานพื้นฐาน และขอแนะนำให้สร้างการตรวจสอบพารามิเตอร์ที่แข็งแกร่งในสถานการณ์การใช้งานจริง
ในการติดตั้ง reflect-metadata
:
npm install reflect-metadata --save
การใช้ Decorators สำหรับ Metadata
หนึ่งในการใช้งานหลักของ decorators คือการแนบ metadata กับคลาสและสมาชิกของมัน metadata นี้สามารถใช้ในขณะรันไทม์เพื่อวัตถุประสงค์ต่างๆ เช่น dependency injection, serialization, และ validation ไลบรารี reflect-metadata
เป็นวิธีมาตรฐานในการจัดเก็บและดึงข้อมูล 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
Decorator factories คือฟังก์ชันที่ส่งคืน decorator ทำให้คุณสามารถส่งอาร์กิวเมนต์ไปยัง decorator ได้ ซึ่งทำให้มีความยืดหยุ่นและนำกลับมาใช้ใหม่ได้มากขึ้น
ตัวอย่าง:
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
decorator factory จะรับข้อความแจ้งการเลิกใช้งานเป็นอาร์กิวเมนต์ และจะบันทึกคำเตือนเมื่อเมธอดที่ถูกตกแต่งถูกเรียกใช้ ซึ่งช่วยให้คุณสามารถทำเครื่องหมายเมธอดว่าเลิกใช้งานแล้วและให้คำแนะนำแก่นักพัฒนาเกี่ยวกับวิธีการย้ายไปใช้ทางเลือกใหม่
กรณีการใช้งานในโลกจริง
Decorators มีการใช้งานที่หลากหลายในการพัฒนา JavaScript สมัยใหม่:
- Dependency Injection: เฟรมเวิร์กอย่าง Angular ใช้ decorators อย่างมากสำหรับ dependency injection
- Routing: ในเว็บแอปพลิเคชัน สามารถใช้ decorators เพื่อกำหนดเส้นทาง (routes) สำหรับ controllers และเมธอด
- Validation: สามารถใช้ decorators เพื่อตรวจสอบข้อมูลอินพุตและให้แน่ใจว่าเป็นไปตามเกณฑ์ที่กำหนด
- Authorization: สามารถใช้ decorators เพื่อบังคับใช้นโยบายความปลอดภัยและจำกัดการเข้าถึงเมธอดหรือทรัพยากรบางอย่าง
- Logging and Profiling: ดังที่แสดงในตัวอย่างข้างต้น สามารถใช้ decorators สำหรับการบันทึกและวิเคราะห์ประสิทธิภาพการทำงานของโค้ด
- State Management: Decorators สามารถทำงานร่วมกับไลบรารีการจัดการสถานะ (state management) เพื่ออัปเดตส่วนประกอบโดยอัตโนมัติเมื่อสถานะเปลี่ยนแปลง
ประโยชน์ของการใช้ Decorators
- ปรับปรุงความสามารถในการอ่านโค้ด (Code Readability): Decorators มี синтаксис (syntax) แบบ declarative สำหรับการเพิ่มฟังก์ชันการทำงาน ทำให้โค้ดเข้าใจและบำรุงรักษาง่ายขึ้น
- การแยกส่วนความรับผิดชอบ (Separation of Concerns): Decorators ช่วยให้คุณสามารถแยกข้อกังวลที่ครอบคลุมหลายส่วน (เช่น การบันทึก, การตรวจสอบ, การอนุญาต) ออกจากตรรกะทางธุรกิจหลัก
- การนำกลับมาใช้ใหม่ (Reusability): Decorators สามารถนำกลับมาใช้ใหม่ได้ในหลายคลาสและเมธอด ซึ่งช่วยลดการทำซ้ำของโค้ด
- ความสามารถในการขยาย (Extensibility): Decorators ทำให้ง่ายต่อการขยายฟังก์ชันการทำงานของโค้ดที่มีอยู่โดยไม่ต้องแก้ไขโดยตรง
ความท้าทายและข้อควรพิจารณา
- ช่วงการเรียนรู้ (Learning Curve): Decorators เป็นฟีเจอร์ที่ค่อนข้างใหม่ และอาจต้องใช้เวลาในการเรียนรู้วิธีการใช้งานอย่างมีประสิทธิภาพ
- ความเข้ากันได้ (Compatibility): ตรวจสอบให้แน่ใจว่าสภาพแวดล้อมเป้าหมายของคุณรองรับ decorators และคุณได้กำหนดค่าเครื่องมือ build ของคุณอย่างถูกต้อง
- การดีบัก (Debugging): การดีบักโค้ดที่ใช้ decorators อาจท้าทายกว่าการดีบักโค้ดปกติ โดยเฉพาะอย่างยิ่งหาก decorators มีความซับซ้อน
- การใช้งานมากเกินไป (Overuse): หลีกเลี่ยงการใช้ decorators มากเกินไป เพราะอาจทำให้โค้ดของคุณเข้าใจและบำรุงรักษายากขึ้น ควรใช้ในเชิงกลยุทธ์สำหรับวัตถุประสงค์เฉพาะ
- ค่าใช้จ่ายในการทำงาน (Runtime Overhead): Decorators อาจมีค่าใช้จ่ายในการทำงานบางอย่าง โดยเฉพาะอย่างยิ่งหากดำเนินการที่ซับซ้อน ควรพิจารณาผลกระทบด้านประสิทธิภาพเมื่อใช้ decorators ในแอปพลิเคชันที่ต้องการประสิทธิภาพสูง
บทสรุป
JavaScript Decorators เป็นเครื่องมือที่มีประสิทธิภาพในการปรับปรุงฟังก์ชันการทำงานของโค้ดและส่งเสริมการแยกส่วนความรับผิดชอบ ด้วย синтаксис (syntax) ที่สะอาดและเป็นแบบ declarative สำหรับการเพิ่ม metadata และแปลงคลาส เมธอด คุณสมบัติ และพารามิเตอร์ decorators สามารถช่วยให้คุณสร้าง codebase ที่สามารถบำรุงรักษา นำกลับมาใช้ใหม่ และขยายได้ง่ายขึ้น แม้ว่าจะมาพร้อมกับช่วงการเรียนรู้และความท้าทายที่อาจเกิดขึ้น แต่ประโยชน์ของการใช้ decorators ในบริบทที่เหมาะสมนั้นมีนัยสำคัญ เมื่อระบบนิเวศของ JavaScript พัฒนาอย่างต่อเนื่อง decorators มีแนวโน้มที่จะกลายเป็นส่วนสำคัญของการพัฒนา JavaScript สมัยใหม่มากขึ้น
ลองพิจารณาสำรวจว่า decorators สามารถทำให้โค้ดที่มีอยู่ของคุณง่ายขึ้นได้อย่างไร หรือช่วยให้คุณสามารถเขียนแอปพลิเคชันที่สื่อความหมายและบำรุงรักษาได้ดีขึ้น ด้วยการวางแผนอย่างรอบคอบและความเข้าใจอย่างถ่องแท้ในความสามารถของมัน คุณสามารถใช้ประโยชน์จาก decorators เพื่อสร้างโซลูชัน JavaScript ที่แข็งแกร่งและปรับขนาดได้มากขึ้น