Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Wzorce projektowe w JavaScript/TypeScript - Module, Factory i Observer

UWAGA: Materiał rozbudowany - Plan nauki

Ten moduł zawiera bardzo rozbudowany materiał edukacyjny. Nie musisz przyswajać wszystkiego na raz! Oto sugerowana kolejność nauki:

Etap 1 - Fundamenty (priorytet: wysoki)

  • Module Pattern i Revealing Module Pattern
  • Factory Pattern
  • Observer Pattern
  • Singleton Pattern

Etap 2 - Wzorce zaawansowane (priorytet: średni)

  • Strategy Pattern
  • Decorator Pattern
  • Command Pattern
  • Prototype Pattern

Etap 3 - Architektura i best practices (priorytet: średni)

  • MVC/MVVM
  • Dependency Injection
  • Repository Pattern

Etap 4 - Praktyka i integracja (do nauki w miarę potrzeb)

  • Reactive Programming (RxJS)
  • State Management Patterns
  • Real-world aplikacje

Wskazówki:

  • Przyswajaj materiał po kolei, dział po dziale
  • Po każdym wzorcu zrób przerwę i przećwicz go samodzielnie
  • Wracaj do trudniejszych tematów po czasie
  • Nie przeskakuj fundamentów - są kluczowe dla zrozumienia reszty
  • Zaawansowane wzorce możesz studiować w miarę potrzeb w projektach

Pamiętaj: lepiej dobrze zrozumieć podstawy niż pobieżnie przejrzeć wszystko!


Tak jak starożytni architekci wykorzystywali sprawdzone wzorce konstrukcyjne do budowania trwałych świątyń, współcześni programiści używają wzorców projektowych do tworzenia solidnego, skalowalnego kodu. W tej lekcji poznamy cztery fundamentalne wzorce: Module Pattern, Revealing Module Pattern, Factory Pattern oraz Observer Pattern, które stanowią podstawę wielu nowoczesnych aplikacji JavaScript i TypeScript.

Module Pattern - Enkapsulacja i prywatność

Module Pattern to jeden z najważniejszych wzorców w JavaScript, który pozwala na enkapsulację kodu i tworzenie prywatnych zmiennych oraz metod. Przed wprowadzeniem ES6 modules, był to główny sposób organizowania kodu w modułach.

Klasyczna implementacja Module Pattern

1// Przykład prostego modułu kalkulatora
2const Calculator = (function() {
3  // Prywatne zmienne i funkcje
4  let result = 0;
5  
6  function validateNumber(num) {
7    if (typeof num !== 'number' || isNaN(num)) {
8      throw new Error('Podana wartość nie jest liczbą');
9    }
10  }
11  
12  function logOperation(operation, num) {
13    console.log(`Wykonano operację: ${operation} ${num}`);
14  }
15  
16  // Publiczny interfejs
17  return {
18    add: function(num) {
19      validateNumber(num);
20      result += num;
21      logOperation('dodawanie', num);
22      return this;
23    },
24    
25    subtract: function(num) {
26      validateNumber(num);
27      result -= num;
28      logOperation('odejmowanie', num);
29      return this;
30    },
31    
32    multiply: function(num) {
33      validateNumber(num);
34      result *= num;
35      logOperation('mnożenie', num);
36      return this;
37    },
38    
39    divide: function(num) {
40      validateNumber(num);
41      if (num === 0) {
42        throw new Error('Nie można dzielić przez zero');
43      }
44      result /= num;
45      logOperation('dzielenie', num);
46      return this;
47    },
48    
49    getResult: function() {
50      return result;
51    },
52    
53    reset: function() {
54      result = 0;
55      console.log('Kalkulator został zresetowany');
56      return this;
57    }
58  };
59})();
60
61// Użycie
62Calculator
63  .add(10)
64  .multiply(2)
65  .subtract(5)
66  .divide(3);
67
68console.log(Calculator.getResult()); // 5

Module Pattern z parametrami

