Cours JavaScript et TypeScript · Module 7: TypeScript avancé
Décorateurs de méthode et de propriété
Dans cette leçon12
« La supervision des opérations individuelles à Jurassic Park est tout aussi importante que la gestion du parc dans son ensemble », explique le Dr Henry Wu, généticien en chef d'InGen. « Tout comme nous surveillons chaque aspect de la vie des dinosaures - leur alimentation, leur comportement et leur croissance - nous devons aussi contrôler et sécuriser les opérations individuelles dans notre système. »
En TypeScript, après avoir compris les décorateurs de classe, l'étape suivante naturelle consiste à approfondir les décorateurs de méthode et de propriété. Ces décorateurs nous permettent de modifier précisément le comportement de membres spécifiques d'une classe, tout comme à Jurassic Park, où chaque opération, du nourrissage des dinosaures à la sécurisation des enclos, requiert une supervision et des protocoles particuliers.
Introduction aux décorateurs de méthode et de propriété
Les décorateurs de méthode et de propriété permettent de modifier le comportement, l'accès ou la valeur de membres individuels d'une classe. Grâce à eux, nous pouvons :
- Ajouter un comportement supplémentaire avant ou après l'exécution d'une méthode
- Valider les paramètres d'une méthode
- Mettre en cache les résultats d'une méthode
- Mesurer le temps d'exécution d'une méthode
- Contrôler l'accès à une propriété
- Transformer les valeurs d'une propriété
- Ajouter des métadonnées aux méthodes et aux propriétés
Décorateurs de méthode
Les décorateurs de méthode sont des fonctions qui prennent trois paramètres :
target- le prototype de la classe (pour les méthodes d'instance) ou le constructeur de la classe (pour les méthodes statiques)propertyKey- le nom de la méthodedescriptor- le descripteur de propriété de la méthode
Syntaxe de base d'un décorateur de méthode :
1function methodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
2 // Modification of the method descriptor
3 // Return the modified descriptor or undefined
4}
5
6class MyClass {
7 @methodDecorator
8 myMethod() {
9 // Method implementation
10 }
11}Exemple : système de journalisation des opérations à Jurassic Park
Commençons par un décorateur de méthode simple qui journalisera l'exécution des opérations à Jurassic Park :
1// Method decorator that logs calls
2function LogOperation(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
3 // Save the original method
4 const originalMethod = descriptor.value;
5
6 // Override the method
7 descriptor.value = function(...args: any[]) {
8 console.log(`[${new Date().toISOString()}] Calling ${propertyKey} with arguments: ${JSON.stringify(args)}`);
9
10 try {
11 // Call the original method
12 const result = originalMethod.apply(this, args);
13
14 // Log the result
15 console.log(`[${new Date().toISOString()}] ${propertyKey} completed. Result: ${JSON.stringify(result)}`);
16
17 return result;
18 } catch (error) {
19 // Log the error
20 console.error(`[${new Date().toISOString()}] ${propertyKey} failed with error: ${error}`);
21
22 // Re-throw the error
23 throw error;
24 }
25 };
26
27 return descriptor;
28}
29
30// Applying the decorator in the enclosure management class
31class EnclosureManagement {
32 constructor(private employeeName: string) {}
33
34 @LogOperation
35 openEnclosure(enclosureId: string): boolean {
36 console.log(`${this.employeeName} opens enclosure: ${enclosureId}`);
37 // Enclosure opening logic here
38 return true;
39 }
40
41 @LogOperation
42 closeEnclosure(enclosureId: string): boolean {
43 console.log(`${this.employeeName} closes enclosure: ${enclosureId}`);
44 // Enclosure closing logic here
45 return true;
46 }
47
48 checkStatus(enclosureId: string): string {
49 // This method is not logged
50 return "Enclosure closed";
51 }
52}
53
54// Using the class with decorated methods
55const manager = new EnclosureManagement("John");
56manager.openEnclosure("A-001");
57manager.closeEnclosure("A-001");
58manager.checkStatus("A-001"); // This operation will not be loggedDans l'exemple ci-dessus, le décorateur LogOperation enregistre chaque appel de méthode, ses arguments et le résultat ou l'erreur. Les méthodes openEnclosure et closeEnclosure sont journalisées, tandis que la méthode checkStatus n'a pas de décorateur, elle n'est donc pas journalisée.
Décorateurs de méthode avec paramètres
On peut aussi créer des décorateurs de méthode qui acceptent des paramètres :
1// Method decorator with parameters
2function RequireAccessLevel(level: number) {
3 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
4 // Save the original method
5 const originalMethod = descriptor.value;
6
7 // Override the method
8 descriptor.value = function(...args: any[]) {
9 // Check if the object has an access level property
10 if (!(this as any).accessLevel) {
11 throw new Error("Object does not have a defined access level!");
12 }
13
14 // Verify the access level
15 if ((this as any).accessLevel < level) {
16 throw new Error(`Required access level: ${level}, available: ${(this as any).accessLevel}`);
17 }
18
19 // Call the original method
20 return originalMethod.apply(this, args);
21 };
22
23 return descriptor;
24 };
25}
26
27// Class representing a laboratory
28class GeneticLaboratory {
29 constructor(public employeeName: string, public accessLevel: number) {}
30
31 @RequireAccessLevel(1)
32 readDnaData(dinosaurId: string): string {
33 return `Dinosaur DNA ${dinosaurId}: ACGT...`;
34 }
35
36 @RequireAccessLevel(2)
37 modifyDNA(dinosaurId: string, sequence: string): boolean {
38 console.log(`DNA modification of dinosaur ${dinosaurId}: ${sequence}`);
39 return true;
40 }
41
42 @RequireAccessLevel(3)
43 createNewSpecies(name: string, baseDNA: string): string {
44 const newSpeciesId = `NGT-${Math.floor(Math.random() * 1000)}`;
45 console.log(`Creating new species: ${name} (ID: ${newSpeciesId})`);
46 return newSpeciesId;
47 }
48}
49
50// Using the class with different access levels
51const intern = new GeneticLaboratory("Tim", 1);
52const scientist = new GeneticLaboratory("Dr. Grant", 2);
53const dyrektor = new GeneticLaboratory("Dr. Wu", 3);
54
55// Testing access
56try {
57 console.log(intern.readDnaData("D-001")); // Will work
58 intern.modifyDNA("D-001", "ACGT"); // Will throw error
59} catch (error) {
60 console.error(error.message);
61}
62
63try {
64 scientist.modifyDNA("D-001", "ACGT"); // Will work
65 scientist.createNewSpecies("Neo-Raptor", "ACGT..."); // Will throw error
66} catch (error) {
67 console.error(error.message);
68}
69
70// Director has full access
71dyrektor.createNewSpecies("Indominus Rex", "ACGT...");Dans cet exemple, le décorateur RequireAccessLevel vérifie si la personne appelant la méthode dispose d'un niveau d'accès suffisant. Différentes méthodes requièrent différents niveaux d'accès, ce qui reflète les protocoles de sécurité réels qui pourraient exister à Jurassic Park.
Décorateurs de propriété
Les décorateurs de propriété sont similaires aux décorateurs de méthode mais ne reçoivent que deux paramètres :
target- le prototype de la classe (pour les propriétés d'instance) ou le constructeur de la classe (pour les propriétés statiques)propertyKey- le nom de la propriété
Syntaxe de base d'un décorateur de propriété :
1function propertyDecorator(target: any, propertyKey: string) {
2 // Decorator implementation
3}
4
5class MyClass {
6 @propertyDecorator
7 myProperty: string;
8}Comme les décorateurs de propriété ne reçoivent pas de descripteur, ils sont souvent utilisés pour générer des getters et des setters ou pour appliquer des métadonnées.
Exemple : système de surveillance des ressources à Jurassic Park
Implémentons un décorateur de propriété qui surveillera les changements de ressources à Jurassic Park :
1// Property decorator that monitors changes
2function MonitorChanges(target: any, propertyKey: string) {
3 // Create a unique name for the private property
4 const privateName = Symbol(propertyKey);
5
6 // Define getter and setter
7 Object.defineProperty(target, propertyKey, {
8 get: function() {
9 // Return the private property value
10 return this[privateName];
11 },
12 set: function(value: any) {
13 // Save the old value
14 const oldValue = this[privateName];
15
16 // Log the change if it's not the first assignment
17 if (this[privateName] !== undefined) {
18 console.log(`[CHANGE][${this.constructor.name}] ${propertyKey}: ${oldValue} -> ${value}`);
19 }
20
21 // Save the new value
22 this[privateName] = value;
23 },
24 enumerable: true,
25 configurable: true
26 });
27}
28
29// Class monitoring park resources
30class ParkResources {
31 @MonitorChanges
32 foodAmount: number = 1000;
33
34 @MonitorChanges
35 generatorsState: "Active" | "Backup" | "Disabled" = "Active";
36
37 @MonitorChanges
38 gateStatus: "Open" | "Closed" = "Closed";
39
40 constructor(public sectionName: string) {}
41
42 useFood(amount: number): boolean {
43 if (this.foodAmount >= amount) {
44 this.foodAmount -= amount;
45 return true;
46 }
47 return false;
48 }
49
50 refillFood(amount: number): void {
51 this.foodAmount += amount;
52 }
53
54 toggleGenerators(state: "Active" | "Backup" | "Disabled"): void {
55 this.generatorsState = state;
56 }
57
58 toggleGate(status: "Open" | "Closed"): void {
59 this.gateStatus = status;
60 }
61}
62
63// Using the class with monitored properties
64const sectionA = new ParkResources("Section A");
65
66// Testing property changes
67sectionA.useFood(200); // foodAmount: 1000 -> 800
68sectionA.refillFood(500); // foodAmount: 800 -> 1300
69sectionA.toggleGenerators("Backup"); // generatorsState: Active -> Backup
70sectionA.toggleGate("Open"); // gateStatus: Closed -> OpenDans cet exemple, le décorateur MonitorChanges crée un getter et un setter pour les propriétés qui journalisent chaque changement de valeur. Ainsi, nous pouvons suivre quand et quels changements ont été effectués, ce qui est crucial pour surveiller les ressources du parc.
Combiner décorateurs de méthode et de propriété
On peut combiner différents décorateurs de méthode et de propriété pour créer des comportements plus avancés :
1// Decorator measuring method execution time
2function MeasureExecutionTime(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
3 const originalMethod = descriptor.value;
4
5 descriptor.value = function(...args: any[]) {
6 const start = performance.now();
7 const result = originalMethod.apply(this, args);
8 const end = performance.now();
9
10 console.log(`[TIME] ${propertyKey}: ${(end - start).toFixed(2)}ms`);
11
12 return result;
13 };
14
15 return descriptor;
16}
17
18// Decorator validating method parameters
19function ValidateParameters(validator: (args: any[]) => boolean, errorMessage: string) {
20 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
21 const originalMethod = descriptor.value;
22
23 descriptor.value = function(...args: any[]) {
24 if (!validator(args)) {
25 throw new Error(`[VALIDATION ERROR] ${propertyKey}: ${errorMessage}`);
26 }
27
28 return originalMethod.apply(this, args);
29 };
30
31 return descriptor;
32 };
33}
34
35// Decorator caching method results
36function Cache(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
37 const originalMethod = descriptor.value;
38 const cache = new Map<string, any>();
39
40 descriptor.value = function(...args: any[]) {
41 const key = JSON.stringify(args);
42
43 if (cache.has(key)) {
44 console.log(`[CACHE] ${propertyKey}: Used cached result for ${key}`);
45 return cache.get(key);
46 }
47
48 const result = originalMethod.apply(this, args);
49 cache.set(key, result);
50
51 return result;
52 };
53
54 return descriptor;
55}
56
57// Decorator adding a value range for a numeric property
58function Range(min: number, max: number) {
59 return function(target: any, propertyKey: string) {
60 // Create a unique name for the private property
61 const privateName = Symbol(propertyKey);
62
63 // Define getter and setter
64 Object.defineProperty(target, propertyKey, {
65 get: function() {
66 return this[privateName];
67 },
68 set: function(value: number) {
69 if (value < min) {
70 console.warn(`[RANGE] ${propertyKey}: Value ${value} below minimum ${min}. Setting ${min}.`);
71 this[privateName] = min;
72 } else if (value > max) {
73 console.warn(`[RANGE] ${propertyKey}: Value ${value} above maximum ${max}. Setting ${max}.`);
74 this[privateName] = max;
75 } else {
76 this[privateName] = value;
77 }
78 },
79 enumerable: true,
80 configurable: true
81 });
82 };
83}
84
85// Class managing the electric fence system
86class ElectricFenceSystem {
87 @Range(0, 10000)
88 voltage: number = 5000; // in volts
89
90 @Range(0, 100)
91 range: number = 50; // in meters
92
93 private statusActive: boolean = false;
94
95 @LogOperation
96 @MeasureExecutionTime
97 @ValidateParameters(
98 args => args[0] >= 0 && args[0] <= 10000,
99 "Invalid voltage. Allowed range: 0-10000V"
100 )
101 setVoltage(newVoltage: number): boolean {
102 // Simulating a long operation for testing the MeasureExecutionTime decorator
103 for (let i = 0; i < 1000000; i++) {
104 // CPU load
105 }
106
107 this.voltage = newVoltage;
108 return true;
109 }
110
111 @LogOperation
112 @Cache
113 getSystemStatus(sectionId: string): { active: boolean; voltage: number; range: number } {
114 console.log(`Fetching system status for section ${sectionId}...`);
115
116 // Simulating a long operation
117 for (let i = 0; i < 500000; i++) {
118 // CPU load
119 }
120
121 return {
122 active: this.statusActive,
123 voltage: this.voltage,
124 range: this.range
125 };
126 }
127
128 @LogOperation
129 @MeasureExecutionTime
130 activateSystem(): boolean {
131 if (this.statusActive) {
132 console.log("System already active");
133 return false;
134 }
135
136 console.log(`Activating electric fence system. Voltage: ${this.voltage}V, Range: ${this.range}m`);
137 this.statusActive = true;
138
139 return true;
140 }
141
142 @LogOperation
143 @MeasureExecutionTime
144 deactivateSystem(): boolean {
145 if (!this.statusActive) {
146 console.log("System already inactive");
147 return false;
148 }
149
150 console.log("Deactivating electric fence system");
151 this.statusActive = false;
152
153 return true;
154 }
155}
156
157// Using the class with multiple decorators
158const fenceSystem = new ElectricFenceSystem();
159
160// Testing various functionalities
161fenceSystem.setVoltage(7500); // Logs operation, measures time, validates parameter
162fenceSystem.setVoltage(12000); // Will exceed range, will be set to 10000
163fenceSystem.activateSystem(); // Logs operation, measures time
164
165// Testing caching
166console.log(fenceSystem.getSystemStatus("A1")); // Full operation
167console.log(fenceSystem.getSystemStatus("A1")); // Uses cache
168console.log(fenceSystem.getSystemStatus("B2")); // Full operation
169console.log(fenceSystem.getSystemStatus("B2")); // Uses cache
170
171// Deactivating the system
172fenceSystem.deactivateSystem();Dans cet exemple, nous utilisons plusieurs décorateurs pour créer un système avancé de clôture électrique :
@Range- garantit que les valeurs de la propriété restent dans la plage indiquée@LogOperation- journalise les appels de méthode@MeasureExecutionTime- mesure le temps d'exécution des méthodes@ValidateParameters- vérifie la validité des paramètres des méthodes@Cache- met en cache les résultats des méthodes en mémoire
Métadonnées et réflexion avec les décorateurs
TypeScript permet aussi d'utiliser des métadonnées avec les décorateurs, ce qui ouvre la voie à des cas d'usage plus avancés. Cela nécessite d'activer les options emitDecoratorMetadata et experimentalDecorators dans le fichier tsconfig.json et d'installer le paquet reflect-metadata.
Voici un exemple d'utilisation des métadonnées avec les décorateurs :
1// We need to import reflect-metadata at the start of the application
2import "reflect-metadata";
3
4// Keys for metadata
5const PARAMETER_KEY = Symbol("parameters");
6
7// Parameter decorator
8function Parameter(target: any, propertyKey: string, parameterIndex: number) {
9 // Get existing metadata or create empty
10 const parameters: number[] = Reflect.getOwnMetadata(PARAMETER_KEY, target, propertyKey) || [];
11
12 // Add parameter index to the list
13 parameters.push(parameterIndex);
14
15 // Save metadata
16 Reflect.defineMetadata(PARAMETER_KEY, parameters, target, propertyKey);
17}
18
19// Method decorator validating parameters
20function ValidateData(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
21 const originalMethod = descriptor.value;
22
23 descriptor.value = function(...args: any[]) {
24 // Get parameter types (requires emitDecoratorMetadata option)
25 const types = Reflect.getMetadata("design:paramtypes", target, propertyKey);
26
27 // Get parameter indices marked with the @Parameter decorator
28 const markedParameters: number[] = Reflect.getOwnMetadata(PARAMETER_KEY, target, propertyKey) || [];
29
30 // Validate parameters
31 for (const index of markedParameters) {
32 const value = args[index];
33 const type = types[index];
34
35 // Type validation
36 if (value === null || value === undefined) {
37 throw new Error(`Parameter #${index} of method ${propertyKey} cannot be null or undefined`);
38 }
39
40 // Validation for different types
41 if (type === String && typeof value !== "string") {
42 throw new Error(`Parameter #${index} of method ${propertyKey} must be a string`);
43 } else if (type === Number && typeof value !== "number") {
44 throw new Error(`Parameter #${index} of method ${propertyKey} must be a number`);
45 } else if (type === Boolean && typeof value !== "boolean") {
46 throw new Error(`Parameter #${index} of method ${propertyKey} must be a boolean`);
47 } else if (type === Date && !(value instanceof Date)) {
48 throw new Error(`Parameter #${index} of method ${propertyKey} must be a Date`);
49 }
50 }
51
52 // Call the original method
53 return originalMethod.apply(this, args);
54 };
55
56 return descriptor;
57}
58
59// Dinosaur operations class with metadata
60class DinosaurOperations {
61 @ValidateData
62 conductResearchHealth(
63 dinosaurId: string,
64 @Parameter examinationDate: Date,
65 @Parameter temperature: number,
66 @Parameter weight: number,
67 notes?: string
68 ): string {
69 console.log(`Health examination of dinosaur ${dinosaurId}`);
70 console.log(`Date: ${examinationDate.toLocaleDateString()}`);
71 console.log(`Temperature: ${temperature}°C`);
72 console.log(`Weight: ${weight}kg`);
73
74 if (notes) {
75 console.log(`Notes: ${notes}`);
76 }
77
78 return "Examination completed";
79 }
80
81 @ValidateData
82 performVaccination(
83 @Parameter dinosaurId: string,
84 @Parameter vaccinationType: string,
85 @Parameter vaccinationDate: Date
86 ): boolean {
87 console.log(`Vaccination of dinosaur ${dinosaurId}`);
88 console.log(`Vaccination type: ${vaccinationType}`);
89 console.log(`Date: ${vaccinationDate.toLocaleDateString()}`);
90
91 return true;
92 }
93}
94
95// Using the class with parameter validation
96const operations = new DinosaurOperations();
97
98try {
99 // Correct call
100 operations.conductResearchHealth(
101 "D-001",
102 new Date(),
103 38.5,
104 1200,
105 "Dinosaur in good condition"
106 );
107
108 // Incorrect call - temperature is not a number
109 operations.conductResearchHealth(
110 "D-002",
111 new Date(),
112 "high" as any, // This will cause an error
113 1500
114 );
115} catch (error) {
116 console.error(error.message);
117}
118
119try {
120 // Correct call
121 operations.performVaccination(
122 "D-003",
123 "Anti-parasite vaccine",
124 new Date()
125 );
126
127 // Incorrect call - date is not a Date object
128 operations.performVaccination(
129 "D-004",
130 "Anti-flu vaccine",
131 "today" as any // This will cause an error
132 );
133} catch (error) {
134 console.error(error.message);
135}Dans cet exemple, nous utilisons les métadonnées et la réflexion pour valider les paramètres d'une méthode. Le décorateur @Parameter marque les paramètres qui doivent être validés, et le décorateur @ValidateData vérifie les types de ces paramètres à chaque appel de la méthode.
Exemple pratique : système de gestion des examens de dinosaures
Combinons maintenant toutes les techniques abordées dans un exemple plus vaste - un système de gestion des examens de dinosaures :
1// Property decorator ensuring unique IDs
2function UniqueID(prefix: string = "ID") {
3 return function(target: any, propertyKey: string) {
4 const generateId = () => `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
5
6 Object.defineProperty(target, propertyKey, {
7 get: function() {
8 // If the property hasn't been set yet, generate an ID
9 if (!this[`_${propertyKey}`]) {
10 this[`_${propertyKey}`] = generateId();
11 }
12 return this[`_${propertyKey}`];
13 },
14 set: function(value: string) {
15 // Allow setting only once
16 if (!this[`_${propertyKey}`]) {
17 this[`_${propertyKey}`] = value;
18 } else {
19 console.warn(`Property ${propertyKey} has already been set and cannot be changed.`);
20 }
21 },
22 enumerable: true,
23 configurable: true
24 });
25 };
26}
27
28// Method decorator for date validation
29function ValidateExaminationDate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
30 const originalMethod = descriptor.value;
31
32 descriptor.value = function(...args: any[]) {
33 // Assume the first argument is the examination date
34 const examinationDate = args[0];
35
36 if (!(examinationDate instanceof Date)) {
37 throw new Error("First parameter must be a Date instance");
38 }
39
40 const now = new Date();
41
42 if (examinationDate > now) {
43 throw new Error("Examination date cannot be in the future");
44 }
45
46 // Check if the date is not older than 30 days
47 const thirtyDaysAgo = new Date();
48 thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
49
50 if (examinationDate < thirtyDaysAgo) {
51 console.warn("Warning: Examination date is older than 30 days");
52 }
53
54 return originalMethod.apply(this, args);
55 };
56
57 return descriptor;
58}
59
60// Method decorator for vaccination frequency control
61function ControlFrequency(minDaysBetweenVaccinations: number) {
62 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
63 // Map storing last vaccination dates for each dinosaur
64 const lastVaccinations = new Map<string, Date>();
65
66 const originalMethod = descriptor.value;
67
68 descriptor.value = function(...args: any[]) {
69 // Assume first argument is dinosaur ID, second is vaccination date
70 const dinosaurId = args[0];
71 const vaccinationDate = args[1];
72
73 if (lastVaccinations.has(dinosaurId)) {
74 const lastVaccine = lastVaccinations.get(dinosaurId)!;
75 const differenceDays = Math.floor((vaccinationDate.getTime() - lastVaccine.getTime()) / (1000 * 60 * 60 * 24));
76
77 if (differenceDays < minDaysBetweenVaccinations) {
78 throw new Error(`Too early for next vaccination. Minimum interval is ${minDaysBetweenVaccinations} days. ${minDaysBetweenVaccinations - differenceDays} days remaining.`);
79 }
80 }
81
82 // Save vaccination date
83 lastVaccinations.set(dinosaurId, vaccinationDate);
84
85 return originalMethod.apply(this, args);
86 };
87
88 return descriptor;
89 };
90}
91
92// Examination results interface
93interface ExaminationResults {
94 temperature: number; // in degrees Celsius
95 weight: number; // in kilograms
96 length: number; // in meters
97 healthStatus: "Healthy" | "Light symptoms" | "Sick" | "Critical";
98 notes?: string;
99}
100
101// Dinosaur examination class
102class DinosaurExamination {
103 @UniqueID("BAD")
104 examinationId: string;
105
106 constructor(
107 public dinosaurId: string,
108 public examinationDate: Date,
109 public results: ExaminationResults,
110 public examiner: string
111 ) {}
112
113 @LogOperation
114 getReport(): string {
115 return `
116 === EXAMINATION REPORT ===
117 Examination ID: ${this.examinationId}
118 Dinosaur ID: ${this.dinosaurId}
119 Date: ${this.examinationDate.toLocaleString()}
120 Conducted by: ${this.examiner}
121
122 RESULTS:
123 - Temperature: ${this.results.temperature}°C
124 - Weight: ${this.results.weight} kg
125 - Length: ${this.results.length} m
126 - Health status: ${this.results.healthStatus}
127 ${this.results.notes ? `- Notes: ${this.results.notes}` : ""}
128 `;
129 }
130}
131
132// Dinosaur vaccination class
133class DinosaurVaccination {
134 @UniqueID("SZCZ")
135 vaccinationId: string;
136
137 constructor(
138 public dinosaurId: string,
139 public vaccinationDate: Date,
140 public genusVaccine: string,
141 public dose: number,
142 public vaccinator: string
143 ) {}
144
145 @LogOperation
146 getReport(): string {
147 return `
148 === VACCINATION REPORT ===
149 Vaccination ID: ${this.vaccinationId}
150 Dinosaur ID: ${this.dinosaurId}
151 Date: ${this.vaccinationDate.toLocaleString()}
152 Vaccine type: ${this.genusVaccine}
153 Dose: ${this.dose} ml
154 Conducted by: ${this.vaccinator}
155 `;
156 }
157}
158
159// Examination and vaccination registry
160class MedicalRegistry {
161 private examinations: DinosaurExamination[] = [];
162 private vaccinations: DinosaurVaccination[] = [];
163
164 @LogOperation
165 @ValidateExaminationDate
166 registerExamination(
167 examinationDate: Date,
168 dinosaurId: string,
169 temperature: number,
170 weight: number,
171 length: number,
172 healthStatus: "Healthy" | "Light symptoms" | "Sick" | "Critical",
173 notes?: string,
174 examiner: string = "Dr. Harding"
175 ): DinosaurExamination {
176 const results: ExaminationResults = {
177 temperature,
178 weight,
179 length,
180 healthStatus,
181 notes
182 };
183
184 const examination = new DinosaurExamination(dinosaurId, examinationDate, results, examiner);
185 this.examinations.push(examination);
186
187 console.log(`Examination registered: ${examination.examinationId} for dinosaur ${dinosaurId}`);
188
189 return examination;
190 }
191
192 @LogOperation
193 @ControlFrequency(90) // Minimum 90 days between vaccinations
194 registerVaccination(
195 dinosaurId: string,
196 vaccinationDate: Date,
197 genusVaccine: string,
198 dose: number,
199 vaccinator: string = "Dr. Harding"
200 ): DinosaurVaccination {
201 const vaccination = new DinosaurVaccination(
202 dinosaurId,
203 vaccinationDate,
204 genusVaccine,
205 dose,
206 vaccinator
207 );
208
209 this.vaccinations.push(vaccination);
210
211 console.log(`Vaccination registered: ${vaccination.vaccinationId} for dinosaur ${dinosaurId}`);
212
213 return vaccination;
214 }
215
216 @Cache
217 @LogOperation
218 getExaminationHistory(dinosaurId: string): DinosaurExamination[] {
219 console.log(`Fetching examination history for dinosaur ${dinosaurId}...`);
220
221 // Simulating a long operation
222 for (let i = 0; i < 500000; i++) {
223 // CPU load
224 }
225
226 return this.examinations.filter(examination => examination.dinosaurId === dinosaurId)
227 .sort((a, b) => b.examinationDate.getTime() - a.examinationDate.getTime());
228 }
229
230 @Cache
231 @LogOperation
232 getVaccinationHistory(dinosaurId: string): DinosaurVaccination[] {
233 console.log(`Fetching vaccination history for dinosaur ${dinosaurId}...`);
234
235 // Simulating a long operation
236 for (let i = 0; i < 500000; i++) {
237 // CPU load
238 }
239
240 return this.vaccinations.filter(vaccination => vaccination.dinosaurId === dinosaurId)
241 .sort((a, b) => b.vaccinationDate.getTime() - a.vaccinationDate.getTime());
242 }
243
244 @MeasureExecutionTime
245 @LogOperation
246 generateMedicalReport(dinosaurId: string): string {
247 const examinations = this.getExaminationHistory(dinosaurId);
248 const vaccinations = this.getVaccinationHistory(dinosaurId);
249
250 let report = `=== MEDICAL REPORT FOR DINOSAUR ${dinosaurId} ===\n\n`;
251
252 report += `Number of examinations: ${examinations.length}\n`;
253 report += `Number of vaccinations: ${vaccinations.length}\n\n`;
254
255 if (examinations.length > 0) {
256 const lastExamination = examinations[0];
257 report += `LAST EXAMINATION:\n`;
258 report += `Date: ${lastExamination.examinationDate.toLocaleString()}\n`;
259 report += `Health status: ${lastExamination.results.healthStatus}\n`;
260 report += `Temperature: ${lastExamination.results.temperature}°C\n`;
261 report += `Weight: ${lastExamination.results.weight} kg\n`;
262 report += `Length: ${lastExamination.results.length} m\n`;
263 if (lastExamination.results.notes) {
264 report += `Notes: ${lastExamination.results.notes}\n`;
265 }
266 report += "\n";
267 }
268
269 if (vaccinations.length > 0) {
270 const lastVaccination = vaccinations[0];
271 report += `LAST VACCINATION:\n`;
272 report += `Date: ${lastVaccination.vaccinationDate.toLocaleString()}\n`;
273 report += `Vaccine type: ${lastVaccination.genusVaccine}\n`;
274 report += `Dose: ${lastVaccination.dose} ml\n\n`;
275 }
276
277 report += `EXAMINATION HISTORY:\n`;
278 examinations.forEach((examination, index) => {
279 report += `${index + 1}. ${examination.examinationDate.toLocaleDateString()} - ${examination.results.healthStatus}\n`;
280 });
281
282 report += `\nVACCINATION HISTORY:\n`;
283 vaccinations.forEach((vaccination, index) => {
284 report += `${index + 1}. ${vaccination.vaccinationDate.toLocaleDateString()} - ${vaccination.genusVaccine}\n`;
285 });
286
287 return report;
288 }
289}
290
291// Testing the system
292const medicalRegistry = new MedicalRegistry();
293
294// Registering examinations and vaccinations
295try {
296 // Past examination - OK
297 const examination1 = medicalRegistry.registerExamination(
298 new Date("2023-09-01"),
299 "TREX-001",
300 38.2,
301 7500,
302 12.5,
303 "Healthy",
304 "Routine examination",
305 "Dr. Harding"
306 );
307
308 // Future examination - Error
309 const examination2 = medicalRegistry.registerExamination(
310 new Date("2025-01-01"), // future
311 "TREX-001",
312 39.1,
313 7450,
314 12.5,
315 "Light symptoms",
316 "Slight fever"
317 );
318} catch (error) {
319 console.error(error.message);
320}
321
322// Registering vaccinations
323try {
324 // First vaccination
325 const vaccination1 = medicalRegistry.registerVaccination(
326 "TREX-001",
327 new Date("2023-01-15"),
328 "Dinosaur flu vaccine",
329 50,
330 "Dr. Sattler"
331 );
332
333 // Second vaccination - too early, should throw error
334 const vaccination2 = medicalRegistry.registerVaccination(
335 "TREX-001",
336 new Date("2023-03-15"), // Only 60 days later, minimum is 90 days
337 "Anti-parasite vaccine",
338 30
339 );
340} catch (error) {
341 console.error(error.message);
342}
343
344// Testing data retrieval
345console.log(medicalRegistry.getExaminationHistory("TREX-001")); // Full fetch
346console.log(medicalRegistry.getExaminationHistory("TREX-001")); // Uses cache
347
348// Generating medical report
349console.log(medicalRegistry.generateMedicalReport("TREX-001"));Dans cet exemple élaboré, nous avons créé un système complet de gestion des examens et des vaccinations des dinosaures à Jurassic Park, en utilisant divers décorateurs :
@UniqueID- garantit que chaque examen et chaque vaccination a un identifiant unique@LogOperation- journalise les appels de méthode@ValidateExaminationDate- valide l'exactitude de la date d'examen@ControlFrequency- garantit des intervalles appropriés entre les vaccinations@Cache- met en cache les résultats des opérations longues@MeasureExecutionTime- mesure le temps de génération des rapports
Ce système montre comment les décorateurs de méthode et de propriété peuvent être utilisés pour créer des fonctionnalités métier avancées tout en garantissant la cohérence du code, la sécurité des données et la performance.
Quand utiliser les décorateurs de méthode et de propriété
Les décorateurs de méthode et de propriété sont particulièrement utiles :
- Lorsque tu veux ajouter une fonctionnalité orientée aspect - journalisation, mesure de performance, validation
- Lorsque tu veux mettre en cache des résultats - pour des méthodes coûteuses en calcul
- Lorsque tu as besoin de valider des paramètres - vérification des types, des plages, de l'exactitude
- Lorsque tu veux contrôler l'accès - autorisation, vérification des permissions
- Lorsque tu veux ajouter des métadonnées - pour des frameworks, des ORM, la sérialisation
- Lorsque tu dois surveiller des changements - suivi des modifications de propriétés
Limites et problèmes
Comme les décorateurs de classe, les décorateurs de méthode et de propriété ont leurs limites et leurs problèmes :
- Débogage plus difficile - un code comportant de nombreux décorateurs peut être plus difficile à suivre
- Problèmes de typage - peut nécessiter des conversions de type (par ex. à l'aide de
as any) - Logique cachée - l'implémentation peut être masquée dans le décorateur, rendant le code plus difficile à comprendre
- Ordre d'application - l'ordre des décorateurs a son importance et peut provoquer des bugs subtils
- Évolutions de la spécification - la spécification des décorateurs continue d'évoluer en JavaScript et TypeScript
Résumé
Les décorateurs de méthode et de propriété en TypeScript sont des outils puissants qui permettent d'ajouter des fonctionnalités supplémentaires aux méthodes et aux propriétés d'une classe de manière déclarative. Tout comme les protocoles opérationnels de Jurassic Park aident à contrôler les actions individuelles, les décorateurs de méthode et de propriété aident à contrôler et à étendre le comportement des éléments individuels d'une classe.
Grâce aux décorateurs, nous pouvons créer des applications plus modulaires, lisibles et faciles à maintenir, en isolant les fonctionnalités transversales telles que la journalisation, la validation ou la sécurité. Dans les grands projets, des décorateurs bien conçus peuvent améliorer significativement la qualité du code et réduire la redondance.
Cependant, comme tout outil avancé, les décorateurs de méthode et de propriété doivent être utilisés avec discernement. Un usage excessif des décorateurs peut conduire à un code difficile à comprendre, à déboguer et à tester. La meilleure pratique consiste à les utiliser là où ils apportent réellement des bénéfices, et non simplement pour le plaisir de les utiliser.
Code de cette leçon : index.ts
1// Décorateurs de méthodes et de propriétés - Supervision des opérations du Parc Jurassique
2// Note: Les décorateurs nécessitent "experimentalDecorators": true dans tsconfig.json
3
4// === DÉCORATEUR DE MÉTHODE - Journalisation des opérations ===
5function LogOperation(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
6 const originalMethod = descriptor.value;
7
8 descriptor.value = function (...args: any[]) {
9 console.log("[LOG] Appel: " + propertyKey + "(" + args.join(", ") + ")");
10 const startTime = Date.now();
11 const result = originalMethod.apply(this, args);
12 const duration = Date.now() - startTime;
13 console.log("[LOG] Terminé: " + propertyKey + " (" + duration + "ms)");
14 return result;
15 };
16
17 return descriptor;
18}
19
20// === DÉCORATEUR DE MÉTHODE - Validation des paramètres ===
21function ValidateSector(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
22 const originalMethod = descriptor.value;
23
24 descriptor.value = function (...args: any[]) {
25 const sectorId = args[0];
26 if (typeof sectorId !== "string" || !/^[A-Z]\d{1,2}$/.test(sectorId)) {
27 throw new Error("Secteur invalide: " + sectorId);
28 }
29 return originalMethod.apply(this, args);
30 };
31
32 return descriptor;
33}
34
35// === DÉCORATEUR DE MÉTHODE - Mémoïsation (cache) ===
36function Memoize(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
37 const originalMethod = descriptor.value;
38 const cache = new Map<string, any>();
39
40 descriptor.value = function (...args: any[]) {
41 const key = JSON.stringify(args);
42 if (cache.has(key)) {
43 console.log("[CACHE] Résultat en cache pour " + propertyKey);
44 return cache.get(key);
45 }
46 const result = originalMethod.apply(this, args);
47 cache.set(key, result);
48 return result;
49 };
50
51 return descriptor;
52}
53
54// === CLASSE AVEC DÉCORATEURS ===
55class DinosaurTracker {
56 private dinosaurs: Map<string, any> = new Map();
57
58 constructor() {
59 this.dinosaurs.set("A1", { name: "Rex", species: "T-Rex", health: 95 });
60 this.dinosaurs.set("B3", { name: "Blue", species: "Velociraptor", health: 88 });
61 this.dinosaurs.set("C2", { name: "Bronty", species: "Brachiosaurus", health: 100 });
62 }
63
64 @LogOperation
65 @ValidateSector
66 checkSector(sectorId: string): any {
67 const dino = this.dinosaurs.get(sectorId);
68 if (!dino) return { sector: sectorId, status: "vide" };
69 return { sector: sectorId, ...dino, status: "actif" };
70 }
71
72 @LogOperation
73 @Memoize
74 calculateFeedingCost(species: string, days: number): number {
75 // Simulation de calculs coûteux
76 const costs: Record<string, number> = {
77 "T-Rex": 500,
78 "Velociraptor": 200,
79 "Brachiosaurus": 300,
80 };
81 return (costs[species] || 100) * days;
82 }
83}
84
85// === TESTS ===
86const tracker = new DinosaurTracker();
87
88console.log("=== Vérification des secteurs ===");
89console.log(tracker.checkSector("A1"));
90console.log(tracker.checkSector("B3"));
91
92console.log("\n=== Coûts de nourrissage ===");
93console.log("T-Rex 7 jours:", tracker.calculateFeedingCost("T-Rex", 7));
94console.log("T-Rex 7 jours (cache):", tracker.calculateFeedingCost("T-Rex", 7));
95
96console.log("\n=== Validation du secteur ===");
97try {
98 tracker.checkSector("invalid");
99} catch (e: any) {
100 console.error("Erreur:", e.message);
101}
102
103// TODO : Crée un décorateur @ReadOnly qui empêche de redéfinir la méthode
104// TODO : Crée un décorateur @Throttle(ms) limitant la fréquence des appelsTu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quel décorateur en TypeScript reçoit le constructeur de la classe comme argument et peut l'étendre ou le remplacer ?
2. Quels trois arguments un décorateur de méthode reçoit-il en TypeScript ?
Tâches pratiques dans le jeu
- Éditeur de code
Implémente les décorateurs Trackable (classe), Authorize (méthode) et MaxLength (propriété) selon les instructions dans le code.
- Tri vertical
Ordonne les étapes de création d'un décorateur de méthode en TypeScript :