Cours JavaScript et TypeScript · Module 9: Design patterns et architecture

Patterns de conception en JavaScript/TypeScript - Module, Factory et Observer

21 min de lecture
Dans cette leçon6

NOTE : Matériel approfondi - Plan d'étude

Ce module contient un matériel pédagogique très dense. Tu n'as pas besoin de tout assimiler d'un coup ! Voici un ordre d'apprentissage suggéré :

Étape 1 - Fondamentaux (priorité : haute)

  • Module Pattern et Revealing Module Pattern
  • Factory Pattern
  • Observer Pattern
  • Singleton Pattern

Étape 2 - Architecture (priorité : moyenne)

  • Les principes SOLID
  • Dependency Injection et Event Bus

Étape 3 - Programmation fonctionnelle et état (priorité : moyenne)

  • Fonctions pures, immutabilité, currying et composition
  • Patterns de gestion d'état : Flux, Event Sourcing, machines à états

Étape 4 - Performance et pratique (à étudier selon les besoins)

  • Code splitting, tree shaking et lazy loading
  • Optimisation du bundle
  • Architecture événementielle et projet final

Conseils :

  • Assimile le matériel de façon séquentielle, section par section
  • Après chaque pattern, fais une pause et exerce-toi par toi-même
  • Reviens aux sujets plus difficiles après un certain temps
  • Ne saute pas les fondamentaux - ils sont la clé pour comprendre le reste
  • Les patterns avancés peuvent être étudiés au besoin dans tes projets

Souviens-toi : il vaut mieux bien comprendre les bases que survoler tout superficiellement !


Tout comme les architectes de l'Antiquité utilisaient des schémas de construction éprouvés pour bâtir des temples durables, les développeurs modernes utilisent des patterns de conception pour créer un code solide et évolutif. Dans cette leçon, tu vas découvrir quatre patterns fondamentaux : Module Pattern, Revealing Module Pattern, Factory Pattern et Observer Pattern, qui constituent la base de nombreuses applications JavaScript et TypeScript modernes.

Module Pattern - Encapsulation et confidentialité

Le Module Pattern est l'un des patterns les plus importants en JavaScript : il permet d'encapsuler du code et de créer des variables et méthodes privées. Avant l'arrivée des modules ES6, c'était le principal moyen d'organiser le code en modules.

Implémentation classique du Module Pattern

1// Example of a simple calculator module
2const Calculator = (function() {
3  // Private variables and functions
4  let result = 0;
5
6  function validateNumber(num) {
7    if (typeof num !== 'number' || isNaN(num)) {
8      throw new Error('The provided value is not a number');
9    }
10  }
11
12  function logOperation(operation, num) {
13    console.log(`Operation performed: ${operation} ${num}`);
14  }
15
16  // Public interface
17  return {
18    add: function(num) {
19      validateNumber(num);
20      result += num;
21      logOperation('addition', num);
22      return this;
23    },
24
25    subtract: function(num) {
26      validateNumber(num);
27      result -= num;
28      logOperation('subtraction', num);
29      return this;
30    },
31
32    multiply: function(num) {
33      validateNumber(num);
34      result *= num;
35      logOperation('multiplication', num);
36      return this;
37    },
38
39    divide: function(num) {
40      validateNumber(num);
41      if (num === 0) {
42        throw new Error('Cannot divide by zero');
43      }
44      result /= num;
45      logOperation('division', num);
46      return this;
47    },
48
49    getResult: function() {
50      return result;
51    },
52
53    reset: function() {
54      result = 0;
55      console.log('Calculator has been reset');
56      return this;
57    }
58  };
59})();
60
61// Usage
62Calculator
63  .add(10)
64  .multiply(2)
65  .subtract(5)
66  .divide(3);
67
68console.log(Calculator.getResult()); // 5

Module Pattern avec paramètres

1// Module for managing application state
2const StateManager = (function(initialState = {}) {
3  // Private state
4  let state = { ...initialState };
5  let listeners = [];
6
7  // Private helper functions
8  function deepClone(obj) {
9    return JSON.parse(JSON.stringify(obj));
10  }
11
12  function notifyListeners(action, newState) {
13    listeners.forEach(listener => {
14      if (typeof listener === 'function') {
15        listener(action, deepClone(newState));
16      }
17    });
18  }
19
20  // Public interface
21  return {
22    getState: function(key) {
23      if (key) {
24        return deepClone(state[key]);
25      }
26      return deepClone(state);
27    },
28
29    setState: function(key, value) {
30      const oldValue = state[key];
31      state[key] = value;
32
33      notifyListeners({
34        type: 'STATE_CHANGED',
35        key: key,
36        oldValue: oldValue,
37        newValue: value
38      }, state);
39
40      return this;
41    },
42
43    updateState: function(updates) {
44      const oldState = deepClone(state);
45      state = { ...state, ...updates };
46
47      notifyListeners({
48        type: 'STATE_UPDATED',
49        oldState: oldState,
50        newState: deepClone(state)
51      }, state);
52
53      return this;
54    },
55
56    subscribe: function(listener) {
57      if (typeof listener === 'function') {
58        listeners.push(listener);
59      }
60
61      // Return an unsubscribe function
62      return function unsubscribe() {
63        const index = listeners.indexOf(listener);
64        if (index > -1) {
65          listeners.splice(index, 1);
66        }
67      };
68    },
69
70    reset: function() {
71      const oldState = deepClone(state);
72      state = { ...initialState };
73
74      notifyListeners({
75        type: 'STATE_RESET',
76        oldState: oldState
77      }, state);
78
79      return this;
80    }
81  };
82})({ theme: 'light', language: 'pl' });
83
84// Usage
85const unsubscribe = StateManager.subscribe((action, newState) => {
86  console.log('State has changed:', action);
87});
88
89StateManager
90  .setState('user', { name: 'Jan', age: 30 })
91  .updateState({ theme: 'dark', notifications: true });

