Cours JavaScript et TypeScript · Module 7: TypeScript avancé

Fabriques de décorateurs

23 min de lecture
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 :

  1. Réutilisation - on peut créer de nombreux décorateurs différents à partir d'une seule fabrique
  2. Configurabilité - on adapte le comportement du décorateur à des besoins précis
  3. Cohérence - tous les décorateurs issus d'une même fabrique partagent la même structure et le même concept
  4. Maintenance facilitée - une modification dans la fabrique met à jour tous les décorateurs qui en sont issus
  5. 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 :

  1. Frameworks et bibliothèques - créer des décorateurs configurables pour les utilisateurs d'un framework
  2. Préoccupations transversales - journalisation, validation, sécurité qui doivent être adaptées à de nombreux cas
  3. Configuration de l'environnement - adapter le comportement des décorateurs à différents environnements (développement, production)
  4. Gestion des ressources - surveillance, suivi et contrôle de l'accès aux ressources
  5. Collecte de métriques - configurer la façon dont les métriques sont collectées selon différents paramètres
  6. 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 :

  1. Améliorer la réutilisation du code - en créant des modèles de décorateurs configurables
  2. Augmenter la lisibilité - en masquant une logique complexe derrière une interface simple et déclarative
  3. Garantir la cohérence - en appliquant les mêmes motifs et structures dans toute l'application
  4. 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. 1. Qu'est-ce qu'une fabrique de décorateurs en TypeScript ?

  2. 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 :

Articles utiles