1// Moduł do zarządzania stanem aplikacji
2const StateManager = (function(initialState = {}) {
3  // Prywatny stan
4  let state = { ...initialState };
5  let listeners = [];
6  
7  // Prywatne funkcje pomocnicze
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  // Publiczny interfejs
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      // Zwracamy funkcję do odsubskrybowania
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// Użycie
85const unsubscribe = StateManager.subscribe((action, newState) => {
86  console.log('Stan się zmienił:', action);
87});
88
89StateManager
90  .setState('user', { name: 'Jan', age: 30 })
91  .updateState({ theme: 'dark', notifications: true });

Revealing Module Pattern - Ulepszona enkapsulacja

Revealing Module Pattern to ewolucja Module Pattern, która zapewnia większą czytelność i lepsze rozdzielenie między funkcjami prywatnymi a publicznymi.

Podstawowa implementacja Revealing Module Pattern

1// Moduł do zarządzania produktami w sklepie
2const ProductManager = (function() {
3  // Prywatne zmienne
4  let products = [];
5  let nextId = 1;
6  
7  // Prywatne funkcje
8  function generateId() {
9    return nextId++;
10  }
11  
12  function validateProduct(product) {
13    if (!product.name || typeof product.name !== 'string') {
14      throw new Error('Nazwa produktu jest wymagana i musi być tekstem');
15    }
16    
17    if (!product.price || typeof product.price !== 'number' || product.price <= 0) {
18      throw new Error('Cena produktu musi być liczbą większą od zera');
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  // Publiczne funkcje
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(`Dodano produkt: ${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(`Produkt o ID ${id} nie został znaleziony`);
65    }
66    
67    const removedProduct = products.splice(index, 1)[0];
68    console.log(`Usunięto produkt: ${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(`Produkt o ID ${id} nie został znaleziony`);
78    }
79    
80    const updatedProduct = { ...products[index], ...updates };
81    validateProduct(updatedProduct);
82    
83    products[index] = updatedProduct;
84    console.log(`Zaktualizowano produkt: ${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(`Usunięto wszystkie produkty (${count})`);
115  }
116  
117  // Revealing - exposing public methods
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// Użycie
132const laptopId = ProductManager.add({
133  name: 'Laptop Dell XPS 13',
134  price: 4500,
135  category: 'Elektronika',
136  description: 'Wysokiej jakości laptop ultrabook'
137});
138
139const phoneId = ProductManager.add({
140  name: 'iPhone 15 Pro',
141  price: 5500,
142  category: 'Elektronika'
143});
144
145console.log('Produkty elektroniczne:', ProductManager.getByCategory('Elektronika'));
146console.log('Produkty posortowane:', ProductManager.getSortedByPrice());

TypeScript z Revealing Module Pattern

1// Interfejsy dla lepszej typizacji
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// Moduł zarządzania użytkownikami w 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  // Prywatne zmienne
36  let users: User[] = [];
37  let nextId: number = 1;
38  
39  // Prywatne funkcje
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  // Publiczne funkcje
72  function createUser(userData: UserCreateData): number {
73    if (!validateEmail(userData.email)) {
74      throw new Error('Nieprawidłowy format email');
75    }
76    
77    if (isEmailTaken(userData.email)) {
78      throw new Error('Użytkownik z tym emailem już istnieje');
79    }
80    
81    if (!userData.name || userData.name.trim().length < 2) {
82      throw new Error('Nazwa użytkownika musi mieć co najmniej 2 znaki');
83    }
84    
85    const user = createUserObject(userData);
86    users.push(user);
87    
88    console.log(`Utworzono użytkownika: ${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('Nazwa użytkownika musi mieć co najmniej 2 znaki');
112    }
113    
114    users[index] = updatedUser;
115    console.log(`Zaktualizowano użytkownika: ${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(`Usunięto użytkownika: ${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 - eksponowanie publicznych metod
161  return {
162    createUser,
163    getUserById,
164    updateUser,
165    deleteUser,
166    getAllUsers,
167    getActiveUsers,
168    getUsersByRole,
169    markUserLogin,
170    getUserCount
171  };
172})();
173
174// Użycie z 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('Administratorzy:', UserManager.getUsersByRole('admin'));
187console.log('Aktywni użytkownicy:', UserManager.getActiveUsers());

Factory Pattern - Tworzenie obiektów

Factory Pattern to wzorzec kreacyjny, który pozwala na tworzenie obiektów bez określania konkretnej klasy obiektu, który ma zostać utworzony. Jest szczególnie przydatny gdy mamy różne typy obiektów o podobnej strukturze.

Prosty Factory Pattern

1// Factory do tworzenia różnych typów pojazdów
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(`Nieznany typ pojazdu: ${type}`);
15    }
16  }
17  
18  static getSupportedTypes() {
19    return ['car', 'motorcycle', 'truck', 'bicycle'];
20  }
21}
22
23// Klasy bazowe
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} został uruchomiony`);
35  }
36  
37  stop() {
38    console.log(`${this.brand} ${this.model} został zatrzymany`);
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 = 'Samochód';
50    this.doors = options.doors || 4;
51    this.fuelType = options.fuelType || 'benzyna';
52    this.transmission = options.transmission || 'manual';
53  }
54  
55  openTrunk() {
56    console.log('Bagażnik został otwarty');
57  }
58  
59  getDetails() {
60    return `${this.getInfo()} - ${this.doors} drzwi, ${this.fuelType}, ${this.transmission}`;
61  }
62}
63
64class Motorcycle extends Vehicle {
65  constructor(options = {}) {
66    super(options);
67    this.type = 'Motocykl';
68    this.engineSize = options.engineSize || 125; // cm³
69    this.hasSidecar = options.hasSidecar || false;
70  }
71  
72  wheelie() {
73    console.log('Wykonuję wheelie!');
74  }
75  
76  getDetails() {
77    const sidecar = this.hasSidecar ? 'z przyczepką' : 'bez przyczepki';
78    return `${this.getInfo()} - ${this.engineSize}cm³, ${sidecar}`;
79  }
80}
81
82class Truck extends Vehicle {
83  constructor(options = {}) {
84    super(options);
85    this.type = 'Ciężarówka';
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(`Nie można załadować ${weight}kg. Maksymalna ładowność: ${this.loadCapacity}kg`);
93      return false;
94    }
95    
96    console.log(`Załadowano ${weight}kg cargo`);
97    return true;
98  }
99  
100  getDetails() {
101    const trailer = this.trailerAttached ? 'z przyczepą' : 'bez przyczepy';
102    return `${this.getInfo()} - ładowność: ${this.loadCapacity}kg, ${trailer}`;
103  }
104}
105
106class Bicycle extends Vehicle {
107  constructor(options = {}) {
108    super(options);
109    this.type = 'Rower';
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('Dzyn dzyn! 🔔');
117  }
118  
119  getDetails() {
120    const electric = this.isElectric ? 'elektryczny' : 'tradycyjny';
121    return `${this.getInfo()} - ${this.gears} biegów, ${electric}`;
122  }
123}
124
125// Użycie Factory Pattern
126const vehicles = [
127  VehicleFactory.createVehicle('car', {
128    brand: 'Toyota',
129    model: 'Corolla',
130    year: 2023,
131    color: 'niebieski',
132    doors: 4,
133    fuelType: 'hybrid'
134  }),
135  
136  VehicleFactory.createVehicle('motorcycle', {
137    brand: 'Harley-Davidson',
138    model: 'Street 750',
139    color: 'czarny',
140    engineSize: 750
141  }),
142  
143  VehicleFactory.createVehicle('bicycle', {
144    brand: 'Trek',
145    model: 'FX 3',
146    color: 'czerwony',
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// Interfejsy dla 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 implementacje
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 implementacje
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// Klasa klienta używająca 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('Hasło');
186    const loginButton = this.factory.createButton('Zaloguj');
187    const modal = this.factory.createModal('Logowanie');
188    
189    console.log('Formularz logowania:');
190    console.log(emailInput.render());
191    console.log(passwordInput.render());
192    console.log(loginButton.render());
193    
194    // Symulacja interakcji
195    emailInput.setValue('user@example.com');
196    passwordInput.setValue('password123');
197    loginButton.onClick();
198    modal.show();
199  }
200}
201
202// Użycie Abstract Factory
203const materialApp = new Application('material');
204materialApp.createLoginForm();
205
206console.log('\n---\n');
207
208const bootstrapApp = new Application('bootstrap');
209bootstrapApp.createLoginForm();

Observer Pattern - Reaktywne programowanie

Observer Pattern to wzorzec behawioralny, który definiuje zależność jeden-do-wielu między obiektami. Gdy stan jednego obiektu się zmienia, wszystkie zależne obiekty są automatycznie powiadamiane i aktualizowane.

Podstawowa implementacja Observer Pattern

1// Klasa Subject (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 został dodany');
11    } else {
12      throw new Error('Observer musi implementować metodę update()');
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 został usunięty');
21    }
22  }
23  
24  notify(data) {
25    console.log(`Powiadamianie ${this.observers.length} obserwatorów`);
26    this.observers.forEach(observer => {
27      observer.update(data);
28    });
29  }
30}
31
32// Konkretny Subject - Monitor giełdowy
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// Observery (subskrybenci)
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} PLN ` +
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' lub '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' ? 'przekroczył' : 'spadł poniżej'} ` +
103                 `${this.thresholdPrice} PLN (aktualna cena: ${priceData.newPrice} PLN)`);
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} akcji ${priceData.symbol} = ${currentValue.toFixed(2)} PLN ` +
128               `(zysk/strata: ${profit.toFixed(2)} PLN / ${profitPercent}%)`);
129  }
130}
131
132// Użycie Observer Pattern
133const teslaStock = new StockPrice('TSLA', 250.00);
134
135// Tworzenie obserwatorów
136const mainDisplay = new PriceDisplay('Główny ekran');
137const mobileApp = new PriceDisplay('Aplikacja mobilna');
138const highPriceAlert = new PriceAlert(260, 'above');
139const lowPriceAlert = new PriceAlert(240, 'below');
140const myPortfolio = new PortfolioTracker(10);
141
142// Subskrypcja obserwatorów
143teslaStock.subscribe(mainDisplay);
144teslaStock.subscribe(mobileApp);
145teslaStock.subscribe(highPriceAlert);
146teslaStock.subscribe(lowPriceAlert);
147teslaStock.subscribe(myPortfolio);
148
149// Symulacja zmian cen
150console.log('=== Symulacja zmian cen akcji TSLA ===\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 z Event Emitter

1// TypeScript implementacja z 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<T[keyof T]>>> = 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    // Zwracamy funkcję do unsubscribe
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// Definicja typów eventów
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// Przykład użycia z TypeScript
75class UserService extends EventEmitter<UserEvents> {
76  private users: Map<string, any> = new Map();
77  
78  login(email: string, password: string): boolean {
79    // Symulacja logowania
80    const userId = 'user_' + Math.random().toString(36).substr(2, 9);
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      // Symulacja zmiany hasła
124      this.emit('passwordChange', {
125        userId,
126        timestamp: new Date()
127      });
128    }
129  }
130}
131
132// Użycie
133const userService = new UserService();
134
135// Nasłuchiwanie eventów
136const unsubscribeLogin = userService.on('login', (data) => {
137  console.log(`Użytkownik ${data.email} zalogował się o ${data.timestamp.toLocaleTimeString()}`);
138});
139
140userService.on('logout', (data) => {
141  console.log(`Użytkownik ${data.userId} wylogował się o ${data.timestamp.toLocaleTimeString()}`);
142});
143
144userService.on('profileUpdate', (data) => {
145  console.log(`Użytkownik ${data.userId} zaktualizował profil:`, data.changes);
146});
147
148userService.once('passwordChange', (data) => {
149  console.log(`🔒 Hasło zostało zmienione dla użytkownika ${data.userId}`);
150});
151
152// Testowanie
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}

Porównanie wzorców i najlepsze praktyki

Kiedy używać którego wzorca?

  1. Module Pattern / Revealing Module Pattern:

    • ✅ Gdy potrzebujesz enkapsulacji i prywatności
    • ✅ Do tworzenia singletonów
    • ✅ Dla organizacji kodu w przestrzenie nazw
    • ❌ Gdy potrzebujesz wielu instancji
    • ❌ Dla dziedziczenia
  2. Factory Pattern:

    • ✅ Gdy typ obiektu jest określany w runtime
    • ✅ Do unifikacji procesu tworzenia obiektów
    • ✅ Gdy masz wiele podobnych klas
    • ❌ Gdy masz tylko jedną klasę do tworzenia
    • ❌ Dla prostych obiektów
  3. Observer Pattern:

    • ✅ Dla reaktywnego programowania
    • ✅ Gdy zmiany w jednym obiekcie wpływają na wiele innych
    • ✅ Do implementacji event-driven architecture
    • ❌ Gdy masz proste, bezpośrednie relacje
    • ❌ Dla synchronicznych operacji

Zalety i wady

1// Przykład łączenia wzorców w praktycznej aplikacji
2interface NotificationData {
3  message: string;
4  type: 'info' | 'warning' | 'error' | 'success';
5  timestamp: Date;
6}
7
8// Singleton z Module Pattern + Observer Pattern
9const NotificationSystem = (function() {
10  // Prywatne zmienne
11  let instance: NotificationSystem | null = null;
12  let notifications: NotificationData[] = [];
13  let observers: Array<(notification: NotificationData) => void> = [];
14  
15  class NotificationSystem {
16    // 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// Użycie połączonych wzorców
68const notificationService = NotificationSystem.getInstance();
69
70// Subskrybenci (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// Testowanie
82notificationService.addNotification('Witaj w aplikacji!', 'info');
83notificationService.addNotification('Ostrzeżenie: Niska przestrzeń dyskowa', 'warning');
84notificationService.addNotification('Błąd połączenia z serwerem', 'error');
85notificationService.addNotification('Dane zostały zapisane pomyślnie', 'success');

Podsumowanie

Wzorce projektowe Module, Factory i Observer to fundamenty nowoczesnego programowania JavaScript/TypeScript:

  • Module Pattern zapewnia enkapsulację i organizację kodu
  • Revealing Module Pattern poprawia czytelność przez jasne definiowanie publicznego API
  • Factory Pattern abstrakcjonizuje proces tworzenia obiektów
  • Observer Pattern umożliwia reaktywne programowanie i luźne powiązanie

Podobnie jak starożytni budowniczowie używali sprawdzonych wzorców architektonicznych, współcześni programiści wykorzystują te wzorce projektowe do tworzenia solidnych, skalowalnych aplikacji. Znajomość tych wzorców jest kluczowa dla każdego programisty JavaScript/TypeScript.

Ir a CodeWorlds