Revealing Module Pattern - Encapsulation améliorée

Le Revealing Module Pattern est une évolution du Module Pattern qui offre une meilleure lisibilité et une meilleure séparation entre fonctions privées et publiques.

Implémentation de base du Revealing Module Pattern

1// Module for managing products in a store
2const ProductManager = (function() {
3  // Private variables
4  let products = [];
5  let nextId = 1;
6
7  // Private functions
8  function generateId() {
9    return nextId++;
10  }
11
12  function validateProduct(product) {
13    if (!product.name || typeof product.name !== 'string') {
14      throw new Error('Product name is required and must be a string');
15    }
16
17    if (!product.price || typeof product.price !== 'number' || product.price <= 0) {
18      throw new Error('Product price must be a number greater than zero');
19    }
20
21    return true;
22  }
23
24  function findProductIndex(id) {
25    return products.findIndex(product => product.id === id);
26  }
27
28  function sortProductsByPrice(ascending = true) {
29    return products.slice().sort((a, b) => {
30      return ascending ? a.price - b.price : b.price - a.price;
31    });
32  }
33
34  function filterProductsByCategory(category) {
35    return products.filter(product =>
36      product.category && product.category.toLowerCase() === category.toLowerCase()
37    );
38  }
39
40  // Public functions
41  function addProduct(productData) {
42    validateProduct(productData);
43
44    const product = {
45      id: generateId(),
46      name: productData.name,
47      price: productData.price,
48      category: productData.category || 'uncategorized',
49      description: productData.description || '',
50      inStock: productData.inStock !== false,
51      createdAt: new Date().toISOString()
52    };
53
54    products.push(product);
55    console.log(`Product added: ${product.name}`);
56
57    return product.id;
58  }
59
60  function removeProduct(id) {
61    const index = findProductIndex(id);
62
63    if (index === -1) {
64      throw new Error(`Product with ID ${id} was not found`);
65    }
66
67    const removedProduct = products.splice(index, 1)[0];
68    console.log(`Product removed: ${removedProduct.name}`);
69
70    return removedProduct;
71  }
72
73  function updateProduct(id, updates) {
74    const index = findProductIndex(id);
75
76    if (index === -1) {
77      throw new Error(`Product with ID ${id} was not found`);
78    }
79
80    const updatedProduct = { ...products[index], ...updates };
81    validateProduct(updatedProduct);
82
83    products[index] = updatedProduct;
84    console.log(`Product updated: ${updatedProduct.name}`);
85
86    return updatedProduct;
87  }
88
89  function getProduct(id) {
90    const product = products.find(p => p.id === id);
91    return product ? { ...product } : null;
92  }
93
94  function getAllProducts() {
95    return products.map(product => ({ ...product }));
96  }
97
98  function getProductsByCategory(category) {
99    return filterProductsByCategory(category).map(product => ({ ...product }));
100  }
101
102  function getProductsSortedByPrice(ascending = true) {
103    return sortProductsByPrice(ascending).map(product => ({ ...product }));
104  }
105
106  function getProductCount() {
107    return products.length;
108  }
109
110  function clearAllProducts() {
111    const count = products.length;
112    products = [];
113    nextId = 1;
114    console.log(`All products removed (${count})`);
115  }
116
117  // Revealing - exposition des méthodes publiques
118  return {
119    add: addProduct,
120    remove: removeProduct,
121    update: updateProduct,
122    get: getProduct,
123    getAll: getAllProducts,
124    getByCategory: getProductsByCategory,
125    getSortedByPrice: getProductsSortedByPrice,
126    count: getProductCount,
127    clear: clearAllProducts
128  };
129})();
130
131// Usage
132const laptopId = ProductManager.add({
133  name: 'Laptop Dell XPS 13',
134  price: 4500,
135  category: 'Electronics',
136  description: 'High-quality ultrabook laptop'
137});
138
139const phoneId = ProductManager.add({
140  name: 'iPhone 15 Pro',
141  price: 5500,
142  category: 'Electronics'
143});
144
145console.log('Electronics products:', ProductManager.getByCategory('Electronics'));
146console.log('Sorted products:', ProductManager.getSortedByPrice());

