Cours JavaScript et TypeScript · Module 7: TypeScript avancé
Fabriques de décorateurs
Dans cette leçon7
« La standardisation et la réutilisation sont la clé du succès dans le parc », souligne Simon Masrani, le nouveau propriétaire de Jurassic Park. « Il nous faut des systèmes faciles à adapter à différentes espèces de dinosaures, à différentes parties du parc et à différents niveaux de sécurité, mais tous selon des standards uniformes. »
Dans le monde de TypeScript, tout comme dans la gestion de Jurassic Park, nous avons souvent besoin de créer des décorateurs similaires mais légèrement différents pour divers cas d'usage. Les fabriques de décorateurs nous permettent de créer des décorateurs hautement configurables que nous pouvons adapter à des besoins précis tout en conservant une structure et une logique cohérentes.
Introduction aux fabriques de décorateurs
Une fabrique de décorateurs est simplement une fonction qui renvoie un décorateur, en acceptant des paramètres de configuration. Cela permet de créer des décorateurs « à la demande » avec différentes variantes de comportement, à partir d'un même modèle.
Principaux avantages des fabriques de décorateurs :
- Réutilisation - on peut créer de nombreux décorateurs différents à partir d'une seule fabrique
- Configurabilité - on adapte le comportement du décorateur à des besoins précis
- Cohérence - tous les décorateurs issus d'une même fabrique partagent la même structure et le même concept
- Maintenance facilitée - une modification dans la fabrique met à jour tous les décorateurs qui en sont issus
- Typage - on peut définir les paramètres de la fabrique avec des types précis
Structure de base d'une fabrique de décorateurs
Une fabrique de décorateurs a généralement la structure suivante :
1// Class decorator factory
2function classDecoratorFactory(options: FactoryOptions) {
3 // Returns a class decorator
4 return function<T extends new (...args: any[]) => any>(target: T) {
5 // Decorator implementation using options
6 //
7 return target; // or modified class
8 };
9}
10
11// Method decorator factory
12function methodDecoratorFactory(options: FactoryOptions) {
13 // Returns a method decorator
14 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
15 // Decorator implementation using options
16 //
17 return descriptor;
18 };
19}
20
21// Property decorator factory
22function propertyDecoratorFactory(options: FactoryOptions) {
23 // Returns a property decorator
24 return function(target: any, propertyKey: string) {
25 // Decorator implementation using options
26 //
27 };
28}Exemples de fabriques de décorateurs à Jurassic Park
Voyons comment utiliser les fabriques de décorateurs dans le contexte de la gestion de Jurassic Park.
Exemple 1 : fabrique de décorateurs de journalisation pour différents sous-systèmes
1// Park subsystem types
2type ParkSubsystem = "Security" | "Enclosures" | "Laboratory" | "Transport" | "Power Supply";
3
4// Logging levels
5type LogLevel = "Debug" | "Info" | "Warning" | "Error" | "Critical";
6
7// Options interface for the logging decorator factory
8interface LoggingOptions {
9 subsystem: ParkSubsystem;
10 level: LogLevel;
11 registerCallOrder?: boolean;
12 executionTime?: boolean;
13}
14
15// Logging decorator factory
16function SystemLogging(options: LoggingOptions) {
17 const { subsystem, level, registerCallOrder = false, executionTime = false } = options;
18
19 // Call counter for each method
20 const callCounter: Record<string, number> = {};
21
22 // We return a method decorator
23 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
24 // Save the original method
25 const originalMethod = descriptor.value;
26
27 // Define a new method
28 descriptor.value = function(...args: any[]) {
29 // Record call order if needed
30 if (registerCallOrder) {
31 if (!callCounter[propertyKey]) {
32 callCounter[propertyKey] = 0;
33 }
34 callCounter[propertyKey]++;
35 console.log(`[${subsystem}] [Call #${callCounter[propertyKey]}] ${propertyKey}`);
36 }
37
38 // Initial logging
39 console.log(`[${level}] [${subsystem}] Calling ${propertyKey}(${JSON.stringify(args)})`);
40
41 // Time measurement if needed
42 const startTime = executionTime ? performance.now() : 0;
43
44 try {
45 // Call the original method
46 const result = originalMethod.apply(this, args);
47
48 // Log after successful execution
49 console.log(`[${level}] [${subsystem}] Completed ${propertyKey} - Status: Success`);
50
51 // Log execution time if needed
52 if (executionTime) {
53 const endTime = performance.now();
54 console.log(`[${subsystem}] Execution time ${propertyKey}: ${(endTime - startTime).toFixed(2)}ms`);
55 }
56
57 return result;
58 } catch (error) {
59 // Log on error
60 console.error(`[${level}] [${subsystem}] Error in ${propertyKey}: ${error.message}`);
61
62 // Propagate the error
63 throw error;
64 }
65 };
66
67 return descriptor;
68 };
69}
70
71// Class managing the enclosure system
72class EnclosuresSystem {
73 // We use the factory to create different logging decorators
74 @SystemLogging({ subsystem: "Enclosures", level: "Info" })
75 openEnclosure(enclosureId: string): boolean {
76 console.log(`Opening enclosure ${enclosureId}`);
77 return true;
78 }
79
80 @SystemLogging({ subsystem: "Enclosures", level: "Warning", executionTime: true })
81 closeEnclosure(enclosureId: string): boolean {
82 console.log(`Closing enclosure ${enclosureId}`);
83
84 // Simulate longer execution
85 for (let i = 0; i < 1000000; i++) {
86 // CPU load
87 }
88
89 return true;
90 }
91
92 @SystemLogging({ subsystem: "Enclosures", level: "Critical", registerCallOrder: true, executionTime: true })
93 securityMode(enclosureId: string, reason: string): boolean {
94 console.log(`Activating safety mode for enclosure ${enclosureId}. Reason: ${reason}`);
95
96 // Simulate complex operation
97 for (let i = 0; i < 2000000; i++) {
98 // CPU load
99 }
100
101 return true;
102 }
103}
104
105// Class managing the security system
106class SecuritySystem {
107 @SystemLogging({ subsystem: "Security", level: "Info" })
108 checkStatus(): { status: string, activeAlarms: number } {
109 return { status: "Normal", activeAlarms: 0 };
110 }
111
112 @SystemLogging({ subsystem: "Security", level: "Warning", executionTime: true })
113 activateAlarm(area: string, level: number): boolean {
114 console.log(`Activating alarm in area ${area} at level ${level}`);
115 return true;
116 }
117
118 @SystemLogging({ subsystem: "Security", level: "Critical", registerCallOrder: true })
119 evacuation(area: string): boolean {
120 console.log(`Evacuation protocol for area ${area}`);
121 return true;
122 }
123}
124
125// Using classes with decorators
126const enclosuresSystem = new EnclosuresSystem();
127const securitySystem = new SecuritySystem();
128
129// Testing
130enclosuresSystem.openEnclosure("A-001");
131enclosuresSystem.closeEnclosure("A-001");
132enclosuresSystem.securityMode("A-001", "Test protocol");
133enclosuresSystem.securityMode("A-001", "Repeat test"); // Will show this is the second call
134
135securitySystem.checkStatus();
136securitySystem.activateAlarm("Sector B", 2);
137securitySystem.evacuation("Sector B");Dans cet exemple, la fabrique de décorateurs SystemLogging crée des décorateurs de journalisation de méthodes adaptables à différents sous-systèmes, niveaux de journalisation et options supplémentaires telles que l'enregistrement de l'ordre des appels ou la mesure du temps d'exécution.
Exemple 2 : fabrique de décorateurs de validation de données
1// Options interface for the validation decorator factory
2interface ValidationOptions<T> {
3 validator: (value: T) => boolean;
4 errorMessage: string;
5 exception?: typeof Error;
6 transform?: (value: T) => T;
7}
8
9// Method parameter validation decorator factory
10function ValidateParameter<T>(options: ValidationOptions<T>) {
11 const { validator, errorMessage, exception = Error, transform } = options;
12
13 // We return a method parameter decorator
14 return function(target: Object, propertyKey: string, parameterIndex: number) {
15 // Metadata key
16 const validationKey = `validation_${propertyKey}_param_${parameterIndex}`;
17
18 // Save validation metadata
19 const validations = Reflect.getOwnMetadata(validationKey, target) || [];
20
21 // Add new validation
22 validations.push({
23 index: parameterIndex,
24 validator,
25 errorMessage,
26 exception,
27 transform
28 });
29
30 // Save updated metadata
31 Reflect.defineMetadata(validationKey, validations, target);
32
33 // Check if a method decorator already exists
34 if (!Reflect.hasMetadata(`validation_${propertyKey}_decorated`, target)) {
35 // Mark the method as already decorated
36 Reflect.defineMetadata(`validation_${propertyKey}_decorated`, true, target);
37
38 // Get the method descriptor
39 const originalMethod = target[propertyKey];
40
41 // Override the method
42 target[propertyKey] = function(...args: any[]) {
43 // Get all validations for this method
44 const validations = Reflect.getOwnMetadata(validationKey, target) || [];
45
46 // Apply validations
47 for (const validation of validations) {
48 const { index, validator, errorMessage, exception, transform } = validation;
49
50 // If the value is provided, validate it
51 if (index < args.length) {
52 const value = args[index];
53
54 // Check validation
55 if (!validator(value)) {
56 throw new exception(`Validation error for parameter ${index} in method ${propertyKey}: ${errorMessage}`);
57 }
58
59 // Apply transformation if provided
60 if (transform) {
61 args[index] = transform(value);
62 }
63 }
64 }
65
66 // Call the original method
67 return originalMethod.apply(this, args);
68 };
69 }
70 };
71}
72
73// Set of predefined validators
74// Non-empty string validator
75function NotEmpty() {
76 return ValidateParameter<string>({
77 validator: (str) => str !== null && str !== undefined && str.trim() !== '',
78 errorMessage: "Value may not be empty"
79 });
80}
81
82// Minimum length validator
83function MinLength(length: number) {
84 return ValidateParameter<string>({
85 validator: (str) => str !== null && str !== undefined && str.length >= length,
86 errorMessage: `Value must have at least ${length} characters`
87 });
88}
89
90// Numeric range validator
91function Range(min: number, max: number) {
92 return ValidateParameter<number>({
93 validator: (num) => num >= min && num <= max,
94 errorMessage: `Value must be in range from ${min} to ${max}`,
95 // Optionally we can clamp the value to the range
96 transform: (num) => Math.min(Math.max(num, min), max)
97 });
98}
99
100// Positive number validator
101function Positive() {
102 return ValidateParameter<number>({
103 validator: (num) => num > 0,
104 errorMessage: "Value must be positive"
105 });
106}
107
108// Date format validator
109function FormatDaty(format: RegExp = /^\d{4}-\d{2}-\d{2}$/) {
110 return ValidateParameter<string>({
111 validator: (str) => format.test(str),
112 errorMessage: "Value must be in date format YYYY-MM-DD"
113 });
114}
115
116// Class for managing dinosaur data
117class DinosaurManagement {
118 // Create a new dinosaur
119 addDinosaur(
120 @NotEmpty() @MinLength(3) name: string,
121 @NotEmpty() species: string,
122 @Positive() @Range(0, 100) age: number,
123 @Positive() weight: number,
124 @FormatDaty() additionDate: string
125 ): string {
126 const dinosaurId = `DINO-${Date.now()}`;
127
128 console.log(`Added new dinosaur:`);
129 console.log(`- ID: ${dinosaurId}`);
130 console.log(`- Name: ${name}`);
131 console.log(`- Species: ${species}`);
132 console.log(`- Age: ${age} years`);
133 console.log(`- Weight: ${weight} kg`);
134 console.log(`- Date added: ${additionDate}`);
135
136 return dinosaurId;
137 }
138
139 // Update dinosaur data
140 updateDinosaur(
141 @NotEmpty() dinosaurId: string,
142 @Range(0, 100) age?: number,
143 @Positive() weight?: number
144 ): boolean {
145 console.log(`Updating dinosaur ${dinosaurId}:`);
146
147 if (age !== undefined) {
148 console.log(`- New age: ${age} years`);
149 }
150
151 if (weight !== undefined) {
152 console.log(`- New weight: ${weight} kg`);
153 }
154
155 return true;
156 }
157
158 // Transfer dinosaur to a new enclosure
159 moveDinosaur(
160 @NotEmpty() dinosaurId: string,
161 @NotEmpty() toEnclosure: string,
162 @FormatDaty() transferDate: string
163 ): boolean {
164 console.log(`Transferring dinosaur ${dinosaurId} to enclosure ${toEnclosure} on ${transferDate}`);
165 return true;
166 }
167}
168
169// Testing the class with parameter validation
170const dinoManagement = new DinosaurManagement();
171
172try {
173 // Correct call
174 dinoManagement.addDinosaur(
175 "Blue",
176 "Velociraptor",
177 5,
178 150,
179 "2023-09-15"
180 );
181
182 // Incorrect call - empty name
183 dinoManagement.addDinosaur(
184 "", // Empty name
185 "Tyrannosaurus",
186 8,
187 7500,
188 "2023-09-15"
189 );
190} catch (error) {
191 console.error(error.message);
192}
193
194try {
195 // Correct call
196 dinoManagement.updateDinosaur("DINO-123", 6, 160);
197
198 // Incorrect call - negative weight
199 dinoManagement.updateDinosaur("DINO-123", 6, -100);
200} catch (error) {
201 console.error(error.message);
202}
203
204try {
205 // Correct call
206 dinoManagement.moveDinosaur("DINO-123", "B-002", "2023-09-20");
207
208 // Incorrect call - invalid date format
209 dinoManagement.moveDinosaur("DINO-123", "B-003", "20/09/2023");
210} catch (error) {
211 console.error(error.message);
212}Dans cet exemple, nous avons créé une fabrique de décorateurs de validation de paramètres qui permet de créer différents validateurs tels que NotEmpty, MinLength, Range, Positive et FormatDaty. Grâce à cela, nous pouvons facilement appliquer une validation aux paramètres des méthodes de manière déclarative.
Exemple 3 : fabrique de décorateurs de permissions
1// Park permission levels
2enum PermissionLevel {
3 Guest = 0,
4 Employee = 1,
5 Manager = 2,
6 Administrator = 3
7}
8
9// System actions
10type SystemAction = "read" | "record" | "deletion" | "administration";
11
12// Mapping permission levels to allowed actions
13const permissionsMap: Record<PermissionLevel, SystemAction[]> = {
14 [PermissionLevel.Guest]: ["read"],
15 [PermissionLevel.Employee]: ["read", "record"],
16 [PermissionLevel.Manager]: ["read", "record", "deletion"],
17 [PermissionLevel.Administrator]: ["read", "record", "deletion", "administration"]
18};
19
20// Security context interface
21interface SecurityContext {
22 user: string;
23 permissionLevel: PermissionLevel;
24 tokens?: string[];
25}
26
27// Permission denied error
28class PermissionError extends Error {
29 constructor(action: string, requiredLevel: PermissionLevel, currentLevel: PermissionLevel) {
30 super(`No permission for action ${action}. Required level: ${PermissionLevel[requiredLevel]}, current: ${PermissionLevel[currentLevel]}`);
31 this.name = "PermissionError";
32 }
33}
34
35// Permission decorator factory
36function RequiresPermission(requiredAction: SystemAction, errorMessage?: string) {
37 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
38 const originalMethod = descriptor.value;
39
40 descriptor.value = function(this: { securityContext?: SecurityContext }, ...args: any[]) {
41 // Check if we have a security context
42 if (!this.securityContext) {
43 throw new Error("Missing security context");
44 }
45
46 const { permissionLevel } = this.securityContext;
47
48 // Check if the user has the action for their permission level
49 const allowedActions = permissionsMap[permissionLevel] || [];
50
51 if (!allowedActions.includes(requiredAction)) {
52 // Find the minimum permission level needed for this action
53 let minimumLevel: PermissionLevel = PermissionLevel.Administrator;
54
55 for (const [level, actions] of Object.entries(permissionsMap)) {
56 if (actions.includes(requiredAction) && Number(level) < minimumLevel) {
57 minimumLevel = Number(level) as PermissionLevel;
58 }
59 }
60
61 throw new PermissionError(
62 requiredAction,
63 minimumLevel,
64 permissionLevel
65 );
66 }
67
68 // Call the original method
69 return originalMethod.apply(this, args);
70 };
71
72 return descriptor;
73 };
74}
75
76// Simplified versions for frequently used levels
77function ForGuests() {
78 return RequiresPermission("read");
79}
80
81function ForEmployees() {
82 return RequiresPermission("record");
83}
84
85function ForManagers() {
86 return RequiresPermission("deletion");
87}
88
89function ForAdministrators() {
90 return RequiresPermission("administration");
91}
92
93// Park management system class
94class ParkManagementSystemClass {
95 securityContext?: SecurityContext;
96
97 constructor() {
98 // No security context by default
99 }
100
101 // Method for setting the security context
102 login(user: string, permissionLevel: PermissionLevel): void {
103 this.securityContext = { user, permissionLevel };
104 console.log(`User ${user} logged in with permission level: ${PermissionLevel[permissionLevel]}`);
105 }
106
107 // Method accessible to everyone
108 @ForGuests()
109 getPublicInfo(): string {
110 return "Public park information";
111 }
112
113 // Method accessible to employees and above
114 @ForEmployees()
115 updateDinosaurData(dinosaurId: string, data: any): boolean {
116 console.log(`Updating dinosaur data ${dinosaurId}`);
117 return true;
118 }
119
120 // Method accessible to managers and above
121 @ForManagers()
122 removeDinosaur(dinosaurId: string): boolean {
123 console.log(`Deleting dinosaur ${dinosaurId}`);
124 return true;
125 }
126
127 // Method accessible only to administrators
128 @ForAdministrators()
129 configurationChange(key: string, value: any): boolean {
130 console.log(`Configuration change: ${key} = ${value}`);
131 return true;
132 }
133
134 // Method using the factory directly
135 @RequiresPermission("administration", "Only administrators can restart system")
136 restartSystem(): boolean {
137 console.log("Restarting system...");
138 return true;
139 }
140}
141
142// Testing the permission system
143const system = new ParkManagementSystemClass();
144
145// Test for guest
146console.log("--- Test for guest ---");
147system.login("JohnVisitor", PermissionLevel.Guest);
148
149try {
150 console.log(system.getPublicInfo()); // Should work
151 system.updateDinosaurData("DINO-123", { age: 5 }); // Should throw error
152} catch (error) {
153 console.error(error.message);
154}
155
156// Test for employee
157console.log("\n--- Test for employee ---");
158system.login("JaneEmployee", PermissionLevel.Employee);
159
160try {
161 console.log(system.getPublicInfo()); // Should work
162 system.updateDinosaurData("DINO-123", { age: 5 }); // Should work
163 system.removeDinosaur("DINO-123"); // Should throw error
164} catch (error) {
165 console.error(error.message);
166}
167
168// Test for manager
169console.log("\n--- Test for manager ---");
170system.login("BobManager", PermissionLevel.Manager);
171
172try {
173 system.removeDinosaur("DINO-123"); // Should work
174 system.configurationChange("maxDino", 100); // Should throw error
175} catch (error) {
176 console.error(error.message);
177}
178
179// Test for administrator
180console.log("\n--- Test for administrator ---");
181system.login("AliceAdmin", PermissionLevel.Administrator);
182
183try {
184 system.configurationChange("maxDino", 100); // Should work
185 system.restartSystem(); // Should work
186} catch (error) {
187 console.error(error.message);
188}Dans cet exemple, nous avons créé une fabrique de décorateurs de permissions qui permet de contrôler l'accès aux méthodes en fonction des niveaux de permission de l'utilisateur. Sur cette base, nous avons créé des décorateurs simplifiés pour les niveaux de permission fréquemment utilisés, tels que ForGuests, ForEmployees, ForManagers et ForAdministrators.
Exemple 4 : fabrique de décorateurs de gestion des ressources
1// Park resource types
2type ResourceType = "dinosaur" | "enclosure" | "building" | "vehicle" | "equipment";
3
4// Resource usage tracking interface
5interface TrackedUsage {
6 resourceId: string;
7 type: ResourceType;
8 startTime: Date;
9 endTime?: Date;
10 user: string;
11 status: "inUse" | "completed" | "cancelled";
12}
13
14// Global resource usage registry
15class ResourceUsageRegistry {
16 private static instance: ResourceUsageRegistry;
17 private history: TrackedUsage[] = [];
18 private activeUsages: Map<string, TrackedUsage> = new Map();
19
20 private constructor() {}
21
22 // Method to get the singleton
23 static getInstance(): ResourceUsageRegistry {
24 if (!ResourceUsageRegistry.instance) {
25 ResourceUsageRegistry.instance = new ResourceUsageRegistry();
26 }
27 return ResourceUsageRegistry.instance;
28 }
29
30 // Method to start tracking
31 startTracking(resourceId: string, type: ResourceType, user: string): string {
32 const usageId = `USE-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
33
34 const tracking: TrackedUsage = {
35 resourceId,
36 type,
37 startTime: new Date(),
38 user,
39 status: "inUse"
40 };
41
42 this.activeUsages.set(usageId, tracking);
43
44 console.log(`Started tracking resource ${type} ${resourceId} by ${user}`);
45
46 return usageId;
47 }
48
49 // Method to stop tracking
50 endTracking(usageId: string, success: boolean = true): boolean {
51 if (!this.activeUsages.has(usageId)) {
52 console.warn(`No active tracking found for ID: ${usageId}`);
53 return false;
54 }
55
56 const tracking = this.activeUsages.get(usageId)!;
57 tracking.endTime = new Date();
58 tracking.status = success ? "completed" : "cancelled";
59
60 this.history.push(tracking);
61 this.activeUsages.delete(usageId);
62
63 console.log(`Stopped tracking resource ${tracking.type} ${tracking.resourceId} by ${tracking.user} with status: ${tracking.status}`);
64
65 return true;
66 }
67
68 // Method to check if a resource is currently in use
69 isResourceInUse(resourceId: string, type: ResourceType): boolean {
70 for (const [usageId, tracking] of this.activeUsages.entries()) {
71 if (tracking.resourceId === resourceId && tracking.type === type) {
72 return true;
73 }
74 }
75 return false;
76 }
77
78 // Method to get resource usage history
79 getResourceHistory(resourceId: string, type: ResourceType): TrackedUsage[] {
80 return this.history.filter(tracking =>
81 tracking.resourceId === resourceId && tracking.type === type
82 );
83 }
84
85 // Method to get the number of active usages
86 countActiveUses(): number {
87 return this.activeUsages.size;
88 }
89}
90
91// Options interface for the resource tracking decorator factory
92interface ResourceTrackingOptions {
93 resourceType: ResourceType;
94 checkAccess?: boolean;
95 resourceIdIndex?: number;
96 unavailabilityMessage?: string;
97}
98
99// Resource tracking decorator factory
100function TrackedResourceUsage(options: ResourceTrackingOptions) {
101 const {
102 resourceType,
103 checkAccess = false,
104 resourceIdIndex = 0,
105 unavailabilityMessage = "Resource is currently unavailable"
106 } = options;
107
108 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
109 const originalMethod = descriptor.value;
110
111 descriptor.value = function(...args: any[]) {
112 // Get the resource ID from parameters
113 const resourceId = args[resourceIdIndex];
114
115 if (!resourceId) {
116 throw new Error(`No resource ID provided (parameter #${resourceIdIndex})`);
117 }
118
119 // Get the registry
120 const registry = ResourceUsageRegistry.getInstance();
121
122 // If availability check is needed
123 if (checkAccess && registry.isResourceInUse(resourceId, resourceType)) {
124 throw new Error(`${unavailabilityMessage} (${resourceType} ${resourceId})`);
125 }
126
127 // Get the username from the object if available
128 const user = this.user || "system";
129
130 // Start tracking
131 const usageId = registry.startTracking(resourceId, resourceType, user);
132
133 try {
134 // Call the original method
135 const result = originalMethod.apply(this, args);
136
137 // End tracking with success
138 registry.endTracking(usageId, true);
139
140 return result;
141 } catch (error) {
142 // End tracking with error
143 registry.endTracking(usageId, false);
144
145 // Propagate the error
146 throw error;
147 }
148 };
149
150 return descriptor;
151 };
152}
153
154// Helper functions for frequently used resource types
155function TrackedDinosaurUsage(options: Omit<ResourceTrackingOptions, "resourceType"> = {}) {
156 return TrackedResourceUsage({ ...options, resourceType: "dinosaur" });
157}
158
159function TrackedEnclosureUsage(options: Omit<ResourceTrackingOptions, "resourceType"> = {}) {
160 return TrackedResourceUsage({ ...options, resourceType: "enclosure" });
161}
162
163function TrackedVehicleUsage(options: Omit<ResourceTrackingOptions, "resourceType"> = {}) {
164 return TrackedResourceUsage({
165 ...options,
166 resourceType: "vehicle",
167 checkAccess: true,
168 unavailabilityMessage: "Vehicle is currently in use"
169 });
170}
171
172function TrackedEquipmentUsage(options: Omit<ResourceTrackingOptions, "resourceType"> = {}) {
173 return TrackedResourceUsage({
174 ...options,
175 resourceType: "equipment",
176 checkAccess: true,
177 unavailabilityMessage: "Equipment is currently in use"
178 });
179}
180
181// Park resource management class
182class ParkResourcesManagement {
183 user: string;
184
185 constructor(user: string) {
186 this.user = user;
187 }
188
189 // Tracking dinosaur examination
190 @TrackedDinosaurUsage()
191 conductResearchDinosaur(dinosaurId: string): { status: string, temperature: number } {
192 console.log(`Performing examination of dinosaur ${dinosaurId}`);
193
194 // Simulate long process
195 for (let i = 0; i < 1000000; i++) {
196 // CPU load
197 }
198
199 return { status: "healthy", temperature: 38.5 };
200 }
201
202 // Tracking dinosaur feeding
203 @TrackedDinosaurUsage()
204 feedDinosaur(dinosaurId: string, foodType: string, amount: number): boolean {
205 console.log(`Feeding dinosaur ${dinosaurId} with ${foodType} (${amount} kg)`);
206
207 // Simulate process
208 for (let i = 0; i < 500000; i++) {
209 // CPU load
210 }
211
212 return true;
213 }
214
215 // Tracking enclosure inspection
216 @TrackedEnclosureUsage()
217 inspectEnclosure(enclosureId: string): { status: string, problems: string[] } {
218 console.log(`Inspecting enclosure ${enclosureId}`);
219
220 // Simulate inspection
221 for (let i = 0; i < 800000; i++) {
222 // CPU load
223 }
224
225 return { status: "OK", problems: [] };
226 }
227
228 // Tracking vehicle usage
229 @TrackedVehicleUsage()
230 useVehicle(vehicleId: string, purpose: string): boolean {
231 console.log(`Using vehicle ${vehicleId} for destination: ${purpose}`);
232
233 // Simulate driving
234 for (let i = 0; i < 1500000; i++) {
235 // CPU load
236 }
237
238 return true;
239 }
240
241 // Tracking equipment usage
242 @TrackedEquipmentUsage({ resourceIdIndex: 1 })
243 useEquipment(purpose: string, equipmentId: string): boolean {
244 console.log(`Using equipment ${equipmentId} for purpose: ${purpose}`);
245
246 // Simulate usage
247 for (let i = 0; i < 700000; i++) {
248 // CPU load
249 }
250
251 return true;
252 }
253
254 // Direct factory usage with custom options
255 @TrackedResourceUsage({
256 resourceType: "building",
257 checkAccess: true,
258 resourceIdIndex: 0,
259 unavailabilityMessage: "Building is currently under maintenance"
260 })
261 performBuildingMaintenance(buildingId: string, maintenanceType: string): boolean {
262 console.log(`Maintenance of building ${buildingId}: ${maintenanceType}`);
263
264 // Simulate maintenance
265 for (let i = 0; i < 2000000; i++) {
266 // CPU load
267 }
268
269 return true;
270 }
271}
272
273// Testing the resource tracking system
274async function testResourceTracking() {
275 const employee1 = new ParkResourcesManagement("John");
276 const employee2 = new ParkResourcesManagement("Alice");
277
278 // Test dinosaur examination
279 console.log("--- Test: Dinosaur examination ---");
280 await employee1.conductResearchDinosaur("DINO-001");
281
282 // Test dinosaur feeding
283 console.log("\n--- Test: Dinosaur feeding ---");
284 await employee2.feedDinosaur("DINO-001", "meat", 50);
285
286 // Test enclosure inspection
287 console.log("\n--- Test: Enclosure inspection ---");
288 await employee1.inspectEnclosure("ZAG-001");
289
290 // Test vehicle usage
291 console.log("\n--- Test: Vehicle usage ---");
292
293 try {
294 // First employee uses the vehicle
295 console.log("Employee 1 uses the vehicle...");
296 const promise1 = employee1.useVehicle("JEEP-001", "Round park");
297
298 // We try to use the same vehicle by another employee
299 console.log("Attempt to use the same vehicle by Employee 2...");
300 const promise2 = employee2.useVehicle("JEEP-001", "Transport");
301
302 await Promise.all([promise1, promise2]);
303 } catch (error) {
304 console.error(`Error: ${error.message}`);
305 }
306
307 // Test equipment usage
308 console.log("\n--- Test: Equipment usage ---");
309 await employee1.useEquipment("Examination", "EQUIP-001");
310
311 // Test building maintenance
312 console.log("\n--- Test: Building maintenance ---");
313 await employee2.performBuildingMaintenance("BLD-001", "Electrical");
314
315 // Check number of active usages
316 const registry = ResourceUsageRegistry.getInstance();
317 console.log(`\nActive usages: ${registry.countActiveUses()}`);
318
319 // Check resource history
320 console.log("\nHistory for dinosaur DINO-001:");
321 const dinoHistory = registry.getResourceHistory("DINO-001", "dinosaur");
322 console.log(`Number of entries: ${dinoHistory.length}`);
323 dinoHistory.forEach((entry, index) => {
324 console.log(`[${index + 1}] ${entry.user}: ${new Date(entry.startTime).toLocaleTimeString()} - ${entry.endTime ? new Date(entry.endTime).toLocaleTimeString() : 'in progress'}`);
325 });
326}
327
328// Run the test
329testResourceTracking();Dans cet exemple, nous avons créé une fabrique de décorateurs pour suivre l'utilisation des ressources à Jurassic Park. La fabrique TrackedResourceUsage crée des décorateurs qui enregistrent automatiquement le début et la fin de l'utilisation d'une ressource, et peuvent aussi vérifier sa disponibilité. Sur cette base, nous avons créé des décorateurs simplifiés pour les types de ressources fréquemment utilisés, tels que TrackedDinosaurUsage, TrackedEnclosureUsage, TrackedVehicleUsage et TrackedEquipmentUsage.
Combiner des fabriques de décorateurs
L'un des plus grands avantages des fabriques de décorateurs est la possibilité de les combiner, ce qui permet de créer des solutions plus complexes et flexibles.
1// Combining decorator factories
2function FullTracking(resourceType: ResourceType, logLevel: LogLevel = "Info") {
3 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
4 // First apply logging
5 SystemLogging({
6 subsystem: resourceType === "dinosaur" ? "Dinosaurs" :
7 resourceType === "enclosure" ? "Enclosures" :
8 resourceType === "vehicle" ? "Transport" :
9 resourceType === "equipment" ? "Equipment" : "System",
10 level: logLevel,
11 executionTime: true
12 })(target, propertyKey, descriptor);
13
14 // Then apply resource tracking
15 return TrackedResourceUsage({
16 resourceType,
17 checkAccess: resourceType === "vehicle" || resourceType === "equipment"
18 })(target, propertyKey, descriptor);
19 };
20}
21
22// Example of using combined factories
23class ParkManagementV2 {
24 user: string;
25 securityContext?: SecurityContext;
26
27 constructor(user: string, permissionLevel: PermissionLevel) {
28 this.user = user;
29 this.securityContext = { user, permissionLevel };
30 }
31
32 // Combining resource tracking, logging, and permissions
33 @ForEmployees()
34 @FullTracking("dinosaur", "Info")
35 takeCareOfDinosaur(dinosaurId: string): boolean {
36 console.log(`Taking care of dinosaur ${dinosaurId}`);
37 return true;
38 }
39
40 // Combining resource tracking, logging, and permissions with a higher level
41 @ForManagers()
42 @FullTracking("enclosure", "Warning")
43 modifyEnclosureSettings(enclosureId: string, settings: any): boolean {
44 console.log(`Modifying enclosure settings ${enclosureId}`);
45 return true;
46 }
47
48 // Combining all necessary decorators for a critical function
49 @ForAdministrators()
50 @FullTracking("equipment", "Critical")
51 configureSecuritySystems(equipmentId: string, parameters: any): boolean {
52 console.log(`Configuring security systems using ${equipmentId}`);
53 return true;
54 }
55}
56
57// Test combining decorator factories
58function testFactoryChaining() {
59 console.log("=== Test: Combining decorator factories ===");
60
61 const employee = new ParkManagementV2("Mary", PermissionLevel.Employee);
62 const manager = new ParkManagementV2("Bob", PermissionLevel.Manager);
63 const admin = new ParkManagementV2("Alice", PermissionLevel.Administrator);
64
65 // Test employee
66 console.log("\n--- Test: Employee ---");
67 try {
68 employee.takeCareOfDinosaur("DINO-002"); // Should work
69 employee.modifyEnclosureSettings("ZAG-002", { temperature: 30 }); // Should throw error
70 } catch (error) {
71 console.error(error.message);
72 }
73
74 // Test manager
75 console.log("\n--- Test: Manager ---");
76 try {
77 manager.modifyEnclosureSettings("ZAG-002", { temperature: 30 }); // Should work
78 manager.configureSecuritySystems("SEC-001", { level: "high" }); // Should throw error
79 } catch (error) {
80 console.error(error.message);
81 }
82
83 // Test administrator
84 console.log("\n--- Test: Administrator ---");
85 try {
86 admin.configureSecuritySystems("SEC-001", { level: "high" }); // Should work
87 } catch (error) {
88 console.error(error.message);
89 }
90}
91
92// Run the test
93testFactoryChaining();Dans cet exemple, nous montrons comment différentes fabriques de décorateurs peuvent être combinées pour créer des solutions complètes. La fonction FullTracking combine les décorateurs de journalisation et de suivi des ressources, puis elle est utilisée conjointement avec des décorateurs de permissions.
Bonnes pratiques pour créer des fabriques de décorateurs
1. Maintenir une cohérence structurelle
Assure-toi que toutes les fabriques de décorateurs ont une structure et une convention de nommage similaires. Ainsi, le code sera plus lisible et plus prévisible.
1// Consistent decorator factory structure
2function DataLogging(options: LoggingOptions) { /* ... */ }
3function DataValidation(options: ValidationOptions) { /* ... */ }
4function DataTracking(options: TrackingOptions) { /* ... */ }2. Utiliser des types appropriés pour les paramètres
Spécifie toujours des types précis pour les paramètres de la fabrique afin de garantir la sécurité des types et le support de l'IDE.
1// Good typing
2interface ValidationOptions<T> {
3 validator: (value: T) => boolean;
4 message: string;
5}
6
7function Validate<T>(options: ValidationOptions<T>) {
8 //
9}3. Fournir des valeurs par défaut sensées
Les fabriques de décorateurs devraient avoir des valeurs par défaut raisonnables pour les options non critiques.
1function LogOperation(options: Partial<LoggingOptions> = {}) {
2 const {
3 level = "Info",
4 detailed = false,
5 executionTime = false
6 } = options;
7
8 //
9}4. Créer des versions simplifiées pour les cas d'usage courants
Pour les configurations fréquemment utilisées, crée des fabriques spécialisées qui facilitent l'utilisation des décorateurs.
1// Specialized versions for different logging levels
2function LogDebug(detailed: boolean = false) {
3 return LogOperation({ level: "Debug", detailed });
4}
5
6function LogInfo() {
7 return LogOperation({ level: "Info" });
8}
9
10function LogWarning() {
11 return LogOperation({ level: "Warning", executionTime: true });
12}
13
14function LogError() {
15 return LogOperation({ level: "Error", detailed: true, executionTime: true });
16}5. Documenter tes fabriques
Des fabriques de décorateurs bien documentées sont plus faciles à utiliser.
1/**
2 * Creates a decorator that validates a method parameter.
3 *
4 * @param options - Validation options
5 * @param options.validator - Function that checks value correctness
6 * @param options.message - Error message for invalid values
7 * @param options.transform - Optional function to transform the value
8 *
9 * @returns A method parameter decorator
10 *
11 * @example
12 * class Example {
13 * method(@Validate({ validator: (x) => x > 0, message: "Must be positive" }) count: number) {
14 * //
15 * }
16 * }
17 */
18function Validate<T>(options: ValidationOptions<T>) {
19 //
20}6. Éviter les dépendances cachées
Les fabriques de décorateurs devraient rendre explicites leurs dépendances.
1// Bad - hidden dependency
2function LogToFile(filePath: string) {
3 // Directly uses the file system
4 const fs = require('fs');
5 //
6}
7
8// Good - explicit dependency
9function LogToFile(logger: FileLogger) {
10 // Logger is passed as a parameter
11 //
12}7. Tester tes fabriques de décorateurs
Écris des tests unitaires pour tes fabriques de décorateurs afin de t'assurer qu'elles fonctionnent comme prévu.
1describe('ValidateRange', () => {
2 it('should pass a value within range', () => {
3 //
4 });
5
6 it('should throw an error for a value out of range', () => {
7 //
8 });
9
10 it('should clamp the value to range when transformation is enabled', () => {
11 //
12 });
13});Cas d'usage des fabriques de décorateurs
Les fabriques de décorateurs sont particulièrement utiles dans les cas suivants :
- Frameworks et bibliothèques - créer des décorateurs configurables pour les utilisateurs d'un framework
- Préoccupations transversales - journalisation, validation, sécurité qui doivent être adaptées à de nombreux cas
- Configuration de l'environnement - adapter le comportement des décorateurs à différents environnements (développement, production)
- Gestion des ressources - surveillance, suivi et contrôle de l'accès aux ressources
- Collecte de métriques - configurer la façon dont les métriques sont collectées selon différents paramètres
- Contrôle d'accès - créer des décorateurs de permissions pour différents niveaux et rôles
Résumé
Les fabriques de décorateurs en TypeScript sont un outil puissant qui permet de créer des décorateurs flexibles, configurables et réutilisables. Tout comme les systèmes de Jurassic Park doivent être adaptés à différents besoins et exigences tout en respectant des standards de sécurité uniformes, les fabriques de décorateurs permettent de créer des décorateurs cohérents mais spécifiques à chaque cas.
Grâce aux fabriques de décorateurs, nous pouvons :
- Améliorer la réutilisation du code - en créant des modèles de décorateurs configurables
- Augmenter la lisibilité - en masquant une logique complexe derrière une interface simple et déclarative
- Garantir la cohérence - en appliquant les mêmes motifs et structures dans toute l'application
- Augmenter la flexibilité - en adaptant les décorateurs à différents cas d'usage
Les fabriques de décorateurs sont une technique TypeScript avancée qui demande une bonne compréhension des décorateurs, mais elles offrent d'énormes bénéfices en matière d'organisation, de réutilisation et de lisibilité du code. Bien conçues, elles deviennent des outils incroyablement précieux dans ton arsenal de programmation.
Code de cette leçon : index.ts
1// Fabriques de décorateurs - Systèmes configurables du Parc Jurassique
2// Fabrique de décorateurs = fonction retournant un décorateur avec des paramètres
3
4// === FABRIQUE: Contrôle d'accès par rôle ===
5function RequireRole(requiredRole: string) {
6 return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
7 const originalMethod = descriptor.value;
8
9 descriptor.value = function (...args: any[]) {
10 const userRole = (this as any).currentUserRole || "guest";
11 if (userRole !== requiredRole && userRole !== "admin") {
12 console.error("[ACCÈS REFUSÉ] " + propertyKey + " nécessite le rôle: " + requiredRole);
13 return null;
14 }
15 console.log("[AUTORISATION] " + userRole + " -> " + propertyKey + " OK");
16 return originalMethod.apply(this, args);
17 };
18
19 return descriptor;
20 };
21}
22
23// === FABRIQUE: Limite de temps d'exécution ===
24function Timeout(maxMs: number) {
25 return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
26 const originalMethod = descriptor.value;
27
28 descriptor.value = function (...args: any[]) {
29 const start = Date.now();
30 const result = originalMethod.apply(this, args);
31 const elapsed = Date.now() - start;
32
33 if (elapsed > maxMs) {
34 console.warn("[TIMEOUT] " + propertyKey + " a duré " + elapsed + "ms (limite: " + maxMs + "ms)");
35 }
36 return result;
37 };
38
39 return descriptor;
40 };
41}
42
43// === FABRIQUE: Répétition des opérations ===
44function Retry(attempts: number, delayLabel: string = "retry") {
45 return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
46 const originalMethod = descriptor.value;
47
48 descriptor.value = function (...args: any[]) {
49 let lastError: Error | null = null;
50
51 for (let i = 1; i <= attempts; i++) {
52 try {
53 const result = originalMethod.apply(this, args);
54 if (i > 1) console.log("[" + delayLabel.toUpperCase() + "] Succès après " + i + " tentative(s)");
55 return result;
56 } catch (error: any) {
57 lastError = error;
58 console.warn("[" + delayLabel.toUpperCase() + "] Tentative " + i + "/" + attempts + " échouée: " + error.message);
59 }
60 }
61
62 throw lastError;
63 };
64
65 return descriptor;
66 };
67}
68
69// === FABRIQUE: Validation des paramètres ===
70function ValidateParams(validators: Record<number, (val: any) => boolean>) {
71 return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
72 const originalMethod = descriptor.value;
73
74 descriptor.value = function (...args: any[]) {
75 for (const [indexStr, validator] of Object.entries(validators)) {
76 const index = Number(indexStr);
77 if (!validator(args[index])) {
78 throw new Error("Validation du paramètre #" + index + " échouée dans " + propertyKey);
79 }
80 }
81 return originalMethod.apply(this, args);
82 };
83
84 return descriptor;
85 };
86}
87
88// === CLASSE AVEC FABRIQUES DE DÉCORATEURS ===
89class ParkSecuritySystem {
90 currentUserRole: string = "admin";
91 private failCount: number = 0;
92
93 @RequireRole("admin")
94 @Timeout(100)
95 initiateLockdown(sector: string): string {
96 return "LOCKDOWN activé dans le secteur " + sector;
97 }
98
99 @RequireRole("scientist")
100 checkDNA(sampleId: string): string {
101 return "Analyse ADN de l'échantillon " + sampleId + " terminée";
102 }
103
104 @Retry(3, "sensor")
105 readSensor(sensorId: string): number {
106 this.failCount++;
107 if (this.failCount % 3 !== 0) {
108 throw new Error("Sensor " + sensorId + " ne répond pas");
109 }
110 return Math.floor(Math.random() * 100);
111 }
112
113 @ValidateParams({ 0: (v: any) => typeof v === "string" && v.length >= 3 })
114 registerDinosaur(name: string): string {
115 return "Dinosaure enregistré: " + name;
116 }
117}
118
119// === TESTS ===
120const security = new ParkSecuritySystem();
121
122console.log("=== Fabriques de décorateurs ===\n");
123
124// Test RequireRole
125console.log(security.initiateLockdown("A1"));
126security.currentUserRole = "guest";
127security.checkDNA("DNA-001");
128security.currentUserRole = "admin";
129
130// Test Retry
131console.log("\n=== Lecture du sensor (avec répétition) ===");
132try {
133 const reading = security.readSensor("S-042");
134 console.log("Lecture:", reading);
135} catch (e: any) {
136 console.error("Erreur finale:", e.message);
137}
138
139// Test ValidateParams
140console.log("\n=== Validation ===");
141console.log(security.registerDinosaur("Rex"));
142try {
143 security.registerDinosaur("AB"); // nom trop court
144} catch (e: any) {
145 console.error("Erreur:", e.message);
146}
147
148// TODO : Crée une fabrique @Cache(ttlMs) avec expiration
149// TODO : Crée une fabrique @RateLimit(maxCalls, windowMs)Tu 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. Qu'est-ce qu'une fabrique de décorateurs en TypeScript ?
2. Que se passe-t-il lorsqu'on déclare deux interfaces portant le même nom en TypeScript ?
Tâches pratiques dans le jeu
- Éditeur de code
Crée un décorateur Cache (mémorisation des résultats), un décorateur Retry (nouvelles tentatives) et un décorateur Deprecated (journalisation d'un avertissement) selon les instructions.
- Clique dans l'ordre
Clique dans le bon ordre : lorsqu'une classe possède plusieurs décorateurs (@A @B @C class X), dans quel ordre les étapes se déroulent-elles ?
- Tri horizontal
Range la configuration correcte de tsconfig.json qui active les décorateurs :