Cours JavaScript et TypeScript · Module 7: TypeScript avancé

Propriétés statiques et abstraites

24 min de lecture
Dans cette leçon4

« À Jurassic Park, nous avons des protocoles et des standards qui s'appliquent à tous les objets d'une même classe », explique le Dr Henry Wu, généticien en chef du parc. « Par exemple, tous nos enclos de dinosaures suivent la même procédure de sécurité, peu importe l'enclos précis dont on parle. En revanche, chaque espèce de dinosaure possède son propre patron d'ADN unique, mais nous savons tous que l'ADN doit exister - même si les détails diffèrent d'une espèce à l'autre. »

En programmation orientée objet, TypeScript propose deux concepts puissants qui reflètent ces idées :

  • Les propriétés et méthodes statiques - partagées par toutes les instances d'une classe
  • Les propriétés et méthodes abstraites - qui doivent être implémentées par les classes qui en héritent

Dans cet exercice, nous allons explorer en profondeur ces concepts en créant un système avancé de gestion de la génétique et des protocoles de sécurité pour Jurassic Park.

Propriétés et méthodes statiques

Les éléments statiques d'une classe appartiennent à la classe elle-même, et non à ses instances. Cela signifie que :

  • Il n'existe qu'une seule copie de chaque propriété statique, quel que soit le nombre d'instances créées
  • L'accès aux propriétés statiques se fait via le nom de la classe, et non via une instance
  • Les méthodes statiques peuvent être appelées directement sur la classe

Utilisation de base des éléments statiques

Voyons comment les éléments statiques peuvent être utilisés dans le contexte de Jurassic Park :

1class SecurityProtocol {
2  // Static property - shared by all instances
3  static readonly LEVEL_CRITICAL: number = 5;
4  static readonly LEVEL_HIGH: number = 4;
5  static readonly LEVEL_MEDIUM: number = 3;
6  static readonly LEVEL_LOW: number = 2;
7  static readonly LEVEL_MINIMAL: number = 1;
8
9  // Static property storing the current global park threat level
10  private static _currentThreatLevel: number = SecurityProtocol.LEVEL_LOW;
11
12  // Instance properties
13  private readonly area: string;
14  private localThreatLevel: number;
15
16  constructor(area: string) {
17    this.area = area;
18    this.localThreatLevel = SecurityProtocol._currentThreatLevel;
19    console.log(`Created security protocol for area: ${area}`);
20  }
21
22  // Static method accessible directly on the class
23  static raiseGlobalThreatLevel(): void {
24    if (this._currentThreatLevel < this.LEVEL_CRITICAL) {
25      this._currentThreatLevel++;
26      console.log(`WARNING: Raised global threat level to ${this._currentThreatLevel}`);
27
28      // Calling another static method
29      this.notifyAboutThreatLevel();
30    } else {
31      console.log("WARNING: Maximum threat level already reached!");
32    }
33  }
34
35  // Another static method
36  static lowerGlobalThreatLevel(): void {
37    if (this._currentThreatLevel > this.LEVEL_MINIMAL) {
38      this._currentThreatLevel--;
39      console.log(`Lowered global threat level to ${this._currentThreatLevel}`);
40    } else {
41      console.log("Threat level is already at minimum.");
42    }
43  }
44
45  // Static getter
46  static get currentThreatLevel(): number {
47    return this._currentThreatLevel;
48  }
49
50  // Private static method
51  private static notifyAboutThreatLevel(): void {
52    console.log(`Notification: Current park threat level: ${this._currentThreatLevel}`);
53  }
54
55  // Instance method using static properties
56  updateLocalThreatLevel(): void {
57    // Updating the local level based on the global level
58    this.localThreatLevel = SecurityProtocol._currentThreatLevel;
59    console.log(`Area ${this.area}: updated threat level to ${this.localThreatLevel}`);
60  }
61
62  get securityStatus(): string {
63    const levels = [
64      "UNKNOWN",
65      "MINIMAL",
66      "LOW",
67      "MEDIUM",
68      "HIGH",
69      "CRITICAL"
70    ];
71
72    return `Area: ${this.area}, Threat level: ${levels[this.localThreatLevel]}`;
73  }
74}
75
76// Using static properties and methods - without creating instances!
77console.log("Default threat level:", SecurityProtocol.currentThreatLevel);
78
79// Calling a static method directly on the class
80SecurityProtocol.raiseGlobalThreatLevel();
81SecurityProtocol.raiseGlobalThreatLevel();
82
83// Creating instances of the class
84const centralProtocol = new SecurityProtocol("Operations Center");
85const enclosureProtocol = new SecurityProtocol("T-Rex Enclosure");
86
87// Displaying the security status
88console.log(centralProtocol.securityStatus);
89console.log(enclosureProtocol.securityStatus);
90
91// Raising the global threat level again
92SecurityProtocol.raiseGlobalThreatLevel();
93
94// Updating local threat levels
95centralProtocol.updateLocalThreatLevel();
96enclosureProtocol.updateLocalThreatLevel();
97
98// Checking updated statuses
99console.log(centralProtocol.securityStatus);
100console.log(enclosureProtocol.securityStatus);

Dans l'exemple ci-dessus :

  1. Les constantes de niveau de menace sont des propriétés statiques, accessibles directement via la classe
  2. La propriété statique _currentThreatLevel est partagée entre toutes les instances
  3. Les méthodes statiques peuvent modifier les propriétés statiques et appeler d'autres méthodes statiques
  4. Les instances de la classe peuvent mettre à jour leur état interne en fonction des propriétés statiques

Quand utiliser les éléments statiques

Les éléments statiques sont particulièrement utiles dans les cas suivants :

  1. Constantes et configuration - Des valeurs qui restent inchangées pour toutes les instances d'une classe
  2. État global - Lorsqu'on a besoin d'une valeur partagée par toutes les instances
  3. Méthodes utilitaires - Des fonctions auxiliaires qui n'ont pas besoin de l'état d'une instance
  4. Méthodes fabriques - Des façons alternatives de créer des instances de la classe

Voici un exemple plus élaboré montrant différents usages des éléments statiques :