TypeScript avec le Revealing Module Pattern

1// Interfaces for better typing
2interface User {
3  id: number;
4  email: string;
5  name: string;
6  role: 'admin' | 'user' | 'moderator';
7  isActive: boolean;
8  lastLogin?: Date;
9}
10
11interface UserCreateData {
12  email: string;
13  name: string;
14  role?: User['role'];
15}
16
17interface UserUpdateData {
18  name?: string;
19  role?: User['role'];
20  isActive?: boolean;
21}
22
23// User management module in TypeScript
24const UserManager = (function(): {
25  createUser: (userData: UserCreateData) => number;
26  getUserById: (id: number) => User | null;
27  updateUser: (id: number, updates: UserUpdateData) => User | null;
28  deleteUser: (id: number) => boolean;
29  getAllUsers: () => User[];
30  getActiveUsers: () => User[];
31  getUsersByRole: (role: User['role']) => User[];
32  markUserLogin: (id: number) => boolean;
33  getUserCount: () => number;
34} {
35  // Private variables
36  let users: User[] = [];
37  let nextId: number = 1;
38
39  // Private functions
40  function generateId(): number {
41    return nextId++;
42  }
43
44  function validateEmail(email: string): boolean {
45    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
46    return emailRegex.test(email);
47  }
48
49  function findUserIndex(id: number): number {
50    return users.findIndex(user => user.id === id);
51  }
52
53  function isEmailTaken(email: string, excludeId?: number): boolean {
54    return users.some(user =>
55      user.email.toLowerCase() === email.toLowerCase() &&
56      user.id !== excludeId
57    );
58  }
59
60  function createUserObject(userData: UserCreateData): User {
61    return {
62      id: generateId(),
63      email: userData.email.toLowerCase(),
64      name: userData.name,
65      role: userData.role || 'user',
66      isActive: true,
67      lastLogin: undefined
68    };
69  }
70
71  // Public functions
72  function createUser(userData: UserCreateData): number {
73    if (!validateEmail(userData.email)) {
74      throw new Error('Invalid email format');
75    }
76
77    if (isEmailTaken(userData.email)) {
78      throw new Error('A user with this email already exists');
79    }
80
81    if (!userData.name || userData.name.trim().length < 2) {
82      throw new Error('Username must be at least 2 characters long');
83    }
84
85    const user = createUserObject(userData);
86    users.push(user);
87
88    console.log(`User created: ${user.name} (${user.email})`);
89    return user.id;
90  }
91
92  function getUserById(id: number): User | null {
93    const user = users.find(u => u.id === id);
94    return user ? { ...user } : null;
95  }
96
97  function updateUser(id: number, updates: UserUpdateData): User | null {
98    const index = findUserIndex(id);
99
100    if (index === -1) {
101      return null;
102    }
103
104    const currentUser = users[index];
105    const updatedUser: User = {
106      ...currentUser,
107      ...updates
108    };
109
110    if (updates.name && updates.name.trim().length < 2) {
111      throw new Error('Username must be at least 2 characters long');
112    }
113
114    users[index] = updatedUser;
115    console.log(`User updated: ${updatedUser.name}`);
116
117    return { ...updatedUser };
118  }
119
120  function deleteUser(id: number): boolean {
121    const index = findUserIndex(id);
122
123    if (index === -1) {
124      return false;
125    }
126
127    const deletedUser = users.splice(index, 1)[0];
128    console.log(`User deleted: ${deletedUser.name}`);
129
130    return true;
131  }
132
133  function getAllUsers(): User[] {
134    return users.map(user => ({ ...user }));
135  }
136
137  function getActiveUsers(): User[] {
138    return users.filter(user => user.isActive).map(user => ({ ...user }));
139  }
140
141  function getUsersByRole(role: User['role']): User[] {
142    return users.filter(user => user.role === role).map(user => ({ ...user }));
143  }
144
145  function markUserLogin(id: number): boolean {
146    const index = findUserIndex(id);
147
148    if (index === -1) {
149      return false;
150    }
151
152    users[index].lastLogin = new Date();
153    return true;
154  }
155
156  function getUserCount(): number {
157    return users.length;
158  }
159
160  // Revealing - exposition des méthodes publiques
161  return {
162    createUser,
163    getUserById,
164    updateUser,
165    deleteUser,
166    getAllUsers,
167    getActiveUsers,
168    getUsersByRole,
169    markUserLogin,
170    getUserCount
171  };
172})();
173
174// Usage with TypeScript
175const adminId = UserManager.createUser({
176  email: 'admin@example.com',
177  name: 'Administrator',
178  role: 'admin'
179});
180
181const userId = UserManager.createUser({
182  email: 'user@example.com',
183  name: 'Jan Kowalski'
184});
185
186console.log('Administrators:', UserManager.getUsersByRole('admin'));
187console.log('Active users:', UserManager.getActiveUsers());

Factory Pattern - Création d'objets

