Εξερευνήστε τη δύναμη των Decorators της TypeScript για προγραμματισμό μεταδεδομένων, προσανατολισμένο σε όψεις (AOP) και βελτίωση κώδικα με δηλωτικά πρότυπα. Ένας πλήρης οδηγός για προγραμματιστές.
Διακοσμητές (Decorators) TypeScript: Κατακτώντας Πρότυπα Προγραμματισμού Μεταδεδομένων για Εύρωστες Εφαρμογές
Στο ευρύ τοπίο της σύγχρονης ανάπτυξης λογισμικού, η διατήρηση καθαρών, κλιμακούμενων και διαχειρίσιμων βάσεων κώδικα είναι υψίστης σημασίας. Η TypeScript, με το ισχυρό σύστημα τύπων και τις προηγμένες δυνατότητές της, παρέχει στους προγραμματιστές τα εργαλεία για να το επιτύχουν αυτό. Ανάμεσα στα πιο ενδιαφέροντα και μετασχηματιστικά χαρακτηριστικά της είναι οι Διακοσμητές (Decorators). Ενώ είναι ακόμα ένα πειραματικό χαρακτηριστικό κατά τη συγγραφή αυτού του άρθρου (πρόταση Stage 3 για το ECMAScript), οι διακοσμητές χρησιμοποιούνται ευρέως σε frameworks όπως το Angular και το TypeORM, αλλάζοντας ριζικά τον τρόπο με τον οποίο προσεγγίζουμε τα πρότυπα σχεδίασης, τον προγραμματισμό μεταδεδομένων και τον προγραμματισμό προσανατολισμένο σε όψεις (AOP).
Αυτός ο περιεκτικός οδηγός θα εμβαθύνει στους διακοσμητές της TypeScript, εξερευνώντας τους μηχανισμούς τους, τους διάφορους τύπους, τις πρακτικές εφαρμογές και τις βέλτιστες πρακτικές. Είτε δημιουργείτε εφαρμογές μεγάλης κλίμακας για επιχειρήσεις, microservices, είτε client-side web interfaces, η κατανόηση των διακοσμητών θα σας δώσει τη δυνατότητα να γράφετε πιο δηλωτικό, συντηρήσιμο και ισχυρό κώδικα TypeScript.
Κατανόηση της Βασικής Έννοιας: Τι είναι ένας Διακοσμητής;
Στην καρδιά του, ένας διακοσμητής είναι ένα ειδικό είδος δήλωσης που μπορεί να επισυναφθεί σε μια δήλωση κλάσης, μεθόδου, accessor, ιδιότητας ή παραμέτρου. Οι διακοσμητές είναι συναρτήσεις που επιστρέφουν μια νέα τιμή (ή τροποποιούν μια υπάρχουσα) για τον στόχο που διακοσμούν. Ο πρωταρχικός τους σκοπός είναι να προσθέσουν μεταδεδομένα ή να αλλάξουν τη συμπεριφορά της δήλωσης στην οποία επισυνάπτονται, χωρίς να τροποποιούν άμεσα την υποκείμενη δομή του κώδικα. Αυτός ο εξωτερικός, δηλωτικός τρόπος επέκτασης του κώδικα είναι απίστευτα ισχυρός.
Σκεφτείτε τους διακοσμητές ως σχολιασμούς ή ετικέτες που εφαρμόζετε σε τμήματα του κώδικά σας. Αυτές οι ετικέτες μπορούν στη συνέχεια να διαβαστούν ή να ενεργοποιηθούν από άλλα μέρη της εφαρμογής σας ή από frameworks, συχνά κατά το χρόνο εκτέλεσης, για να παρέχουν πρόσθετη λειτουργικότητα ή διαμόρφωση.
Η Σύνταξη ενός Διακοσμητή
Οι διακοσμητές έχουν ως πρόθεμα το σύμβολο @
, ακολουθούμενο από το όνομα της συνάρτησης του διακοσμητή. Τοποθετούνται αμέσως πριν από τη δήλωση που διακοσμούν.
@MyDecorator
class MyClass {
@AnotherDecorator
myMethod() {
// ...
}
}
Ενεργοποίηση των Decorators στην TypeScript
Πριν μπορέσετε να χρησιμοποιήσετε διακοσμητές, πρέπει να ενεργοποιήσετε την επιλογή του compiler experimentalDecorators
στο αρχείο σας tsconfig.json
. Επιπλέον, για προηγμένες δυνατότητες αντανάκλασης μεταδεδομένων (που χρησιμοποιούνται συχνά από frameworks), θα χρειαστείτε επίσης το emitDecoratorMetadata
και το polyfill 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
# ή
yarn add reflect-metadata
Και να το εισαγάγετε στην κορυφή του αρχείου εισόδου της εφαρμογής σας (π.χ., main.ts
ή app.ts
):
import "reflect-metadata";
// Ο κώδικας της εφαρμογής σας ακολουθεί
Εργοστάσια Διακοσμητών (Decorator Factories): Προσαρμογή στα Χέρια σας
Ενώ ένας βασικός διακοσμητής είναι μια συνάρτηση, συχνά θα χρειαστεί να περάσετε ορίσματα σε έναν διακοσμητή για να διαμορφώσετε τη συμπεριφορά του. Αυτό επιτυγχάνεται με τη χρήση ενός εργοστασίου διακοσμητών (decorator factory). Ένα εργοστάσιο διακοσμητών είναι μια συνάρτηση που επιστρέφει την πραγματική συνάρτηση διακοσμητή. Όταν εφαρμόζετε ένα εργοστάσιο διακοσμητών, το καλείτε με τα ορίσματά του, και αυτό επιστρέφει τη συνάρτηση διακοσμητή που η TypeScript εφαρμόζει στον κώδικά σας.
Δημιουργία ενός Απλού Παραδείγματος Εργοστασίου Διακοσμητών
Ας δημιουργήσουμε ένα εργοστάσιο για έναν διακοσμητή Logger
που μπορεί να καταγράφει μηνύματα με διαφορετικά προθέματα.
function Logger(prefix: string) {
return function (target: Function) {
console.log(`[${prefix}] Η κλάση ${target.name} έχει οριστεί.`);
};
}
@Logger("APP_INIT")
class ApplicationBootstrap {
constructor() {
console.log("Η εφαρμογή ξεκινά...");
}
}
const app = new ApplicationBootstrap();
// Έξοδος:
// [APP_INIT] Η κλάση ApplicationBootstrap έχει οριστεί.
// Η εφαρμογή ξεκινά...
Σε αυτό το παράδειγμα, το Logger("APP_INIT")
είναι η κλήση του εργοστασίου διακοσμητών. Επιστρέφει την πραγματική συνάρτηση διακοσμητή η οποία δέχεται το target: Function
(τον κατασκευαστή της κλάσης) ως όρισμά της. Αυτό επιτρέπει τη δυναμική διαμόρφωση της συμπεριφοράς του διακοσμητή.
Τύποι Διακοσμητών στην TypeScript
Η TypeScript υποστηρίζει πέντε διακριτούς τύπους διακοσμητών, καθένας από τους οποίους εφαρμόζεται σε ένα συγκεκριμένο είδος δήλωσης. Η υπογραφή της συνάρτησης του διακοσμητή ποικίλλει ανάλογα με το πλαίσιο στο οποίο εφαρμόζεται.
1. Διακοσμητές Κλάσης (Class Decorators)
Οι διακοσμητές κλάσης εφαρμόζονται σε δηλώσεις κλάσεων. Η συνάρτηση του διακοσμητή δέχεται τον κατασκευαστή της κλάσης ως το μοναδικό της όρισμα. Ένας διακοσμητής κλάσης μπορεί να παρατηρήσει, να τροποποιήσει ή ακόμα και να αντικαταστήσει έναν ορισμό κλάσης.
Υπογραφή:
function ClassDecorator(target: Function) { ... }
Τιμή Επιστροφής:
Αν ο διακοσμητής κλάσης επιστρέψει μια τιμή, θα αντικαταστήσει τη δήλωση της κλάσης με την παρεχόμενη συνάρτηση κατασκευαστή. Αυτό είναι ένα ισχυρό χαρακτηριστικό, που χρησιμοποιείται συχνά για mixins ή για επέκταση κλάσεων. Αν δεν επιστραφεί καμία τιμή, χρησιμοποιείται η αρχική κλάση.
Περιπτώσεις Χρήσης:
- Καταχώρηση κλάσεων σε ένα κοντέινερ έγχυσης εξαρτήσεων (dependency injection container).
- Εφαρμογή mixins ή πρόσθετων λειτουργιών σε μια κλάση.
- Διαμορφώσεις ειδικές για frameworks (π.χ., δρομολόγηση σε ένα web framework).
- Προσθήκη lifecycle hooks σε κλάσεις.
Παράδειγμα Διακοσμητή Κλάσης: Έγχυση μιας Υπηρεσίας
Φανταστείτε ένα απλό σενάριο έγχυσης εξαρτήσεων όπου θέλετε να επισημάνετε μια κλάση ως "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(`Καταχωρήθηκε η υπηρεσία: ${serviceName}`);
// Προαιρετικά, θα μπορούσατε να επιστρέψετε μια νέα κλάση εδώ για να επεκτείνετε τη συμπεριφορά
return class extends constructor {
createdAt = new Date();
// Πρόσθετες ιδιότητες ή μέθοδοι για όλες τις εγχεόμενες υπηρεσίες
};
};
}
@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("--- Οι Υπηρεσίες Καταχωρήθηκαν ---");
console.log(Array.from(InjectableServiceRegistry.keys()));
const userServiceConstructor = InjectableServiceRegistry.get("UserService");
if (userServiceConstructor) {
const userServiceInstance = new userServiceConstructor();
console.log("Χρήστες:", userServiceInstance.getUsers());
// console.log("User Service Created At:", userServiceInstance.createdAt); // Αν χρησιμοποιείται η επιστρεφόμενη κλάση
}
Αυτό το παράδειγμα δείχνει πώς ένας διακοσμητής κλάσης μπορεί να καταχωρήσει μια κλάση και ακόμη και να τροποποιήσει τον κατασκευαστή της. Ο διακοσμητής Injectable
καθιστά την κλάση ανιχνεύσιμη από ένα θεωρητικό σύστημα έγχυσης εξαρτήσεων.
2. Διακοσμητές Μεθόδου (Method Decorators)
Οι διακοσμητές μεθόδου εφαρμόζονται σε δηλώσεις μεθόδων. Δέχονται τρία ορίσματα: το αντικείμενο-στόχο (για στατικά μέλη, η συνάρτηση κατασκευαστή· για μέλη στιγμιότυπου, το πρωτότυπο της κλάσης), το όνομα της μεθόδου και τον περιγραφητή ιδιότητας (property descriptor) της μεθόδου.
Υπογραφή:
function MethodDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
Τιμή Επιστροφής:
Ένας διακοσμητής μεθόδου μπορεί να επιστρέψει ένα νέο PropertyDescriptor
. Αν το κάνει, αυτός ο περιγραφητής θα χρησιμοποιηθεί για να ορίσει τη μέθοδο. Αυτό σας επιτρέπει να τροποποιήσετε ή να αντικαταστήσετε την υλοποίηση της αρχικής μεθόδου, καθιστώντας το απίστευτα ισχυρό για AOP.
Περιπτώσεις Χρήσης:
- Καταγραφή κλήσεων μεθόδων και των ορισμάτων/αποτελεσμάτων τους.
- Προσωρινή αποθήκευση (caching) αποτελεσμάτων μεθόδων για βελτίωση της απόδοσης.
- Εφαρμογή ελέγχων εξουσιοδότησης πριν από την εκτέλεση της μεθόδου.
- Μέτρηση του χρόνου εκτέλεσης της μεθόδου.
- Debouncing ή throttling κλήσεων μεθόδων.
Παράδειγμα Διακοσμητή Μεθόδου: Παρακολούθηση Απόδοσης
Ας δημιουργήσουμε έναν διακοσμητή 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(`Η μέθοδος "${propertyKey}" εκτελέστηκε σε ${duration.toFixed(2)} ms`);
return result;
};
return descriptor;
}
class DataProcessor {
@MeasurePerformance
processData(data: number[]): number[] {
// Προσομοίωση μιας σύνθετης, χρονοβόρας λειτουργίας
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(`Δεδομένα για το ID: ${id}`);
}, 500);
});
}
}
const processor = new DataProcessor();
processor.processData([1, 2, 3]);
processor.fetchRemoteData("abc").then(result => console.log(result));
Ο διακοσμητής MeasurePerformance
περιτυλίγει την αρχική μέθοδο με λογική χρονισμού, εκτυπώνοντας τη διάρκεια εκτέλεσης χωρίς να επιβαρύνει την επιχειρησιακή λογική μέσα στην ίδια τη μέθοδο. Αυτό είναι ένα κλασικό παράδειγμα Προγραμματισμού Προσανατολισμένου σε Όψεις (AOP).
3. Διακοσμητές Προσπέλασης (Accessor Decorators)
Οι διακοσμητές προσπέλασης εφαρμόζονται σε δηλώσεις accessor (get
και set
). Παρόμοια με τους διακοσμητές μεθόδου, δέχονται το αντικείμενο-στόχο, το όνομα του accessor και τον περιγραφητή ιδιότητάς του.
Υπογραφή:
function AccessorDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
Τιμή Επιστροφής:
Ένας διακοσμητής προσπέλασης μπορεί να επιστρέψει ένα νέο PropertyDescriptor
, το οποίο θα χρησιμοποιηθεί για να ορίσει τον accessor.
Περιπτώσεις Χρήσης:
- Επικύρωση κατά την ανάθεση τιμής σε μια ιδιότητα.
- Μετασχηματισμός μιας τιμής πριν ανατεθεί ή μετά την ανάκτησή της.
- Έλεγχος δικαιωμάτων πρόσβασης για ιδιότητες.
Παράδειγμα Διακοσμητή Προσπέλασης: Προσωρινή Αποθήκευση Getters
Ας δημιουργήσουμε έναν διακοσμητή που αποθηκεύει προσωρινά το αποτέλεσμα ενός ακριβού υπολογισμού 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] Υπολογισμός τιμής για ${String(propertyKey)}`);
this[cacheKey] = originalGetter.apply(this);
} else {
console.log(`[Cache Hit] Χρήση αποθηκευμένης τιμής για ${String(propertyKey)}`);
}
return this[cacheKey];
};
}
return descriptor;
}
class ReportGenerator {
private data: number[];
constructor(data: number[]) {
this.data = data;
}
// Προσομοιώνει έναν ακριβό υπολογισμό
@CachedGetter
get expensiveSummary(): number {
console.log("Εκτέλεση ακριβού υπολογισμού σύνοψης...");
return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;
}
}
const generator = new ReportGenerator([10, 20, 30, 40, 50]);
console.log("Πρώτη πρόσβαση:", generator.expensiveSummary);
console.log("Δεύτερη πρόσβαση:", generator.expensiveSummary);
console.log("Τρίτη πρόσβαση:", generator.expensiveSummary);
Αυτός ο διακοσμητής διασφαλίζει ότι ο υπολογισμός του getter expensiveSummary
εκτελείται μόνο μία φορά· οι επόμενες κλήσεις επιστρέφουν την αποθηκευμένη τιμή. Αυτό το πρότυπο είναι πολύ χρήσιμο για τη βελτιστοποίηση της απόδοσης όπου η πρόσβαση σε μια ιδιότητα περιλαμβάνει βαριούς υπολογισμούς ή εξωτερικές κλήσεις.
4. Διακοσμητές Ιδιότητας (Property Decorators)
Οι διακοσμητές ιδιότητας εφαρμόζονται σε δηλώσεις ιδιοτήτων. Δέχονται δύο ορίσματα: το αντικείμενο-στόχο (για στατικά μέλη, η συνάρτηση κατασκευαστή· για μέλη στιγμιότυπου, το πρωτότυπο της κλάσης) και το όνομα της ιδιότητας.
Υπογραφή:
function PropertyDecorator(target: Object, propertyKey: string | symbol) { ... }
Τιμή Επιστροφής:
Οι διακοσμητές ιδιότητας δεν μπορούν να επιστρέψουν καμία τιμή. Η κύρια χρήση τους είναι η καταχώρηση μεταδεδομένων σχετικά με την ιδιότητα. Δεν μπορούν να αλλάξουν άμεσα την τιμή της ιδιότητας ή τον περιγραφητή της κατά τη στιγμή της διακόσμησης, καθώς ο περιγραφητής για μια ιδιότητα δεν έχει οριστεί πλήρως όταν εκτελούνται οι διακοσμητές ιδιότητας.
Περιπτώσεις Χρήσης:
- Καταχώρηση ιδιοτήτων για serialization/deserialization.
- Εφαρμογή κανόνων επικύρωσης σε ιδιότητες.
- Ορισμός προεπιλεγμένων τιμών ή διαμορφώσεων για ιδιότητες.
- Αντιστοίχιση στηλών 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)} είναι απαιτούμενο.`
});
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:", validate(user1)); // []
const user2 = new UserProfile("", "Smith");
console.log("Σφάλματα επικύρωσης User 2:", validate(user2)); // ["Το firstName είναι απαιτούμενο."]
const user3 = new UserProfile("Alice", "");
console.log("Σφάλματα επικύρωσης User 3:", validate(user3)); // ["Το lastName είναι απαιτούμενο."]
Ο διακοσμητής Required
απλώς καταχωρεί τον κανόνα επικύρωσης σε έναν κεντρικό χάρτη validationRules
. Μια ξεχωριστή συνάρτηση validate
χρησιμοποιεί στη συνέχεια αυτά τα μεταδεδομένα για να ελέγξει το στιγμιότυπο κατά το χρόνο εκτέλεσης. Αυτό το πρότυπο διαχωρίζει τη λογική επικύρωσης από τον ορισμό των δεδομένων, καθιστώντας την επαναχρησιμοποιήσιμη και καθαρή.
5. Διακοσμητές Παραμέτρου (Parameter Decorators)
Οι διακοσμητές παραμέτρου εφαρμόζονται σε παραμέτρους μέσα σε έναν κατασκευαστή κλάσης ή μια μέθοδο. Δέχονται τρία ορίσματα: το αντικείμενο-στόχο (για στατικά μέλη, η συνάρτηση κατασκευαστή· για μέλη στιγμιότυπου, το πρωτότυπο της κλάσης), το όνομα της μεθόδου (ή undefined
για παραμέτρους κατασκευαστή), και τον τακτικό δείκτη της παραμέτρου στη λίστα παραμέτρων της συνάρτησης.
Υπογραφή:
function ParameterDecorator(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) { ... }
Τιμή Επιστροφής:
Οι διακοσμητές παραμέτρου δεν μπορούν να επιστρέψουν καμία τιμή. Όπως και οι διακοσμητές ιδιότητας, ο κύριος ρόλος τους είναι να προσθέτουν μεταδεδομένα σχετικά με την παράμετρο.
Περιπτώσεις Χρήσης:
- Καταχώρηση τύπων παραμέτρων για έγχυση εξαρτήσεων (π.χ.,
@Inject()
στο Angular). - Εφαρμογή επικύρωσης ή μετασχηματισμού σε συγκεκριμένες παραμέτρους.
- Εξαγωγή μεταδεδομένων σχετικά με παραμέτρους αιτημάτων API σε web frameworks.
Παράδειγμα Διακοσμητή Παραμέτρου: Έγχυση Δεδομένων Αιτήματος
Ας προσομοιώσουμε πώς ένα web framework θα μπορούσε να χρησιμοποιήσει διακοσμητές παραμέτρου για να εγχύσει συγκεκριμένα δεδομένα σε μια παράμετρο μεθόδου, όπως ένα ID χρήστη από ένα αίτημα.
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);
};
}
// Μια υποθετική συνάρτηση framework για την κλήση μιας μεθόδου με επιλυμένες παραμέτρους
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(`Ανάκτηση χρήστη με ID: ${userId}, Token: ${authToken || "Δ/Υ"}`);
return { id: userId, name: "Jane Doe" };
}
deleteUser(@RequestParam("id") userId: string) {
console.log(`Διαγραφή χρήστη με ID: ${userId}`);
return { status: "deleted", id: userId };
}
}
const userController = new UserController();
// Προσομοίωση ενός εισερχόμενου αιτήματος
const mockRequest = {
id: "user123",
token: "abc-123",
someOtherProp: "xyz"
};
console.log("\n--- Εκτέλεση getUser ---");
executeWithParams(userController, "getUser", mockRequest);
console.log("\n--- Εκτέλεση deleteUser ---");
executeWithParams(userController, "deleteUser", { id: "user456" });
Αυτό το παράδειγμα δείχνει πώς οι διακοσμητές παραμέτρου μπορούν να συλλέξουν πληροφορίες σχετικά με τις απαιτούμενες παραμέτρους μεθόδου. Ένα framework μπορεί στη συνέχεια να χρησιμοποιήσει αυτά τα συλλεγμένα μεταδεδομένα για να επιλύσει και να εγχύσει αυτόματα τις κατάλληλες τιμές όταν καλείται η μέθοδος, απλοποιώντας σημαντικά τη λογική του controller ή της υπηρεσίας.
Σύνθεση και Σειρά Εκτέλεσης Διακοσμητών
Οι διακοσμητές μπορούν να εφαρμοστούν σε διάφορους συνδυασμούς, και η κατανόηση της σειράς εκτέλεσής τους είναι κρίσιμη για την πρόβλεψη της συμπεριφοράς και την αποφυγή απροσδόκητων προβλημάτων.
Πολλαπλοί Διακοσμητές σε Έναν Στόχο
Όταν πολλαπλοί διακοσμητές εφαρμόζονται σε μία μόνο δήλωση (π.χ., μια κλάση, μέθοδο ή ιδιότητα), εκτελούνται με μια συγκεκριμένη σειρά: από κάτω προς τα πάνω, ή από δεξιά προς τα αριστερά, για την αξιολόγησή τους. Ωστόσο, τα αποτελέσματά τους εφαρμόζονται με την αντίστροφη σειρά.
@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] Κλήση ${String(propertyKey)} με ορίσματα:`, args);
const result = originalMethod.apply(this, args);
console.log(`[LOG] Η μέθοδος ${String(propertyKey)} επέστρεψε:`, 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"]; // Προσομοίωση ανάκτησης ρόλων τρέχοντος χρήστη
const authorized = roles.some(role => currentUserRoles.includes(role));
if (!authorized) {
console.warn(`[AUTH] Άρνηση πρόσβασης για ${String(propertyKey)}. Απαιτούμενοι ρόλοι: ${roles.join(", ")}`);
throw new Error("Μη εξουσιοδοτημένη πρόσβαση");
}
console.log(`[AUTH] Η πρόσβαση επετράπη για ${String(propertyKey)}`);
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class SecureService {
@LogCall
@Authorization(["admin"])
deleteSensitiveData(id: string) {
console.log(`Διαγραφή ευαίσθητων δεδομένων για ID: ${id}`);
return `Το ID δεδομένων ${id} διαγράφηκε.`;
}
@Authorization(["user"])
@LogCall // Η σειρά άλλαξε εδώ
fetchPublicData(query: string) {
console.log(`Ανάκτηση δημόσιων δεδομένων με ερώτημα: ${query}`);
return `Δημόσια δεδομένα για ερώτημα: ${query}`;
}
}
const service = new SecureService();
try {
console.log("\n--- Κλήση deleteSensitiveData (Χρήστης Admin) ---");
service.deleteSensitiveData("record123");
} catch (error: any) {
console.error(error.message);
}
try {
console.log("\n--- Κλήση fetchPublicData (Μη-Admin Χρήστης) ---");
// Προσομοίωση ενός μη-admin χρήστη που προσπαθεί να αποκτήσει πρόσβαση στο fetchPublicData που απαιτεί ρόλο 'user'
const mockUserRoles = ["guest"]; // Αυτό θα αποτύχει στον έλεγχο ταυτότητας
// Για να γίνει αυτό δυναμικό, θα χρειαζόταν ένα σύστημα DI ή στατικό context για τους ρόλους του τρέχοντος χρήστη.
// Για απλότητα, υποθέτουμε ότι ο διακοσμητής Authorization έχει πρόσβαση στο context του τρέχοντος χρήστη.
// Ας προσαρμόσουμε τον διακοσμητή Authorization ώστε να υποθέτει πάντα 'admin' για λόγους επίδειξης,
// ώστε η πρώτη κλήση να πετύχει και η δεύτερη να αποτύχει για να δείξουμε διαφορετικές διαδρομές.
// Εκ νέου εκτέλεση με ρόλο χρήστη για να πετύχει το fetchPublicData.
// Φανταστείτε το currentUserRoles στο Authorization να γίνεται: ['user']
// Για αυτό το παράδειγμα, ας το κρατήσουμε απλό και ας δείξουμε την επίδραση της σειράς.
service.fetchPublicData("search term"); // Αυτό θα εκτελέσει Auth -> Log
} catch (error: any) {
console.error(error.message);
}
/* Αναμενόμενη έξοδος για deleteSensitiveData:
[AUTH] Η πρόσβαση επετράπη για deleteSensitiveData
[LOG] Κλήση deleteSensitiveData με ορίσματα: [ 'record123' ]
Διαγραφή ευαίσθητων δεδομένων για ID: record123
[LOG] Η μέθοδος deleteSensitiveData επέστρεψε: Το ID δεδομένων record123 διαγράφηκε.
*/
/* Αναμενόμενη έξοδος για fetchPublicData (αν ο χρήστης έχει ρόλο 'user'):
[LOG] Κλήση fetchPublicData με ορίσματα: [ 'search term' ]
[AUTH] Η πρόσβαση επετράπη για fetchPublicData
Ανάκτηση δημόσιων δεδομένων με ερώτημα: search term
[LOG] Η μέθοδος fetchPublicData επέστρεψε: Δημόσια δεδομένα για ερώτημα: search term
*/
Παρατηρήστε τη σειρά: για το deleteSensitiveData
, το Authorization
(κάτω) εκτελείται πρώτο, και μετά το LogCall
(πάνω) το περιτυλίγει. Η εσωτερική λογική του Authorization
εκτελείται πρώτη. Για το fetchPublicData
, το LogCall
(κάτω) εκτελείται πρώτο, και μετά το Authorization
(πάνω) το περιτυλίγει. Αυτό σημαίνει ότι η όψη του LogCall
θα είναι έξω από την όψη του Authorization
. Αυτή η διαφορά είναι κρίσιμη για διασταυρούμενες ανησυχίες (cross-cutting concerns) όπως η καταγραφή ή ο χειρισμός σφαλμάτων, όπου η σειρά εκτέλεσης μπορεί να επηρεάσει σημαντικά τη συμπεριφορά.
Σειρά Εκτέλεσης για Διαφορετικούς Στόχους
Όταν μια κλάση, τα μέλη της και οι παράμετροί της έχουν όλοι διακοσμητές, η σειρά εκτέλεσης είναι καλά καθορισμένη:
- Διακοσμητές Παραμέτρου εφαρμόζονται πρώτοι, για κάθε παράμετρο, ξεκινώντας από την τελευταία παράμετρο προς την πρώτη.
- Στη συνέχεια, Διακοσμητές Μεθόδου, Προσπέλασης ή Ιδιότητας εφαρμόζονται για κάθε μέλος.
- Τέλος, Διακοσμητές Κλάσης εφαρμόζονται στην ίδια την κλάση.
Μέσα σε κάθε κατηγορία, πολλαπλοί διακοσμητές στον ίδιο στόχο εφαρμόζονται από κάτω προς τα πάνω (ή από δεξιά προς τα αριστερά).
Παράδειγμα: Πλήρης Σειρά Εκτέλεσης
function log(message: string) {
return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {
if (typeof descriptorOrIndex === 'number') {
console.log(`Διακοσμητής Παραμέτρου: ${message} στην παράμετρο #${descriptorOrIndex} του ${String(propertyKey || "constructor")}`);
} else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {
if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {
console.log(`Διακοσμητής Μεθόδου/Προσπέλασης: ${message} στο ${String(propertyKey)}`);
} else {
console.log(`Διακοσμητής Ιδιότητας: ${message} στο ${String(propertyKey)}`);
}
} else {
console.log(`Διακοσμητής Κλάσης: ${message} στο ${target.name}`);
}
return descriptorOrIndex; // Επιστροφή περιγραφητή για μέθοδο/προσπέλαση, undefined για τα άλλα
};
}
@log("Επιπέδου Κλάσης D")
@log("Επιπέδου Κλάσης C")
class MyDecoratedClass {
@log("Στατική Ιδιότητα A")
static staticProp: string = "";
@log("Ιδιότητα Στιγμιότυπου B")
instanceProp: number = 0;
@log("Μέθοδος D")
@log("Μέθοδος C")
myMethod(
@log("Παράμετρος Z") paramZ: string,
@log("Παράμετρος Y") paramY: number
) {
console.log("Η μέθοδος myMethod εκτελέστηκε.");
}
@log("Getter/Setter F")
get myAccessor() {
return "";
}
set myAccessor(value: string) {
//...
}
constructor() {
console.log("Ο κατασκευαστής εκτελέστηκε.");
}
}
new MyDecoratedClass();
// Κλήση μεθόδου για να ενεργοποιηθεί ο διακοσμητής μεθόδου
new MyDecoratedClass().myMethod("hello", 123);
/* Προβλεπόμενη Σειρά Εξόδου (κατά προσέγγιση, ανάλογα με τη συγκεκριμένη έκδοση TypeScript και τη μεταγλώττιση):
Διακοσμητής Παραμέτρου: Παράμετρος Y στην παράμετρο #1 του myMethod
Διακοσμητής Παραμέτρου: Παράμετρος Z στην παράμετρο #0 του myMethod
Διακοσμητής Ιδιότητας: Στατική Ιδιότητα A στο staticProp
Διακοσμητής Ιδιότητας: Ιδιότητα Στιγμιότυπου B στο instanceProp
Διακοσμητής Μεθόδου/Προσπέλασης: Getter/Setter F στο myAccessor
Διακοσμητής Μεθόδου/Προσπέλασης: Μέθοδος C στο myMethod
Διακοσμητής Μεθόδου/Προσπέλασης: Μέθοδος D στο myMethod
Διακοσμητής Κλάσης: Επιπέδου Κλάσης C στο MyDecoratedClass
Διακοσμητής Κλάσης: Επιπέδου Κλάσης D στο MyDecoratedClass
Ο κατασκευαστής εκτελέστηκε.
Η μέθοδος myMethod εκτελέστηκε.
*/
Ο ακριβής χρόνος καταγραφής στην κονσόλα μπορεί να διαφέρει ελαφρώς ανάλογα με το πότε καλείται ένας κατασκευαστής ή μια μέθοδος, αλλά η σειρά με την οποία εκτελούνται οι ίδιες οι συναρτήσεις διακοσμητών (και συνεπώς οι παρενέργειές τους ή οι επιστρεφόμενες τιμές τους εφαρμόζονται) ακολουθεί τους παραπάνω κανόνες.
Πρακτικές Εφαρμογές και Πρότυπα Σχεδίασης με Διακοσμητές
Οι διακοσμητές, ειδικά σε συνδυασμό με το polyfill reflect-metadata
, ανοίγουν ένα νέο πεδίο προγραμματισμού βασισμένου σε μεταδεδομένα. Αυτό επιτρέπει ισχυρά πρότυπα σχεδίασης που αφαιρούν τον επαναλαμβανόμενο κώδικα (boilerplate) και τις διασταυρούμενες ανησυχίες.
1. Έγχυση Εξαρτήσεων (Dependency Injection - DI)
Μία από τις πιο εξέχουσες χρήσεις των διακοσμητών είναι στα πλαίσια Έγχυσης Εξαρτήσεων (όπως το @Injectable()
, @Component()
του Angular, κ.λπ., ή η εκτεταμένη χρήση DI του NestJS). Οι διακοσμητές σας επιτρέπουν να δηλώνετε εξαρτήσεις απευθείας σε κατασκευαστές ή ιδιότητες, επιτρέποντας στο framework να δημιουργεί αυτόματα στιγμιότυπα και να παρέχει τις σωστές υπηρεσίες.
Παράδειγμα: Απλοποιημένη Έγχυση Υπηρεσίας
import "reflect-metadata"; // Απαραίτητο για το 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(`Η κλάση ${target.name} δεν έχει επισημανθεί ως @Injectable.`);
}
// Λήψη τύπων παραμέτρων κατασκευαστή (απαιτεί emitDecoratorMetadata)
const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", target) || [];
const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];
const dependencies = paramTypes.map((paramType, index) => {
// Χρήση ρητού token @Inject αν παρέχεται, αλλιώς εξαγωγή τύπου
const token = explicitInjections[index] || paramType;
if (token === undefined) {
throw new Error(`Δεν είναι δυνατή η επίλυση της παραμέτρου στη θέση ${index} για το ${target.name}. Μπορεί να είναι κυκλική εξάρτηση ή πρωτογενής τύπος χωρίς ρητό @Inject.`);
}
return Container.resolve(token);
});
const instance = new target(...dependencies);
Container.instances.set(target, instance);
return instance;
}
}
// Ορισμός υπηρεσιών
@Injectable()
class DatabaseService {
connect() {
console.log("Σύνδεση με τη βάση δεδομένων...");
return "Σύνδεση ΒΔ";
}
}
@Injectable()
class AuthService {
private db: DatabaseService;
constructor(db: DatabaseService) {
this.db = db;
}
login() {
console.log(`AuthService: Έλεγχος ταυτότητας με χρήση ${this.db.connect()}`);
return "Ο χρήστης συνδέθηκε";
}
}
@Injectable()
class UserService {
private authService: AuthService;
private dbService: DatabaseService; // Παράδειγμα έγχυσης μέσω ιδιότητας με χρήση προσαρμοσμένου διακοσμητή ή χαρακτηριστικού του framework
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: Ανάκτηση προφίλ χρήστη...");
return { id: 1, name: "Global User" };
}
}
// Επίλυση της κύριας υπηρεσίας
console.log("--- Επίλυση UserService ---");
const userService = Container.resolve(UserService);
console.log(userService.getUserProfile());
console.log("\n--- Επίλυση AuthService (θα πρέπει να είναι αποθηκευμένη) ---");
const authService = Container.resolve(AuthService);
authService.login();
Αυτό το περίπλοκο παράδειγμα δείχνει πώς οι διακοσμητές @Injectable
και @Inject
, σε συνδυασμό με το reflect-metadata
, επιτρέπουν σε έναν προσαρμοσμένο Container
να επιλύει και να παρέχει αυτόματα εξαρτήσεις. Τα μεταδεδομένα design:paramtypes
που εκπέμπονται αυτόματα από την TypeScript (όταν το emitDecoratorMetadata
είναι true) είναι κρίσιμα εδώ.
2. Προγραμματισμός Προσανατολισμένος σε Όψεις (Aspect-Oriented Programming - AOP)
Το AOP εστιάζει στην τμηματοποίηση των διασταυρούμενων ανησυχιών (π.χ., καταγραφή, ασφάλεια, συναλλαγές) που διατρέχουν πολλαπλές κλάσεις και modules. Οι διακοσμητές ταιριάζουν άριστα για την υλοποίηση των εννοιών του AOP στην TypeScript.
Παράδειγμα: Καταγραφή με Διακοσμητή Μεθόδου
Επιστρέφοντας στον διακοσμητή LogCall
, είναι ένα τέλειο παράδειγμα AOP. Προσθέτει συμπεριφορά καταγραφής σε οποιαδήποτε μέθοδο χωρίς να τροποποιεί τον αρχικό κώδικα της μεθόδου. Αυτό διαχωρίζει το "τι να κάνεις" (επιχειρησιακή λογική) από το "πώς να το κάνεις" (καταγραφή, παρακολούθηση απόδοσης, κ.λπ.).
function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[LOG AOP] Είσοδος στη μέθοδο: ${String(propertyKey)} με ορίσματα:`, args);
try {
const result = originalMethod.apply(this, args);
console.log(`[LOG AOP] Έξοδος από τη μέθοδο: ${String(propertyKey)} με αποτέλεσμα:`, result);
return result;
} catch (error: any) {
console.error(`[LOG AOP] Σφάλμα στη μέθοδο ${String(propertyKey)}:`, error.message);
throw error;
}
};
return descriptor;
}
class PaymentProcessor {
@LogMethod
processPayment(amount: number, currency: string) {
if (amount <= 0) {
throw new Error("Το ποσό πληρωμής πρέπει να είναι θετικό.");
}
console.log(`Επεξεργασία πληρωμής ${amount} ${currency}...`);
return `Η πληρωμή ${amount} ${currency} διεκπεραιώθηκε επιτυχώς.`;
}
@LogMethod
refundPayment(transactionId: string) {
console.log(`Επιστροφή χρημάτων για το ID συναλλαγής: ${transactionId}...`);
return `Η επιστροφή χρημάτων ξεκίνησε για το ${transactionId}.`;
}
}
const processor = new PaymentProcessor();
processor.processPayment(100, "USD");
try {
processor.processPayment(-50, "EUR");
} catch (error: any) {
console.error("Ανιχνεύθηκε σφάλμα:", error.message);
}
Αυτή η προσέγγιση διατηρεί την κλάση PaymentProcessor
εστιασμένη αποκλειστικά στη λογική πληρωμών, ενώ ο διακοσμητής LogMethod
χειρίζεται τη διασταυρούμενη ανησυχία της καταγραφής.
3. Επικύρωση και Μετασχηματισμός
Οι διακοσμητές είναι απίστευτα χρήσιμοι για τον ορισμό κανόνων επικύρωσης απευθείας σε ιδιότητες ή για τον μετασχηματισμό δεδομένων κατά τη διάρκεια της σειριοποίησης/αποσειριοποίησης (serialization/deserialization).
Παράδειγμα: Επικύρωση Δεδομένων με Διακοσμητές Ιδιότητας
Το προηγούμενο παράδειγμα @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)} πρέπει να είναι θετικός αριθμός.`);
}
function MaxLength(maxLength: number) {
return function (target: Object, propertyKey: string | symbol) {
addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `Το ${String(propertyKey)} πρέπει να έχει μήκος το πολύ ${maxLength} χαρακτήρες.`);
};
}
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:", Product.validate(product1)); // []
const product2 = new Product("Πολύ μακρύ όνομα προϊόντος που υπερβαίνει το όριο των πενήντα χαρακτήρων για δοκιμαστικούς σκοπούς", 50);
console.log("Σφάλματα Product 2:", Product.validate(product2)); // ["Το name πρέπει να έχει μήκος το πολύ 50 χαρακτήρες."]
const product3 = new Product("Βιβλίο", -10);
console.log("Σφάλματα Product 3:", Product.validate(product3)); // ["Το price πρέπει να είναι θετικός αριθμός."]
Αυτή η ρύθμιση σας επιτρέπει να ορίζετε δηλωτικά κανόνες επικύρωσης στις ιδιότητες του μοντέλου σας, κάνοντας τα μοντέλα δεδομένων σας αυτο-περιγραφικά ως προς τους περιορισμούς τους.
Βέλτιστες Πρακτικές και Παρατηρήσεις
Ενώ οι διακοσμητές είναι ισχυροί, πρέπει να χρησιμοποιούνται με φειδώ. Η κακή χρήση τους μπορεί να οδηγήσει σε κώδικα που είναι πιο δύσκολο να αποσφαλματωθεί ή να κατανοηθεί.
Πότε να Χρησιμοποιείτε Διακοσμητές (και Πότε Όχι)
- Χρησιμοποιήστε τους για:
- Διασταυρούμενες ανησυχίες: Καταγραφή, caching, εξουσιοδότηση, διαχείριση συναλλαγών.
- Δήλωση μεταδεδομένων: Ορισμός σχήματος για ORMs, κανόνες επικύρωσης, διαμόρφωση DI.
- Ενσωμάτωση σε framework: Όταν δημιουργείτε ή χρησιμοποιείτε frameworks που αξιοποιούν μεταδεδομένα.
- Μείωση επαναλαμβανόμενου κώδικα: Αφαίρεση επαναλαμβανόμενων προτύπων κώδικα.
- Αποφύγετε τους για:
- Απλές κλήσεις συναρτήσεων: Αν μια απλή κλήση συνάρτησης μπορεί να επιτύχει το ίδιο αποτέλεσμα με σαφήνεια, προτιμήστε την.
- Επιχειρησιακή λογική: Οι διακοσμητές πρέπει να επεκτείνουν, όχι να ορίζουν, την κύρια επιχειρησιακή λογική.
- Υπερβολική πολυπλοκότητα: Αν η χρήση ενός διακοσμητή καθιστά τον κώδικα λιγότερο ευανάγνωστο ή πιο δύσκολο να ελεγχθεί, ξανασκεφτείτε το.
Επιπτώσεις στην Απόδοση
Οι διακοσμητές εκτελούνται κατά τη μεταγλώττιση (ή κατά τον ορισμό στο runtime της JavaScript αν έχουν μεταγλωττιστεί). Ο μετασχηματισμός ή η συλλογή μεταδεδομένων συμβαίνει όταν ορίζεται η κλάση/μέθοδος, όχι σε κάθε κλήση. Επομένως, η επίδραση στην απόδοση του runtime από την *εφαρμογή* των διακοσμητών είναι ελάχιστη. Ωστόσο, η *λογική μέσα* στους διακοσμητές σας μπορεί να έχει επίπτωση στην απόδοση, ειδικά αν εκτελούν δαπανηρές λειτουργίες σε κάθε κλήση μεθόδου (π.χ., πολύπλοκοι υπολογισμοί μέσα σε έναν διακοσμητή μεθόδου).
Συντηρησιμότητα και Αναγνωσιμότητα
Οι διακοσμητές, όταν χρησιμοποιούνται σωστά, μπορούν να βελτιώσουν σημαντικά την αναγνωσιμότητα μετακινώντας τον επαναλαμβανόμενο κώδικα εκτός της κύριας λογικής. Ωστόσο, αν εκτελούν πολύπλοκους, κρυφούς μετασχηματισμούς, η αποσφαλμάτωση μπορεί να γίνει πρόκληση. Βεβαιωθείτε ότι οι διακοσμητές σας είναι καλά τεκμηριωμένοι και η συμπεριφορά τους είναι προβλέψιμη.
Πειραματική Κατάσταση και το Μέλλον των Διακοσμητών
Είναι σημαντικό να επαναλάβουμε ότι οι διακοσμητές της TypeScript βασίζονται σε μια πρόταση Stage 3 του TC39. Αυτό σημαίνει ότι η προδιαγραφή είναι σε μεγάλο βαθμό σταθερή, αλλά θα μπορούσε ακόμα να υποστεί μικρές αλλαγές πριν γίνει μέρος του επίσημου προτύπου ECMAScript. Frameworks όπως το Angular τους έχουν υιοθετήσει, ποντάροντας στην τελική τους τυποποίηση. Αυτό συνεπάγεται ένα ορισμένο επίπεδο ρίσκου, αν και δεδομένης της ευρείας υιοθέτησής τους, σημαντικές αλλαγές που θα σπάσουν τη συμβατότητα είναι απίθανες.
Η πρόταση του TC39 έχει εξελιχθεί. Η τρέχουσα υλοποίηση της TypeScript βασίζεται σε μια παλαιότερη έκδοση της πρότασης. Υπάρχει μια διάκριση μεταξύ "Legacy Decorators" και "Standard Decorators". Όταν το επίσημο πρότυπο κυκλοφορήσει, η TypeScript πιθανότατα θα ενημερώσει την υλοποίησή της. Για τους περισσότερους προγραμματιστές που χρησιμοποιούν frameworks, αυτή η μετάβαση θα διαχειριστεί από το ίδιο το framework. Για τους συγγραφείς βιβλιοθηκών, η κατανόηση των λεπτών διαφορών μεταξύ των παλαιών και των μελλοντικών τυπικών διακοσμητών μπορεί να καταστεί απαραίτητη.
Η Επιλογή του Compiler emitDecoratorMetadata
Αυτή η επιλογή, όταν οριστεί σε true
στο tsconfig.json
, δίνει εντολή στον compiler της TypeScript να εκπέμψει ορισμένα μεταδεδομένα τύπου σχεδιασμού στον μεταγλωττισμένο JavaScript. Αυτά τα μεταδεδομένα περιλαμβάνουν τον τύπο των παραμέτρων του κατασκευαστή (design:paramtypes
), τον τύπο επιστροφής των μεθόδων (design:returntype
) και τον τύπο των ιδιοτήτων (design:type
).
Αυτά τα εκπεμπόμενα μεταδεδομένα δεν αποτελούν μέρος του τυπικού runtime της JavaScript. Συνήθως καταναλώνονται από το polyfill reflect-metadata
, το οποίο στη συνέχεια τα καθιστά προσβάσιμα μέσω των συναρτήσεων Reflect.getMetadata()
. Αυτό είναι απολύτως κρίσιμο για προηγμένα πρότυπα όπως η Έγχυση Εξαρτήσεων, όπου ένα κοντέινερ πρέπει να γνωρίζει τους τύπους των εξαρτήσεων που απαιτεί μια κλάση χωρίς ρητή διαμόρφωση.
Προηγμένα Πρότυπα με Διακοσμητές
Οι διακοσμητές μπορούν να συνδυαστούν και να επεκταθούν για τη δημιουργία ακόμα πιο εξελιγμένων προτύπων.
1. Διακόσμηση Διακοσμητών (Higher-Order Decorators)
Μπορείτε να δημιουργήσετε διακοσμητές που τροποποιούν ή συνθέτουν άλλους διακοσμητές. Αυτό είναι λιγότερο συνηθισμένο αλλά αποδεικνύει τη συναρτησιακή φύση των διακοσμητών.
// Ένας διακοσμητής που διασφαλίζει ότι μια μέθοδος καταγράφεται και απαιτεί επίσης ρόλους admin
function AdminAndLoggedMethod() {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Εφαρμογή του Authorization πρώτα (εσωτερικά)
Authorization(["admin"])(target, propertyKey, descriptor);
// Στη συνέχεια εφαρμογή του LogCall (εξωτερικά)
LogCall(target, propertyKey, descriptor);
return descriptor; // Επιστροφή του τροποποιημένου περιγραφητή
};
}
class AdminPanel {
@AdminAndLoggedMethod()
deleteUserAccount(userId: string) {
console.log(`Διαγραφή λογαριασμού χρήστη: ${userId}`);
return `Ο χρήστης ${userId} διαγράφηκε.`;
}
}
const adminPanel = new AdminPanel();
adminPanel.deleteUserAccount("user007");
/* Αναμενόμενη Έξοδος (υποθέτοντας ρόλο admin):
[AUTH] Η πρόσβαση επετράπη για deleteUserAccount
[LOG] Κλήση deleteUserAccount με ορίσματα: [ 'user007' ]
Διαγραφή λογαριασμού χρήστη: user007
[LOG] Η μέθοδος deleteUserAccount επέστρεψε: Ο χρήστης user007 διαγράφηκε.
*/
Εδώ, το AdminAndLoggedMethod
είναι ένα εργοστάσιο που επιστρέφει έναν διακοσμητή, και μέσα σε αυτόν τον διακοσμητή, εφαρμόζει δύο άλλους διακοσμητές. Αυτό το πρότυπο μπορεί να ενσωματώσει πολύπλοκες συνθέσεις διακοσμητών.
2. Χρήση Διακοσμητών για Mixins
Ενώ η TypeScript προσφέρει άλλους τρόπους υλοποίησης mixins, οι διακοσμητές μπορούν να χρησιμοποιηθούν για την έγχυση δυνατοτήτων σε κλάσεις με δηλωτικό τρόπο.
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("Το αντικείμενο απορρίφθηκε.");
}
}
class Loggable {
log(message: string) {
console.log(`[Loggable] ${message}`);
}
}
@ApplyMixins([Disposable, Loggable])
class MyResource implements Disposable, Loggable {
// Αυτές οι ιδιότητες/μέθοδοι εγχέονται από τον διακοσμητή
isDisposed!: boolean;
dispose!: () => void;
log!: (message: string) => void;
constructor(public name: string) {
this.log(`Ο πόρος ${this.name} δημιουργήθηκε.`);
}
cleanUp() {
this.dispose();
this.log(`Ο πόρος ${this.name} καθαρίστηκε.`);
}
}
const resource = new MyResource("NetworkConnection");
console.log(`Έχει απορριφθεί: ${resource.isDisposed}`);
resource.cleanUp();
console.log(`Έχει απορριφθεί: ${resource.isDisposed}`);
Αυτός ο διακοσμητής @ApplyMixins
αντιγράφει δυναμικά μεθόδους και ιδιότητες από βασικούς κατασκευαστές στο πρωτότυπο της παραγόμενης κλάσης, ουσιαστικά "αναμιγνύοντας" λειτουργικότητες.
Συμπέρασμα: Ενδυναμώνοντας τη Σύγχρονη Ανάπτυξη με TypeScript
Οι διακοσμητές της TypeScript είναι ένα ισχυρό και εκφραστικό χαρακτηριστικό που επιτρέπει ένα νέο παράδειγμα προγραμματισμού βασισμένου σε μεταδεδομένα και προσανατολισμένου σε όψεις. Επιτρέπουν στους προγραμματιστές να βελτιώνουν, να τροποποιούν και να προσθέτουν δηλωτικές συμπεριφορές σε κλάσεις, μεθόδους, ιδιότητες, accessors και παραμέτρους χωρίς να αλλοιώνουν την κύρια λογική τους. Αυτός ο διαχωρισμός των αρμοδιοτήτων οδηγεί σε καθαρότερο, πιο συντηρήσιμο και εξαιρετικά επαναχρησιμοποιήσιμο κώδικα.
Από την απλοποίηση της έγχυσης εξαρτήσεων και την υλοποίηση στιβαρών συστημάτων επικύρωσης έως την προσθήκη διασταυρούμενων ανησυχιών όπως η καταγραφή και η παρακολούθηση της απόδοσης, οι διακοσμητές παρέχουν μια κομψή λύση σε πολλές κοινές προκλήσεις ανάπτυξης. Ενώ η πειραματική τους κατάσταση απαιτεί προσοχή, η ευρεία υιοθέτησή τους σε μεγάλα frameworks σηματοδοτεί την πρακτική τους αξία και τη μελλοντική τους σημασία.
Κατακτώντας τους διακοσμητές της TypeScript, αποκτάτε ένα σημαντικό εργαλείο στο οπλοστάσιό σας, που σας επιτρέπει να δημιουργείτε πιο εύρωστες, κλιμακούμενες και ευφυείς εφαρμογές. Υιοθετήστε τους υπεύθυνα, κατανοήστε τους μηχανισμούς τους και ξεκλειδώστε ένα νέο επίπεδο δηλωτικής δύναμης στα έργα σας με TypeScript.