1class GeneticRegistry {
2  // Static constants
3  static readonly NUCLEOTIDE_BASE = ["A", "C", "G", "T"];
4  static readonly MIN_DNA_LENGTH = 1000000;
5
6  // Global state
7  private static _registeredSpeciesCount: number = 0;
8  private static _speciesRegistrations: Map<string, string> = new Map();
9
10  // Instance properties
11  private id: string;
12  private dnaSequence: string;
13  private name: string;
14
15  constructor(name: string, dnaSequence: string) {
16    if (dnaSequence.length < GeneticRegistry.MIN_DNA_LENGTH) {
17      throw new Error(`DNA sequence is too short. Minimum required: ${GeneticRegistry.MIN_DNA_LENGTH} base pairs.`);
18    }
19
20    this.name = name;
21    this.dnaSequence = dnaSequence;
22    this.id = GeneticRegistry.generateId(name);
23
24    // Registering a new species
25    GeneticRegistry._speciesRegistrations.set(this.id, name);
26    GeneticRegistry._registeredSpeciesCount++;
27  }
28
29  // Utility method - creates an ID based on name
30  private static generateId(name: string): string {
31    const timestamp = Date.now();
32    const cleanName = name.toLowerCase().replace(/[^a-z0-9]/g, "");
33    return `DNO-${cleanName}-${timestamp}`;
34  }
35
36  // Factory method - creates an instance with random DNA
37  static createTestSpecies(name: string): GeneticRegistry {
38    const dnaLength = this.MIN_DNA_LENGTH + Math.floor(Math.random() * 500000);
39    let sequence = "";
40
41    for (let i = 0; i < dnaLength; i++) {
42      const randomIndex = Math.floor(Math.random() * this.NUCLEOTIDE_BASE.length);
43      sequence += this.NUCLEOTIDE_BASE[randomIndex];
44    }
45
46    return new GeneticRegistry(name, sequence);
47  }
48
49  // Utility method - verifies the correctness of a DNA sequence
50  static verifySequence(sequence: string): boolean {
51    if (sequence.length < this.MIN_DNA_LENGTH) return false;
52
53    // Checks if the sequence contains only allowed nucleotides
54    for (const nucleotide of sequence) {
55      if (!this.NUCLEOTIDE_BASE.includes(nucleotide)) {
56        return false;
57      }
58    }
59
60    return true;
61  }
62
63  // Getter for the static property
64  static get registeredSpeciesCount(): number {
65    return this._registeredSpeciesCount;
66  }
67
68  // Static method returning all registered species
69  static getAllSpecies(): string[] {
70    return Array.from(this._speciesRegistrations.values());
71  }
72
73  // Instance method using static fields
74  analyzeSequence(): { proportions: Record<string, number>, length: number } {
75    const proportions: Record<string, number> = {};
76
77    // Initializing counters for each nucleotide
78    GeneticRegistry.NUCLEOTIDE_BASE.forEach(n => {
79      proportions[n] = 0;
80    });
81
82    // Counting nucleotides
83    for (const nucleotide of this.dnaSequence) {
84      proportions[nucleotide]++;
85    }
86
87    // Converting to percentages
88    GeneticRegistry.NUCLEOTIDE_BASE.forEach(n => {
89      proportions[n] = Number((proportions[n] / this.dnaSequence.length * 100).toFixed(2));
90    });
91
92    return {
93      proportions,
94      length: this.dnaSequence.length
95    };
96  }
97
98  // Instance getters
99  get id(): string {
100    return this.id;
101  }
102
103  get speciesName(): string {
104    return this.name;
105  }
106}
107
108// Using static methods and properties
109console.log("Minimum DNA length:", GeneticRegistry.MIN_DNA_LENGTH);
110console.log("Available nucleotides:", GeneticRegistry.NUCLEOTIDE_BASE.join(", "));
111
112// Verifying a sample sequence
113const exampleSequence = "ACGTACGTACGT".repeat(100000); // Repeating to reach min. length
114console.log("Sequence verification:", GeneticRegistry.verifySequence(exampleSequence));
115
116// Creating instances using the factory method
117const trex = GeneticRegistry.createTestSpecies("Tyrannosaurus Rex");
118const raptor = GeneticRegistry.createTestSpecies("Velociraptor");
119const trike = GeneticRegistry.createTestSpecies("Triceratops");
120
121// Checking how many species have been registered
122console.log("Number of registered species:", GeneticRegistry.registeredSpeciesCount);
123console.log("Species list:", GeneticRegistry.getAllSpecies());
124
125// Using instance methods
126const trexAnalysis = trex.analyzeSequence();
127console.log(`Analysis: ${trex.speciesName} (ID: ${trex.id}):`);
128console.log(`Sequence length: ${trexAnalysis.length} base pairs`);
129console.log("Nucleotide proportions:", trexAnalysis.proportions);

Cet exemple illustre :

  1. L'utilisation de constantes statiques (NUCLEOTIDE_BASE, MIN_DNA_LENGTH)
  2. Un état global qui suit le nombre et l'enregistrement des espèces
  3. Des méthodes utilitaires statiques comme verifySequence et generateId
  4. Une méthode fabrique createTestSpecies qui crée des instances avec des données aléatoires
  5. Comment les méthodes d'instance peuvent utiliser les propriétés statiques

Classes et méthodes abstraites

Les classes abstraites en TypeScript définissent un modèle qui doit être implémenté par les classes qui en héritent. On ne peut pas créer directement une instance d'une classe abstraite - elle sert de « plan » de base pour d'autres classes.

Les méthodes abstraites, définies dans une classe abstraite, doivent être implémentées par les classes qui en héritent. Elles définissent un contrat précisant quelles méthodes doivent être disponibles, mais ne contiennent pas leur propre implémentation.

Les bases des classes abstraites

Voyons comment utiliser les classes abstraites dans le contexte de Jurassic Park :