Le Factory Pattern est un pattern de création qui permet de créer des objets sans préciser la classe concrète de l'objet à instancier. Il est particulièrement utile lorsqu'on a différents types d'objets ayant une structure similaire.

Factory Pattern simple

1// Factory for creating different types of vehicles
2class VehicleFactory {
3  static createVehicle(type, options = {}) {
4    switch (type.toLowerCase()) {
5      case 'car':
6        return new Car(options);
7      case 'motorcycle':
8        return new Motorcycle(options);
9      case 'truck':
10        return new Truck(options);
11      case 'bicycle':
12        return new Bicycle(options);
13      default:
14        throw new Error(`Unknown vehicle type: ${type}`);
15    }
16  }
17
18  static getSupportedTypes() {
19    return ['car', 'motorcycle', 'truck', 'bicycle'];
20  }
21}
22
23// Base classes
24class Vehicle {
25  constructor(options = {}) {
26    this.brand = options.brand || 'Unknown';
27    this.model = options.model || 'Unknown';
28    this.year = options.year || new Date().getFullYear();
29    this.color = options.color || 'white';
30    this.maxSpeed = options.maxSpeed || 0;
31  }
32
33  start() {
34    console.log(`${this.brand} ${this.model} has been started`);
35  }
36
37  stop() {
38    console.log(`${this.brand} ${this.model} has been stopped`);
39  }
40
41  getInfo() {
42    return `${this.brand} ${this.model} (${this.year}) - ${this.color}`;
43  }
44}
45
46class Car extends Vehicle {
47  constructor(options = {}) {
48    super(options);
49    this.type = 'Car';
50    this.doors = options.doors || 4;
51    this.fuelType = options.fuelType || 'gasoline';
52    this.transmission = options.transmission || 'manual';
53  }
54
55  openTrunk() {
56    console.log('Trunk has been opened');
57  }
58
59  getDetails() {
60    return `${this.getInfo()} - ${this.doors} doors, ${this.fuelType}, ${this.transmission}`;
61  }
62}
63
64class Motorcycle extends Vehicle {
65  constructor(options = {}) {
66    super(options);
67    this.type = 'Motorcycle';
68    this.engineSize = options.engineSize || 125; // cc
69    this.hasSidecar = options.hasSidecar || false;
70  }
71
72  wheelie() {
73    console.log('Doing a wheelie!');
74  }
75
76  getDetails() {
77    const sidecar = this.hasSidecar ? 'with sidecar' : 'without sidecar';
78    return `${this.getInfo()} - ${this.engineSize}cc, ${sidecar}`;
79  }
80}
81
82class Truck extends Vehicle {
83  constructor(options = {}) {
84    super(options);
85    this.type = 'Truck';
86    this.loadCapacity = options.loadCapacity || 1000; // kg
87    this.trailerAttached = options.trailerAttached || false;
88  }
89
90  loadCargo(weight) {
91    if (weight > this.loadCapacity) {
92      console.log(`Cannot load ${weight}kg. Maximum capacity: ${this.loadCapacity}kg`);
93      return false;
94    }
95
96    console.log(`Loaded ${weight}kg cargo`);
97    return true;
98  }
99
100  getDetails() {
101    const trailer = this.trailerAttached ? 'with trailer' : 'without trailer';
102    return `${this.getInfo()} - capacity: ${this.loadCapacity}kg, ${trailer}`;
103  }
104}
105
106class Bicycle extends Vehicle {
107  constructor(options = {}) {
108    super(options);
109    this.type = 'Bicycle';
110    this.gears = options.gears || 1;
111    this.isElectric = options.isElectric || false;
112    this.maxSpeed = options.maxSpeed || 25;
113  }
114
115  ringBell() {
116    console.log('Ring ring!');
117  }
118
119  getDetails() {
120    const electric = this.isElectric ? 'electric' : 'traditional';
121    return `${this.getInfo()} - ${this.gears} gears, ${electric}`;
122  }
123}
124
125// Using Factory Pattern
126const vehicles = [
127  VehicleFactory.createVehicle('car', {
128    brand: 'Toyota',
129    model: 'Corolla',
130    year: 2023,
131    color: 'blue',
132    doors: 4,
133    fuelType: 'hybrid'
134  }),
135
136  VehicleFactory.createVehicle('motorcycle', {
137    brand: 'Harley-Davidson',
138    model: 'Street 750',
139    color: 'black',
140    engineSize: 750
141  }),
142
143  VehicleFactory.createVehicle('bicycle', {
144    brand: 'Trek',
145    model: 'FX 3',
146    color: 'red',
147    gears: 24,
148    isElectric: false
149  })
150];
151
152vehicles.forEach(vehicle => {
153  console.log(vehicle.getDetails());
154  vehicle.start();
155});

Abstract Factory Pattern

