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 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.
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()); // 51// 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 to ewolucja Module Pattern, która zapewnia większą czytelność i lepsze rozdzielenie między funkcjami prywatnymi a publicznymi.
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());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 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.
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});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 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.
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);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}Module Pattern / Revealing Module Pattern:
Factory Pattern:
Observer Pattern:
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');Wzorce projektowe Module, Factory i Observer to fundamenty nowoczesnego programowania JavaScript/TypeScript:
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.