1// Abstract base class for all dinosaurs
2abstract class Dinosaur {
3  // Properties common to all dinosaurs
4  private _id: string;
5  protected _name: string;
6  protected _species: string;
7  protected _age: number;
8  protected _weight: number;
9
10  constructor(id: string, name: string, species: string, age: number, weight: number) {
11    this._id = id;
12    this._name = name;
13    this._species = species;
14    this._age = age;
15    this._weight = weight;
16  }
17
18  // Getters
19  get id(): string { return this._id; }
20  get name(): string { return this._name; }
21  get species(): string { return this._species; }
22  get age(): number { return this._age; }
23  get weight(): number { return this._weight; }
24
25  // Methods implemented in all derived classes
26  abstract calculateCaloricNeeds(): number;
27  abstract determineThreatLevel(): number;
28
29  // Abstract property - must be implemented in derived classes
30  abstract get dietType(): string;
31
32  // Concrete method (not abstract) - available in all derived classes
33  getBasicData(): string {
34    return `${this._name} (${this._species}) - ID: ${this._id}, Age: ${this._age} years, Weight: ${this._weight} kg`;
35  }
36
37  // Method using abstract methods
38  generateReportDinosaur(): string {
39    const calories = this.calculateCaloricNeeds();
40    const threatLevel = this.determineThreatLevel();
41
42    return `
43=== DINOSAUR REPORT ===
44${this.getBasicData()}
45Diet type: ${this.dietType}
46Daily caloric demand: ${calories} kcal
47Threat level: ${threatLevel}/10
48======================
49    `;
50  }
51}
52
53// Class inheriting from the abstract class
54class PredatoryDinosaur extends Dinosaur {
55  private _biteForce: number;
56  private _runSpeed: number;
57
58  constructor(
59    id: string,
60    name: string,
61    species: string,
62    age: number,
63    weight: number,
64    biteForce: number,
65    runSpeed: number
66  ) {
67    super(id, name, species, age, weight);
68    this._biteForce = biteForce;
69    this._runSpeed = runSpeed;
70  }
71
72  // Getters
73  get biteForce(): number { return this._biteForce; }
74  get runSpeed(): number { return this._runSpeed; }
75
76  // Implementation of abstract property
77  get dietType(): string {
78    return "carnivore";
79  }
80
81  // Implementations of abstract methods
82  calculateCaloricNeeds(): number {
83    // For predators we use a complex formula
84    return Math.round(this._weight * 0.6 * (this._runSpeed * 0.2));
85  }
86
87  determineThreatLevel(): number {
88    // Threat level depends on bite force, speed, and weight
89    const base = (this._biteForce * 0.7 + this._runSpeed * 0.3) / 100;
90    return Math.min(10, Math.max(1, Math.round(base)));
91  }
92
93  // Method specific to predators
94  hunt(): string {
95    return `${this._name} hunts at speed ${this._runSpeed} km/h and bite force ${this._biteForce} PSI`;
96  }
97}
98
99// Another inheriting class
100class HerbivorousDinosaur extends Dinosaur {
101  private _neckLength: number;
102  private _armorType: "none" | "light" | "medium" | "heavy";
103
104  constructor(
105    id: string,
106    name: string,
107    species: string,
108    age: number,
109    weight: number,
110    neckLength: number,
111    armorType: "none" | "light" | "medium" | "heavy"
112  ) {
113    super(id, name, species, age, weight);
114    this._neckLength = neckLength;
115    this._armorType = armorType;
116  }
117
118  // Getters
119  get neckLength(): number { return this._neckLength; }
120  get armorType(): string { return this._armorType; }
121
122  // Implementation of abstract property
123  get dietType(): string {
124    return "herbivorous";
125  }
126
127  // Implementations of abstract methods
128  calculateCaloricNeeds(): number {
129    // For herbivores we use a simpler formula - they need more calories per kilogram
130    return Math.round(this._weight * 0.8);
131  }
132
133  determineThreatLevel(): number {
134    // Threat level is lower for herbivores, but depends on weight and armor type
135    let base = this._weight / 1000;
136
137    // Modifier dependent on armor
138    const armorModifiers = {
139      "none": 0,
140      "light": 1,
141      "medium": 2,
142      "heavy": 3
143    };
144
145    base += armorModifiers[this._armorType];
146
147    return Math.min(7, Math.max(1, Math.round(base))); // Max. 7 for herbivores
148  }
149
150  // Method specific to herbivores
151  graze(): string {
152    const reachHeight = this._neckLength > 2 ? "tall" : "low";
153    return `${this._name} is grazing, eating plants from ${reachHeight} tree sections`;
154  }
155}
156
157// We cannot create an instance of an abstract class
158// const invalid = new Dinosaur("ID", "Name", "Species", 1, 100); // Error!
159
160// Creating instances of derived classes
161const trex = new PredatoryDinosaur("T-REX-1", "Rex", "Tyrannosaurus", 8, 7000, 12800, 32);
162const stegosaurus = new HerbivorousDinosaur("STEG-1", "Spike", "Stegosaurus", 12, 5000, 1.2, "heavy");
163const diplodocus = new HerbivorousDinosaur("DIPL-1", "Dippy", "Diplodocus", 15, 16000, 6.5, "none");
164
165// Using methods implemented in derived classes
166console.log(trex.generateReportDinosaur());
167console.log(stegosaurus.generateReportDinosaur());
168console.log(diplodocus.generateReportDinosaur());
169
170// Using methods specific to a given class
171console.log(trex.hunt());
172console.log(stegosaurus.graze());
173
174// For both types of dinosaurs we can use base class methods
175console.log(trex.getBasicData());
176console.log(diplodocus.getBasicData());

Dans l'exemple ci-dessus :

  1. Dinosaur est une classe abstraite contenant des propriétés et méthodes communes
  2. Les méthodes abstraites calculateCaloricNeeds() et determineThreatLevel() doivent être implémentées par les classes qui en héritent
  3. La propriété abstraite dietType doit être définie dans les classes dérivées
  4. Les classes PredatoryDinosaur et HerbivorousDinosaur implémentent les méthodes et propriétés abstraites
  5. On ne peut pas créer directement une instance de la classe Dinosaur

Applications des classes et méthodes abstraites

Les classes abstraites sont particulièrement utiles dans les cas suivants :

  1. Définir une interface commune - Spécifier un ensemble cohérent de méthodes pour un groupe de classes apparentées
  2. Partager du code - Fournir une implémentation commune à des groupes de classes
  3. Imposer un contrat - Garantir que toutes les classes dérivées implémenteront les méthodes requises
  4. Créer des hiérarchies de types - Construire des relations complexes entre types