1// Interfaces for Abstract Factory
2interface Button {
3  render(): string;
4  onClick(): void;
5}
6
7interface Input {
8  render(): string;
9  getValue(): string;
10  setValue(value: string): void;
11}
12
13interface Modal {
14  render(): string;
15  show(): void;
16  hide(): void;
17}
18
19// Abstract Factory Interface
20interface UIFactory {
21  createButton(text: string): Button;
22  createInput(placeholder: string): Input;
23  createModal(title: string): Modal;
24}
25
26// Material Design implementations
27class MaterialButton implements Button {
28  constructor(private text: string) {}
29
30  render(): string {
31    return `<button class="material-button">${this.text}</button>`;
32  }
33
34  onClick(): void {
35    console.log(`Material button '${this.text}' clicked`);
36  }
37}
38
39class MaterialInput implements Input {
40  private value: string = '';
41
42  constructor(private placeholder: string) {}
43
44  render(): string {
45    return `<input class="material-input" placeholder="${this.placeholder}" />`;
46  }
47
48  getValue(): string {
49    return this.value;
50  }
51
52  setValue(value: string): void {
53    this.value = value;
54    console.log(`Material input value set to: ${value}`);
55  }
56}
57
58class MaterialModal implements Modal {
59  private isVisible: boolean = false;
60
61  constructor(private title: string) {}
62
63  render(): string {
64    return `
65      <div class="material-modal">
66        <div class="material-modal-header">${this.title}</div>
67        <div class="material-modal-content"></div>
68      </div>
69    `;
70  }
71
72  show(): void {
73    this.isVisible = true;
74    console.log(`Material modal '${this.title}' shown`);
75  }
76
77  hide(): void {
78    this.isVisible = false;
79    console.log(`Material modal '${this.title}' hidden`);
80  }
81}
82
83// Bootstrap implementations
84class BootstrapButton implements Button {
85  constructor(private text: string) {}
86
87  render(): string {
88    return `<button class="btn btn-primary">${this.text}</button>`;
89  }
90
91  onClick(): void {
92    console.log(`Bootstrap button '${this.text}' clicked`);
93  }
94}
95
96class BootstrapInput implements Input {
97  private value: string = '';
98
99  constructor(private placeholder: string) {}
100
101  render(): string {
102    return `<input class="form-control" placeholder="${this.placeholder}" />`;
103  }
104
105  getValue(): string {
106    return this.value;
107  }
108
109  setValue(value: string): void {
110    this.value = value;
111    console.log(`Bootstrap input value set to: ${value}`);
112  }
113}
114
115class BootstrapModal implements Modal {
116  private isVisible: boolean = false;
117
118  constructor(private title: string) {}
119
120  render(): string {
121    return `
122      <div class="modal">
123        <div class="modal-dialog">
124          <div class="modal-content">
125            <div class="modal-header">${this.title}</div>
126            <div class="modal-body"></div>
127          </div>
128        </div>
129      </div>
130    `;
131  }
132
133  show(): void {
134    this.isVisible = true;
135    console.log(`Bootstrap modal '${this.title}' shown`);
136  }
137
138  hide(): void {
139    this.isVisible = false;
140    console.log(`Bootstrap modal '${this.title}' hidden`);
141  }
142}
143
144// Concrete Factories
145class MaterialUIFactory implements UIFactory {
146  createButton(text: string): Button {
147    return new MaterialButton(text);
148  }
149
150  createInput(placeholder: string): Input {
151    return new MaterialInput(placeholder);
152  }
153
154  createModal(title: string): Modal {
155    return new MaterialModal(title);
156  }
157}
158
159class BootstrapUIFactory implements UIFactory {
160  createButton(text: string): Button {
161    return new BootstrapButton(text);
162  }
163
164  createInput(placeholder: string): Input {
165    return new BootstrapInput(placeholder);
166  }
167
168  createModal(title: string): Modal {
169    return new BootstrapModal(title);
170  }
171}
172
173// Client class using Factory
174class Application {
175  private factory: UIFactory;
176
177  constructor(theme: 'material' | 'bootstrap') {
178    this.factory = theme === 'material'
179      ? new MaterialUIFactory()
180      : new BootstrapUIFactory();
181  }
182
183  createLoginForm(): void {
184    const emailInput = this.factory.createInput('Email');
185    const passwordInput = this.factory.createInput('Password');
186    const loginButton = this.factory.createButton('Log in');
187    const modal = this.factory.createModal('Login');
188
189    console.log('Login form:');
190    console.log(emailInput.render());
191    console.log(passwordInput.render());
192    console.log(loginButton.render());
193
194    // Simulating interaction
195    emailInput.setValue('user@example.com');
196    passwordInput.setValue('password123');
197    loginButton.onClick();
198    modal.show();
199  }
200}
201
202// Using Abstract Factory
203const materialApp = new Application('material');
204materialApp.createLoginForm();
205
206console.log('\n---\n');
207
208const bootstrapApp = new Application('bootstrap');
209bootstrapApp.createLoginForm();

Observer Pattern - Programmation réactive

L'Observer Pattern est un pattern comportemental qui définit une dépendance un-à-plusieurs entre objets. Lorsque l'état d'un objet change, tous les objets qui en dépendent sont automatiquement notifiés et mis à jour.

Implémentation de base de l'Observer Pattern