Voyons un exemple plus avancé qui combine classes et méthodes abstraites avec des éléments statiques dans un système de gestion des protocoles de sécurité de Jurassic Park :

1// Helper types
2type AlertLevel = "normall" | "elevated" | "high" | "critical";
3type StatusSystem = "active" | "inactive" | "maintenance" | "failure";
4
5// Interface for monitoring systems
6interface MonitoringSystem {
7  startMonitoring(): void;
8  stopMonitoring(): void;
9  getCurrentStatus(): StatusSystem;
10}
11
12// Abstract base class for all security systems
13abstract class SecuritySystem implements MonitoringSystem {
14  // Static properties - for all security systems
15  private static _parkAlertLevel: AlertLevel = "normall";
16  private static _activeSystems: Set<string> = new Set();
17  private static _incidents: Array<{date: Date, systemType: string, location: string, description: string}> = [];
18
19  // Regular properties
20  protected _id: string;
21  protected _location: string;
22  protected _status: StatusSystem;
23  protected _isMonitored: boolean;
24  protected _reviewDate: Date;
25
26  constructor(id: string, location: string) {
27    this._id = id;
28    this._location = location;
29    this._status = "inactive";
30    this._isMonitored = false;
31    this._reviewDate = new Date();
32
33    // System registration
34    SecuritySystem.registerSystem(id, this.constructor.name);
35  }
36
37  // Static methods
38  static registerSystem(id: string, systemType: string): void {
39    console.log(`Registered new security system: ${systemType} (ID: ${id})`);
40  }
41
42  static reportIncident(systemType: string, location: string, description: string): void {
43    const incident = {
44      date: new Date(),
45      systemType,
46      location,
47      description
48    };
49
50    this._incidents.push(incident);
51    console.log(`ALERT: Incident reported at location ${location}: ${description}`);
52
53    // Automatically raising the alert level
54    if (this._parkAlertLevel === "normall") {
55      this.changeAlertLevel("elevated");
56    } else if (this._parkAlertLevel === "elevated") {
57      this.changeAlertLevel("high");
58    } else if (this._parkAlertLevel === "high") {
59      this.changeAlertLevel("critical");
60    }
61  }
62
63  static changeAlertLevel(newLevel: AlertLevel): void {
64    this._parkAlertLevel = newLevel;
65    console.log(`WARNING: Changed park alert level to: ${newLevel.toUpperCase()}`);
66  }
67
68  static get alertLevel(): AlertLevel {
69    return this._parkAlertLevel;
70  }
71
72  static get recentIncidents(): Array<{date: Date, systemType: string, location: string, description: string}> {
73    // Returning the 5 most recent incidents
74    return [...this._incidents].slice(-5);
75  }
76
77  // Instance getters
78  get id(): string { return this._id; }
79  get location(): string { return this._location; }
80  get status(): StatusSystem { return this._status; }
81
82  // Implementation of MonitoringSystem interface
83  startMonitoring(): void {
84    this._isMonitored = true;
85    SecuritySystem._activeSystems.add(this._id);
86    console.log(`Monitoring started for system ${this.constructor.name} in location ${this._location}`);
87  }
88
89  stopMonitoring(): void {
90    this._isMonitored = false;
91    SecuritySystem._activeSystems.delete(this._id);
92    console.log(`Monitoring stopped for system ${this.constructor.name} in location ${this._location}`);
93  }
94
95  getCurrentStatus(): StatusSystem {
96    return this._status;
97  }
98
99  // Abstract methods that must be implemented
100  abstract start(): void;
101  abstract stop(): void;
102  abstract performReview(): void;
103
104  // Abstract properties
105  abstract get systemType(): string;
106  abstract get alarmPriorityLevel(): number; // 1-10
107
108  // Method using abstract properties and methods
109  generateStatusReport(): string {
110    return `
111=== SECURITY SYSTEM REPORT ===
112ID: ${this._id}
113Type: ${this.systemType}
114Location: ${this._location}
115Status: ${this._status}
116Monitored: ${this._isMonitored ? "YES" : "NO"}
117Alarm priority: ${this.alarmPriorityLevel}/10
118Last review: ${this._reviewDate.toLocaleDateString()}
119Global alert level: ${SecuritySystem._parkAlertLevel.toUpperCase()}
120=================================
121    `;
122  }
123}
124
125// Derived class - Electric Fence System
126class ElectricFenceSystem extends SecuritySystem {
127  private _voltage: number; // in volts
128  private _fenceLength: number; // in meters
129  private _securityType: "standard" | "reinforced" | "ultra-reinforced";
130
131  constructor(
132    id: string,
133    location: string,
134    fenceLength: number,
135    securityType: "standard" | "reinforced" | "ultra-reinforced"
136  ) {
137    super(id, location);
138    this._voltage = 0;
139    this._fenceLength = fenceLength;
140    this._securityType = securityType;
141  }
142
143  // Implementation of abstract properties
144  get systemType(): string {
145    return "Fence Electric";
146  }
147
148  get alarmPriorityLevel(): number {
149    // Priority depends on the security type
150    const base = this._securityType === "standard" ? 7 :
151                  this._securityType === "reinforced" ? 8 : 10;
152
153    // We take fence length into account - longer ones have higher priority
154    const modifier = Math.min(2, Math.floor(this._fenceLength / 1000));
155
156    return Math.min(10, base + modifier);
157  }
158
159  // Implementation of abstract methods
160  start(): void {
161    if (this._status === "maintenance") {
162      console.log(`ERROR: Cannot start system ${this._id} - is under maintenance`);
163      return;
164    }
165
166    this._status = "active";
167
168    // Voltage depends on the security type
169    this._voltage = this._securityType === "standard" ? 10000 :
170                   this._securityType === "reinforced" ? 20000 : 30000;
171
172    console.log(`Started fence system ${this._id} in location ${this._location}`);
173    console.log(`Voltage: ${this._voltage}V, Length: ${this._fenceLength}m`);
174  }
175
176  stop(): void {
177    this._status = "inactive";
178    this._voltage = 0;
179    console.log(`Stopped fence system ${this._id} in location ${this._location}`);
180
181    // Reporting an incident at high alert level
182    if (SecuritySystem.alertLevel === "high" || SecuritySystem.alertLevel === "critical") {
183      SecuritySystem.reportIncident(
184        this.systemType,
185        this._location,
186        `Fence shutdown at high alert level!`
187      );
188    }
189  }
190
191  performReview(): void {
192    const previousStatus = this._status;
193    this._status = "maintenance";
194    console.log(`Started fence system review ${this._id}`);
195
196    // Simulation of inspection
197    setTimeout(() => {
198      this._reviewDate = new Date();
199      this._status = previousStatus;
200      console.log(`Completed fence system review ${this._id}`);
201    }, 2000);
202  }
203
204  // Methods specific to this system type
205  changeVoltage(newVoltage: number): void {
206    if (this._status !== "active") {
207      console.log(`ERROR: Cannot change voltage - system is not active`);
208      return;
209    }
210
211    this._voltage = newVoltage;
212    console.log(`Changed fence voltage ${this._id} to ${newVoltage}V`);
213  }
214
215  reportFenceBreach(): void {
216    this._status = "failure";
217    SecuritySystem.reportIncident(
218      this.systemType,
219      this._location,
220      `Electric fence breach detected!`
221    );
222  }
223}
224
225// Another derived class - Video Monitoring System
226class VideoMonitoringSystem extends SecuritySystem {
227  private _cameraCount: number;
228  private _resolution: "SD" | "HD" | "4K";
229  private _nightMode: boolean;
230
231  constructor(
232    id: string,
233    location: string,
234    countKamer: number,
235    resolution: "SD" | "HD" | "4K"
236  ) {
237    super(id, location);
238    this._cameraCount = countKamer;
239    this._resolution = resolution;
240    this._nightMode = false;
241  }
242
243  // Implementation of abstract properties
244  get systemType(): string {
245    return "Video monitoring";
246  }
247
248  get alarmPriorityLevel(): number {
249    // Priority depends on the number of cameras and resolution
250    const base = 5;
251
252    // Resolution modifier
253    const resolutionModifier =
254      this._resolution === "SD" ? 0 :
255      this._resolution === "HD" ? 1 : 2;
256
257    // Camera count modifier
258    const cameraModifier = Math.min(2, Math.floor(this._cameraCount / 10));
259
260    return Math.min(10, base + resolutionModifier + cameraModifier);
261  }
262
263  // Implementation of abstract methods
264  start(): void {
265    this._status = "active";
266    console.log(`Started monitoring system ${this._id} in location ${this._location}`);
267    console.log(`Camera count: ${this._cameraCount}, Resolution: ${this._resolution}`);
268  }
269
270  stop(): void {
271    this._status = "inactive";
272    console.log(`Stopped monitoring system ${this._id} in location ${this._location}`);
273  }
274
275  performReview(): void {
276    this._status = "maintenance";
277    console.log(`Started monitoring system review ${this._id}`);
278
279    // In this case, the inspection completes immediately
280    this._reviewDate = new Date();
281    this._status = "active";
282    console.log(`Completed monitoring system review ${this._id}`);
283  }
284
285  // Methods specific to this system type
286  enableNightMode(): void {
287    this._nightMode = true;
288    console.log(`Enabled night mode in monitoring system ${this._id}`);
289  }
290
291  disableNightMode(): void {
292    this._nightMode = false;
293    console.log(`Disabled night mode in monitoring system ${this._id}`);
294  }
295
296  detectMotion(area: string): void {
297    console.log(`System ${this._id}: Motion detected in area ${area}`);
298
299    if (SecuritySystem.alertLevel !== "normall") {
300      SecuritySystem.reportIncident(
301        this.systemType,
302        `${this._location} - area ${area}`,
303        `Detected unauthorized motion!`
304      );
305    }
306  }
307}
308
309// Demonstration of Jurassic Park security system
310function testSecuritySystem() {
311  console.log("=== INITIALIZING JURASSIC PARK SECURITY SYSTEMS ===");
312
313  // Creating fence systems
314  const mainFence = new ElectricFenceSystem("FNC-001", "Sector A - Main entrance", 2500, "ultra-reinforced");
315  const tRexFence = new ElectricFenceSystem("FNC-002", "Sector B - Enclosure T-Rex", 1200, "ultra-reinforced");
316  const raptorFence = new ElectricFenceSystem("FNC-003", "Sector C - Raptor Enclosure", 800, "reinforced");
317
318  // Creating monitoring systems
319  const mainMonitoring = new VideoMonitoringSystem("CAM-001", "Control center", 20, "4K");
320  const monitoringTRex = new VideoMonitoringSystem("CAM-002", "Sector B - Enclosure T-Rex", 12, "HD");
321  const raptorMonitoring = new VideoMonitoringSystem("CAM-003", "Sector C - Raptor Enclosure", 8, "HD");
322
323  // Starting all systems
324  console.log("\n=== STARTING SYSTEMS ===");
325  mainFence.start();
326  tRexFence.start();
327  raptorFence.start();
328
329  mainMonitoring.start();
330  monitoringTRex.start();
331  raptorMonitoring.start();
332
333  // Starting monitoring
334  console.log("\n=== ACTIVATION MONITORING ===");
335  mainFence.startMonitoring();
336  tRexFence.startMonitoring();
337  raptorFence.startMonitoring();
338
339  mainMonitoring.startMonitoring();
340  monitoringTRex.startMonitoring();
341  raptorMonitoring.startMonitoring();
342
343  // Checking statuses
344  console.log("\n=== SYSTEM REPORTS ===");
345  console.log(mainFence.generateStatusReport());
346  console.log(monitoringTRex.generateStatusReport());
347
348  // Simulating an incident
349  console.log("\n=== SIMULATION INCIDENT ===");
350  console.log("Current alert level:", SecuritySystem.alertLevel);
351
352  raptorMonitoring.detectMotion("South corner");
353  raptorFence.reportFenceBreach();
354
355  // Checking alert level after incidents
356  console.log("\nCurrent alert level after incidents:", SecuritySystem.alertLevel);
357
358  // Displaying recent incidents
359  console.log("\n=== LAST INCIDENTS ===");
360  SecuritySystem.recentIncidents.forEach((incident, index) => {
361    console.log(`${index + 1}. [${incident.date.toLocaleString()}] ${incident.systemType} in ${incident.location}: ${incident.description}`);
362  });
363
364  // Conducting inspection
365  console.log("\n=== SYSTEM REVIEW ===");
366  tRexFence.performReview();
367  mainMonitoring.performReview();
368
369  // Manual alert level change
370  console.log("\n=== NORMALIZATION SITUATION ===");
371  SecuritySystem.changeAlertLevel("normall");
372}
373
374// Running the test
375testSecuritySystem();