1// Subject class (Observable)
2class Subject {
3  constructor() {
4    this.observers = [];
5  }
6
7  subscribe(observer) {
8    if (typeof observer.update === 'function') {
9      this.observers.push(observer);
10      console.log('Observer has been added');
11    } else {
12      throw new Error('Observer must implement the update() method');
13    }
14  }
15
16  unsubscribe(observer) {
17    const index = this.observers.indexOf(observer);
18    if (index > -1) {
19      this.observers.splice(index, 1);
20      console.log('Observer has been removed');
21    }
22  }
23
24  notify(data) {
25    console.log(`Notifying ${this.observers.length} observers`);
26    this.observers.forEach(observer => {
27      observer.update(data);
28    });
29  }
30}
31
32// Concrete Subject - Stock market monitor
33class StockPrice extends Subject {
34  constructor(symbol, price) {
35    super();
36    this.symbol = symbol;
37    this.price = price;
38    this.history = [{ price, timestamp: new Date() }];
39  }
40
41  setPrice(newPrice) {
42    const oldPrice = this.price;
43    this.price = newPrice;
44
45    const change = newPrice - oldPrice;
46    const changePercent = ((change / oldPrice) * 100).toFixed(2);
47
48    const priceData = {
49      symbol: this.symbol,
50      oldPrice,
51      newPrice,
52      change,
53      changePercent,
54      timestamp: new Date()
55    };
56
57    this.history.push({
58      price: newPrice,
59      timestamp: priceData.timestamp
60    });
61
62    this.notify(priceData);
63  }
64
65  getPrice() {
66    return this.price;
67  }
68
69  getHistory() {
70    return [...this.history];
71  }
72}
73
74// Observers (subscribers)
75class PriceDisplay {
76  constructor(name) {
77    this.name = name;
78  }
79
80  update(priceData) {
81    const direction = priceData.change >= 0 ? '▲' : '▼';
82    const changeText = priceData.change >= 0 ? '+' : '';
83
84    console.log(`[${this.name}] ${priceData.symbol}: ${priceData.newPrice} USD ` +
85               `(${changeText}${priceData.change.toFixed(2)} / ${changeText}${priceData.changePercent}%) ${direction}`);
86  }
87}
88
89class PriceAlert {
90  constructor(thresholdPrice, type = 'above') {
91    this.thresholdPrice = thresholdPrice;
92    this.type = type; // 'above' or 'below'
93    this.alerted = false;
94  }
95
96  update(priceData) {
97    const shouldAlert = this.type === 'above'
98      ? priceData.newPrice >= this.thresholdPrice
99      : priceData.newPrice <= this.thresholdPrice;
100
101    if (shouldAlert && !this.alerted) {
102      console.log(`ALERT: ${priceData.symbol} ${this.type === 'above' ? 'exceeded' : 'dropped below'} ` +
103                 `${this.thresholdPrice} USD (current price: ${priceData.newPrice} USD)`);
104      this.alerted = true;
105    } else if (!shouldAlert) {
106      this.alerted = false;
107    }
108  }
109}
110
111class PortfolioTracker {
112  constructor(shares) {
113    this.shares = shares;
114    this.initialValue = null;
115  }
116
117  update(priceData) {
118    const currentValue = priceData.newPrice * this.shares;
119
120    if (this.initialValue === null) {
121      this.initialValue = priceData.oldPrice * this.shares;
122    }
123
124    const profit = currentValue - this.initialValue;
125    const profitPercent = ((profit / this.initialValue) * 100).toFixed(2);
126
127    console.log(`Portfolio: ${this.shares} shares of ${priceData.symbol} = ${currentValue.toFixed(2)} USD ` +
128               `(profit/loss: ${profit.toFixed(2)} USD / ${profitPercent}%)`);
129  }
130}
131
132// Using Observer Pattern
133const teslaStock = new StockPrice('TSLA', 250.00);
134
135// Creating observers
136const mainDisplay = new PriceDisplay('Main screen');
137const mobileApp = new PriceDisplay('Mobile app');
138const highPriceAlert = new PriceAlert(260, 'above');
139const lowPriceAlert = new PriceAlert(240, 'below');
140const myPortfolio = new PortfolioTracker(10);
141
142// Subscribing observers
143teslaStock.subscribe(mainDisplay);
144teslaStock.subscribe(mobileApp);
145teslaStock.subscribe(highPriceAlert);
146teslaStock.subscribe(lowPriceAlert);
147teslaStock.subscribe(myPortfolio);
148
149// Simulating price changes
150console.log('=== TSLA Stock Price Change Simulation ===\n');
151
152teslaStock.setPrice(255.75);
153console.log('');
154
155teslaStock.setPrice(262.30);
156console.log('');
157
158teslaStock.setPrice(245.20);
159console.log('');
160
161teslaStock.setPrice(235.80);

Observer Pattern avec un Event Emitter

1// TypeScript implementation with Event Emitter
2interface EventListener<T = any> {
3  (data: T): void;
4}
5
6class EventEmitter<T extends Record<string, any> = Record<string, any>> {
7  private listeners: Map<keyof T, Set<EventListener<any>>> = new Map();
8
9  on<K extends keyof T>(event: K, listener: EventListener<T[K]>): () => void {
10    if (!this.listeners.has(event)) {
11      this.listeners.set(event, new Set());
12    }
13
14    this.listeners.get(event)!.add(listener);
15
16    // Return an unsubscribe function
17    return () => this.off(event, listener);
18  }
19
20  off<K extends keyof T>(event: K, listener: EventListener<T[K]>): void {
21    const eventListeners = this.listeners.get(event);
22    if (eventListeners) {
23      eventListeners.delete(listener);
24      if (eventListeners.size === 0) {
25        this.listeners.delete(event);
26      }
27    }
28  }
29
30  emit<K extends keyof T>(event: K, data: T[K]): void {
31    const eventListeners = this.listeners.get(event);
32    if (eventListeners) {
33      eventListeners.forEach(listener => {
34        try {
35          listener(data);
36        } catch (error) {
37          console.error(`Error in event listener for ${String(event)}:`, error);
38        }
39      });
40    }
41  }
42
43  once<K extends keyof T>(event: K, listener: EventListener<T[K]>): void {
44    const onceListener = (data: T[K]) => {
45      listener(data);
46      this.off(event, onceListener);
47    };
48
49    this.on(event, onceListener);
50  }
51
52  removeAllListeners<K extends keyof T>(event?: K): void {
53    if (event) {
54      this.listeners.delete(event);
55    } else {
56      this.listeners.clear();
57    }
58  }
59
60  listenerCount<K extends keyof T>(event: K): number {
61    const eventListeners = this.listeners.get(event);
62    return eventListeners ? eventListeners.size : 0;
63  }
64}
65
66// Event type definitions
67interface UserEvents {
68  login: { userId: string; email: string; timestamp: Date };
69  logout: { userId: string; timestamp: Date };
70  profileUpdate: { userId: string; changes: Record<string, any> };
71  passwordChange: { userId: string; timestamp: Date };
72}
73
74// Usage example with TypeScript
75class UserService extends EventEmitter<UserEvents> {
76  private users: Map<string, any> = new Map();
77
78  login(email: string, password: string): boolean {
79    // Simulating login
80    const userId = 'user_' + Math.random().toString(36).slice(2, 11);
81
82    this.users.set(userId, {
83      id: userId,
84      email,
85      lastLogin: new Date()
86    });
87
88    this.emit('login', {
89      userId,
90      email,
91      timestamp: new Date()
92    });
93
94    return true;
95  }
96
97  logout(userId: string): void {
98    if (this.users.has(userId)) {
99      this.emit('logout', {
100        userId,
101        timestamp: new Date()
102      });
103
104      this.users.delete(userId);
105    }
106  }
107
108  updateProfile(userId: string, changes: Record<string, any>): void {
109    if (this.users.has(userId)) {
110      const user = this.users.get(userId);
111      const updatedUser = { ...user, ...changes };
112      this.users.set(userId, updatedUser);
113
114      this.emit('profileUpdate', {
115        userId,
116        changes
117      });
118    }
119  }
120
121  changePassword(userId: string, newPassword: string): void {
122    if (this.users.has(userId)) {
123      // Simulating password change
124      this.emit('passwordChange', {
125        userId,
126        timestamp: new Date()
127      });
128    }
129  }
130}
131
132// Usage
133const userService = new UserService();
134
135// Listening for events
136const unsubscribeLogin = userService.on('login', (data) => {
137  console.log(`User ${data.email} logged in at ${data.timestamp.toLocaleTimeString()}`);
138});
139
140userService.on('logout', (data) => {
141  console.log(`User ${data.userId} logged out at ${data.timestamp.toLocaleTimeString()}`);
142});
143
144userService.on('profileUpdate', (data) => {
145  console.log(`User ${data.userId} updated their profile:`, data.changes);
146});
147
148userService.once('passwordChange', (data) => {
149  console.log(`Password has been changed for user ${data.userId}`);
150});
151
152// Testing
153console.log('=== Test User Service Events ===\n');
154
155const success = userService.login('jan@example.com', 'password123');
156if (success) {
157  const userId = Array.from(userService['users'].keys())[0];
158
159  setTimeout(() => {
160    userService.updateProfile(userId, {
161      name: 'Jan Kowalski',
162      age: 30
163    });
164  }, 1000);
165
166  setTimeout(() => {
167    userService.changePassword(userId, 'newPassword456');
168  }, 2000);
169
170  setTimeout(() => {
171    userService.logout(userId);
172  }, 3000);
173}

Comparaison des patterns et bonnes pratiques