Dans cet exemple avancé :

  1. Nous utilisons la classe abstraite SecuritySystem comme base de tous les systèmes de sécurité
  2. La classe définit des propriétés et méthodes statiques (par ex. le niveau d'alerte du parc, la liste des incidents)
  3. Les méthodes et propriétés abstraites imposent l'implémentation de fonctionnalités spécifiques dans les classes dérivées
  4. Les classes dérivées (ElectricFenceSystem et VideoMonitoringSystem) implémentent les éléments abstraits
  5. Les éléments statiques permettent une coordination centrale (par ex. signaler des incidents et gérer les niveaux d'alerte)

Combiner éléments statiques et abstraits

Les éléments statiques et abstraits peuvent être un outil puissant lorsqu'on les utilise ensemble dans une hiérarchie de classes bien conçue. Voici quelques motifs et exemples de leur application :

1. Classe de base abstraite avec méthodes fabriques statiques

1abstract class DinosaurSpecies {
2  // Static properties
3  private static _registeredDinosaurs: Map<string, DinosaurSpecies> = new Map();
4
5  // Instance properties
6  private _name: string;
7  private _eraValue: string;
8  protected _dietType: "carnivore" | "herbivorous" | "omnivore";
9
10  constructor(name: string, era: string, diet: "carnivore" | "herbivorous" | "omnivore") {
11    this._name = name;
12    this._eraValue = era;
13    this._dietType = diet;
14
15    // Adding to registry
16    DinosaurSpecies._registeredDinosaurs.set(name, this);
17  }
18
19  // Static factory method - creates the appropriate species based on diet
20  static createSpecies(
21    name: string,
22    era: string,
23    diet: "carnivore" | "herbivorous" | "omnivore",
24    additionalParams: Record<string, any>
25  ): DinosaurSpecies {
26    if (diet === "carnivore") {
27      return new CarnivoreSpecies(
28        name,
29        era,
30        additionalParams.biteForce,
31        additionalParams.runSpeed
32      );
33    } else {
34      return new HerbivoreSpecies(
35        name,
36        era,
37        additionalParams.neckLength || 0,
38        additionalParams.hasArmor || false
39      );
40    }
41  }
42
43  // Static method for retrieving registered species
44  static getSpecies(name: string): DinosaurSpecies | undefined {
45    return this._registeredDinosaurs.get(name);
46  }
47
48  static get allSpecies(): string[] {
49    return Array.from(this._registeredDinosaurs.keys());
50  }
51
52  // Instance getters
53  get name(): string { return this._name; }
54  get era(): string { return this._eraValue; }
55  get dietType(): string { return this._dietType; }
56
57  // Abstract methods
58  abstract behaviorDescription(): string;
59  abstract estimateWeight(length: number): number;
60
61  // Method using abstract methods
62  generateDescription(): string {
63    return `${this._name} - dinosaur ${this._dietType} from the ${this._eraValue} era.
64${this.behaviorDescription()}`;
65  }
66}
67
68// Derived classes
69class CarnivoreSpecies extends DinosaurSpecies {
70  private _biteForce: number; // in PSI
71  private _runSpeed: number; // in km/h
72
73  constructor(name: string, era: string, biteForce: number, runSpeed: number) {
74    super(name, era, "carnivore");
75    this._biteForce = biteForce;
76    this._runSpeed = runSpeed;
77  }
78
79  // Getters
80  get biteForce(): number { return this._biteForce; }
81  get runSpeed(): number { return this._runSpeed; }
82
83  // Implementation of abstract methods
84  behaviorDescription(): string {
85    return `Hunts at speed ${this._runSpeed} km/h and bite force ${this._biteForce} PSI.`;
86  }
87
88  estimateWeight(length: number): number {
89    // Approximate formula for predators
90    return length * length * 100;
91  }
92}
93
94class HerbivoreSpecies extends DinosaurSpecies {
95  private _neckLength: number; // in meters
96  private _hasArmor: boolean;
97
98  constructor(name: string, era: string, neckLength: number, hasArmor: boolean) {
99    super(name, era, "herbivorous");
100    this._neckLength = neckLength;
101    this._hasArmor = hasArmor;
102  }
103
104  // Getters
105  get neckLength(): number { return this._neckLength; }
106  get hasArmor(): boolean { return this._hasArmor; }
107
108  // Implementation of abstract methods
109  behaviorDescription(): string {
110    const neckDescription = this._neckLength > 3
111      ? `Has a long neck (${this._neckLength}m) allowing it to eat tall vegetation.`
112      : "Feeds on low vegetation.";
113
114    const armorDescription = this._hasArmor
115      ? "Has armor for protection against predators."
116      : "Does not have protective armor.";
117
118    return `${neckDescription} ${armorDescription}`;
119  }
120
121  estimateWeight(length: number): number {
122    // Approximate formula for herbivores (usually heavier)
123    let weight = length * length * 150;
124
125    // Additional weight for armor
126    if (this._hasArmor) {
127      weight *= 1.3;
128    }
129
130    return weight;
131  }
132}
133
134// Usage
135function testDinosaurSpecies() {
136  // Creating species using the factory method
137  DinosaurSpecies.createSpecies(
138    "Tyrannosaurus Rex",
139    "late Cretaceous",
140    "carnivore",
141    { biteForce: 12800, runSpeed: 32 }
142  );
143
144  DinosaurSpecies.createSpecies(
145    "Velociraptor",
146    "late Cretaceous",
147    "carnivore",
148    { biteForce: 2000, runSpeed: 64 }
149  );
150
151  DinosaurSpecies.createSpecies(
152    "Brachiosaurus",
153    "late Jurassic",
154    "herbivorous",
155    { neckLength: 9, hasArmor: false }
156  );
157
158  DinosaurSpecies.createSpecies(
159    "Stegosaurus",
160    "late Jurassic",
161    "herbivorous",
162    { neckLength: 0.5, hasArmor: true }
163  );
164
165  // Accessing registered species
166  console.log("Available species:", DinosaurSpecies.allSpecies);
167
168  // Retrieving species information
169  const trex = DinosaurSpecies.getSpecies("Tyrannosaurus Rex");
170  const brach = DinosaurSpecies.getSpecies("Brachiosaurus");
171
172  if (trex) {
173    console.log(trex.generateDescription());
174    console.log(`Estimated weight (12m long): ${trex.estimateWeight(12)} kg`);
175  }
176
177  if (brach) {
178    console.log(brach.generateDescription());
179    console.log(`Estimated weight (25m long): ${brach.estimateWeight(25)} kg`);
180  }
181
182  // Type checking
183  const steg = DinosaurSpecies.getSpecies("Stegosaurus") as HerbivoreSpecies;
184  if (steg && steg instanceof HerbivoreSpecies) {
185    console.log(`Stegosaurus has armor: ${steg.hasArmor ? "YES" : "NO"}`);
186  }
187
188  const raptor = DinosaurSpecies.getSpecies("Velociraptor") as CarnivoreSpecies;
189  if (raptor && raptor instanceof CarnivoreSpecies) {
190    console.log(`Velociraptor - run speed: ${raptor.runSpeed} km/h`);
191  }
192}
193
194testDinosaurSpecies();

2. Méthodes statiques abstraites

Depuis la version 4.0 de TypeScript, il est possible de définir des méthodes statiques abstraites. Cela permet de créer des hiérarchies de classes où les classes dérivées doivent implémenter des méthodes statiques précises :

1abstract class LocationSystem {
2  // Instance properties
3  protected _id: string;
4  protected _name: string;
5
6  constructor(id: string, name: string) {
7    this._id = id;
8    this._name = name;
9  }
10
11  // Abstract static method that derived classes must implement
12  static abstract generateLocationId(type: string, numer: number): string;
13
14  // Regular abstract instance method
15  abstract getLocationInfo(): Record<string, any>;
16
17  // Instance method
18  generateReport(): string {
19    const info = this.getLocationInfo();
20
21    let report = `=== REPORT LOCATION: ${this._name} (ID: ${this._id}) ===\n`;
22
23    for (const [key, value] of Object.entries(info)) {
24      report += `${key}: ${value}\n`;
25    }
26
27    return report;
28  }
29}
30
31class DinosaurEnclosure extends LocationSystem {
32  private _species: string;
33  private _area: number; // in hectares
34  private _specimenCount: number;
35
36  constructor(numer: number, name: string, species: string, area: number, specimenCount: number) {
37    // Generating ID for the enclosure
38    const id = DinosaurEnclosure.generateLocationId("enclosure", numer);
39    super(id, name);
40
41    this._species = species;
42    this._area = area;
43    this._specimenCount = specimenCount;
44  }
45
46  // Implementation of abstract static method
47  static generateLocationId(type: string, numer: number): string {
48    return `${type.toUpperCase()}-${numer.toString().padStart(3, '0')}`;
49  }
50
51  // Implementation of abstract instance method
52  getLocationInfo(): Record<string, any> {
53    return {
54      type: "Dinosaur enclosure",
55      species: this._species,
56      area: `${this._area} ha`,
57      specimenCount: this._specimenCount,
58      populationDensity: `${(this._specimenCount / this._area).toFixed(2)} spec./ha`
59    };
60  }
61
62  // Methods specific to this class
63  addSpecimen(): void {
64    this._specimenCount++;
65    console.log(`Added new specimen to enclosure ${this._name}. New count: ${this._specimenCount}`);
66  }
67
68  removeSpecimen(): boolean {
69    if (this._specimenCount > 0) {
70      this._specimenCount--;
71      console.log(`Removed specimen from enclosure ${this._name}. New count: ${this._specimenCount}`);
72      return true;
73    }
74
75    console.log(`Error: Cannot remove specimen from enclosure ${this._name} - no specimens.`);
76    return false;
77  }
78}
79
80class TouristAttraction extends LocationSystem {
81  private _attractionType: string;
82  private _capacity: number;
83  private _waitingTime: number; // in minutes
84
85  constructor(numer: number, name: string, attractionType: string, capacity: number) {
86    // Generating ID for the attraction
87    const id = TouristAttraction.generateLocationId("attraction", numer);
88    super(id, name);
89
90    this._attractionType = attractionType;
91    this._capacity = capacity;
92    this._waitingTime = 0;
93  }
94
95  // Implementation of abstract static method
96  static generateLocationId(type: string, numer: number): string {
97    // Different ID format for attractions
98    return `${type.substring(0, 3).toUpperCase()}${numer.toString().padStart(4, '0')}`;
99  }
100
101  // Implementation of abstract instance method
102  getLocationInfo(): Record<string, any> {
103    return {
104      type: "Tourist attraction",
105      category: this._attractionType,
106      capacity: this._capacity,
107      waitingTime: `${this._waitingTime} min`
108    };
109  }
110
111  // Methods specific to this class
112  updateWaitingTime(newWaitingTime: number): void {
113    this._waitingTime = newWaitingTime;
114    console.log(`Updated waiting time for ${this._name}: ${this._waitingTime} min`);
115  }
116}
117
118// Application
119function testLocationSystem() {
120  // Creating enclosures
121  const tRexEnclosure = new DinosaurEnclosure(1, "T-Rex Kingdom", "Tyrannosaurus Rex", 5, 1);
122  const raptorEnclosure = new DinosaurEnclosure(2, "Raptor Valley", "Velociraptor", 3, 6);
123
124  // Creating attractions
125  const visitorCenter = new TouristAttraction(101, "Genetics Center", "educational", 500);
126  const jeepSafari = new TouristAttraction(102, "Jurassic Safari", "ride", 50);
127
128  // Displaying reports
129  console.log(tRexEnclosure.generateReport());
130  console.log(raptorEnclosure.generateReport());
131  console.log(visitorCenter.generateReport());
132  console.log(jeepSafari.generateReport());
133
134  // Performing operations
135  raptorEnclosure.addSpecimen();
136  raptorEnclosure.addSpecimen();
137  jeepSafari.updateWaitingTime(45);
138
139  // Checking updated data
140  console.log(raptorEnclosure.generateReport());
141  console.log(jeepSafari.generateReport());
142
143  // Demonstration of different ID formats generated by static method implementations
144  console.log("Enclosure ID format:", DinosaurEnclosure.generateLocationId("enclosure", 42));
145  console.log("Attraction ID format:", TouristAttraction.generateLocationId("attraction", 42));
146}
147
148testLocationSystem();

Résumé

Dans cet exercice, nous avons découvert deux mécanismes puissants de TypeScript :

  1. Propriétés et méthodes statiques :

    • Appartiennent à la classe, et non aux instances
    • Permettent de partager des données et des fonctionnalités entre toutes les instances
    • Idéales pour définir des constantes, un état global et des méthodes utilitaires
    • Accessibles directement via le nom de la classe, sans créer d'instance
  2. Classes et méthodes abstraites :

    • Définissent un « modèle » ou un « contrat » qui doit être implémenté par les classes qui en héritent
    • Ne peuvent pas être instanciées directement
    • Imposent l'implémentation de méthodes précises dans les classes dérivées
    • Permettent de partager des fonctionnalités communes au sein d'une hiérarchie de classes

En combinant ces concepts, nous pouvons créer des systèmes élégants, modulaires et sûrs en matière de types, parfaits pour modéliser des relations et processus complexes, comme ceux du système de gestion de Jurassic Park.

Le Dr Henry Wu le résumerait ainsi : « Dans la nature comme en programmation, il y a de la beauté dans la structure et l'ordre. Les éléments statiques assurent la cohérence de toute l'espèce, tandis que les concepts abstraits forment le plan de base que chaque organisme doit implémenter à sa manière. Ensemble, ils créent une harmonie parfaite - avec les protections adéquates, bien sûr... dans la plupart des cas. »

Code de cette leçon : index.ts
1// Static properties et abstract classes - Parc Jurassique
2console.log("=== STATIC PROPERTIES AND ABSTRACT CLASSES ===\n");
3
4// ===========================================
5// 1. Classe abstraite Dinosaur
6// ===========================================
7
8abstract class Dinosaur {
9  // Compteur statique pour tous les dinosaures
10  static totalCount: number = 0;
11  static readonly PARK_NAME: string = "Jurassic World";
12
13  // Méthode fabrique statique
14  static createId(): string {
15    return `DINO-${String(++Dinosaur.totalCount).padStart(4, "0")}`;
16  }
17
18  // Champs d'instance
19  readonly id: string;
20  name: string;
21  protected weight: number;
22
23  constructor(name: string, weight: number) {
24    this.id = Dinosaur.createId();
25    this.name = name;
26    this.weight = weight;
27  }
28
29  // Méthode abstraite - chaque espèce doit l'implémenter
30  abstract makeSound(): string;
31  abstract getDangerLevel(): number;
32
33  // Méthode commune
34  getProfile(): string {
35    return `[${this.id}] ${this.name} - ${this.weight}kg`;
36  }
37}
38
39// ===========================================
40// 2. Implémentations des classes concrètes
41// ===========================================
42
43class TRex extends Dinosaur {
44  private biteForce: number;
45
46  constructor(name: string, weight: number, biteForce: number) {
47    super(name, weight);
48    this.biteForce = biteForce;
49  }
50
51  makeSound(): string {
52    return "ROOOOAAAR!";
53  }
54
55  getDangerLevel(): number {
56    return 10;
57  }
58}
59
60class Triceratops extends Dinosaur {
61  private hornLength: number;
62
63  constructor(name: string, weight: number, hornLength: number) {
64    super(name, weight);
65    this.hornLength = hornLength;
66  }
67
68  makeSound(): string {
69    return "BRRRRR!";
70  }
71
72  getDangerLevel(): number {
73    return 4;
74  }
75}
76
77// ===========================================
78// 3. Méthode fabrique statique
79// ===========================================
80
81class DinosaurFactory {
82  static registry: Map<string, Dinosaur> = new Map();
83
84  static create(type: "trex" | "triceratops", name: string): Dinosaur {
85    let dino: Dinosaur;
86    if (type === "trex") {
87      dino = new TRex(name, 8000, 12800);
88    } else {
89      dino = new Triceratops(name, 6000, 90);
90    }
91    DinosaurFactory.registry.set(dino.id, dino);
92    return dino;
93  }
94
95  static getAll(): Dinosaur[] {
96    return Array.from(DinosaurFactory.registry.values());
97  }
98}
99
100// ===========================================
101// Test
102// ===========================================
103
104const rex = DinosaurFactory.create("trex", "Rexy");
105const trike = DinosaurFactory.create("triceratops", "Cera");
106const rex2 = DinosaurFactory.create("trex", "Buck");
107
108console.log("Park:", Dinosaur.PARK_NAME);
109console.log("Nombre de dinosaures:", Dinosaur.totalCount);
110console.log("");
111
112DinosaurFactory.getAll().forEach(dino => {
113  console.log(dino.getProfile());
114  console.log("  Son:", dino.makeSound());
115  console.log("  Danger:", dino.getDangerLevel(), "/10");
116});
117
118// On ne peut pas créer d'instance d'une classe abstraite :
119// const dino = new Dinosaur("Test", 100); // Error!
120console.log("\nUne classe abstraite ne peut pas être instanciée directement.");

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 classe abstraite en TypeScript ?

  2. 2. En quoi les propriétés statiques diffèrent-elles des propriétés ordinaires dans une classe ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Implémente des classes avec des propriétés statiques et abstraites.

  • Clique dans l'ordre

    Range les éléments de la déclaration d'une classe abstraite :

  • Éditeur de code

    Crée des classes avec différents modificateurs d'accès pour le système du parc

Articles utiles