Quand utiliser quel pattern ?

  1. Module Pattern / Revealing Module Pattern :

    • À utiliser quand tu as besoin d'encapsulation et de confidentialité
    • À utiliser pour créer des singletons
    • À utiliser pour organiser le code en espaces de noms
    • À éviter quand tu as besoin de plusieurs instances
    • À éviter quand tu as besoin d'héritage
  2. Factory Pattern :

    • À utiliser quand le type d'objet est déterminé à l'exécution
    • À utiliser pour unifier le processus de création d'objets
    • À utiliser quand tu as de nombreuses classes similaires
    • À éviter quand tu n'as qu'une seule classe à créer
    • À éviter pour des objets simples
  3. Observer Pattern :

    • À utiliser pour la programmation réactive
    • À utiliser quand les changements d'un objet affectent plusieurs autres
    • À utiliser pour mettre en place une architecture événementielle
    • À éviter quand les relations sont simples et directes
    • À éviter quand l'émetteur attend un résultat du destinataire : un simple appel de méthode suffit alors

Combiner les patterns en pratique

Pour finir, un exemple où trois patterns travaillent ensemble : le Module Pattern cache l'état, une méthode statique crée les notifications et l'Observer les envoie aux abonnés.

1// Example of combining patterns in a practical application
2interface NotificationData {
3  message: string;
4  type: 'info' | 'warning' | 'error' | 'success';
5  timestamp: Date;
6}
7
8// Singleton with Module Pattern + Observer Pattern
9const NotificationSystem = (function() {
10  // Private variables
11  let instance: NotificationSystem | null = null;
12  let notifications: NotificationData[] = [];
13  let observers: Array<(notification: NotificationData) => void> = [];
14
15  class NotificationSystem {
16    // Static factory method (a simple factory, not the Factory Method pattern)
17    static createNotification(message: string, type: NotificationData['type']): NotificationData {
18      return {
19        message,
20        type,
21        timestamp: new Date()
22      };
23    }
24
25    addNotification(message: string, type: NotificationData['type']): void {
26      const notification = NotificationSystem.createNotification(message, type);
27      notifications.push(notification);
28      this.notifyObservers(notification);
29    }
30
31    // Observer Pattern methods
32    subscribe(callback: (notification: NotificationData) => void): () => void {
33      observers.push(callback);
34
35      return () => {
36        const index = observers.indexOf(callback);
37        if (index > -1) {
38          observers.splice(index, 1);
39        }
40      };
41    }
42
43    private notifyObservers(notification: NotificationData): void {
44      observers.forEach(observer => observer(notification));
45    }
46
47    getNotifications(): NotificationData[] {
48      return [...notifications];
49    }
50
51    clearNotifications(): void {
52      notifications = [];
53    }
54  }
55
56  // Singleton implementation
57  return {
58    getInstance(): NotificationSystem {
59      if (!instance) {
60        instance = new NotificationSystem();
61      }
62      return instance;
63    }
64  };
65})();
66
67// Using combined patterns
68const notificationService = NotificationSystem.getInstance();
69
70// Subscribers (Observer Pattern)
71const unsubscribe1 = notificationService.subscribe((notification) => {
72  console.log(`[UI] ${notification.type.toUpperCase()}: ${notification.message}`);
73});
74
75const unsubscribe2 = notificationService.subscribe((notification) => {
76  if (notification.type === 'error') {
77    console.log(`[LOG] Error logged: ${notification.message} at ${notification.timestamp}`);
78  }
79});
80
81// Testing
82notificationService.addNotification('Welcome to the application!', 'info');
83notificationService.addNotification('Warning: Low disk space', 'warning');
84notificationService.addNotification('Server connection error', 'error');
85notificationService.addNotification('Data saved successfully', 'success');

Résumé

Les patterns Module, Factory et Observer sont les fondations de la programmation moderne en JavaScript/TypeScript :

  • Module Pattern assure l'encapsulation et l'organisation du code
  • Revealing Module Pattern améliore la lisibilité grâce à une définition claire de l'API publique
  • Factory Pattern abstrait le processus de création d'objets
  • Observer Pattern permet la programmation réactive et un couplage faible

Tout comme les bâtisseurs de l'Antiquité utilisaient des schémas architecturaux éprouvés, les développeurs modernes utilisent ces patterns de conception pour créer des applications solides et évolutives. La maîtrise de ces patterns est essentielle pour tout développeur JavaScript/TypeScript.

Code de cette leçon : index.js
1// Pattern Module - système de gestion du parc
2// Exercice : implémente la classe ParkCore avec un système de modules
3
4class ParkCore {
5  constructor(name) {
6    this.name = name;
7    this.modules = new Map();
8    this.isRunning = false;
9  }
10
11  // Implémente les méthodes :
12  // registerModule(name, module) - enregistre un module
13  // getModule(name) - retourne un module
14  // start() - démarre tous les modules
15
16  registerModule(name, module) {
17    // Ton code ici
18  }
19
20  getModule(name) {
21    // Ton code ici
22  }
23
24  start() {
25    // Ton code ici
26  }
27}
28
29const park = new ParkCore("Jurassic Park");
30console.log("Park:", park.name);

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. Quel pattern de conception est le mieux adapté pour garantir qu'une seule instance du système de sécurité existe ?

Articles utiles