NOTE: Extended material - Study plan
This module contains very extensive educational material. You don't have to absorb everything at once! Here is a suggested learning order:
Stage 1 - Fundamentals (priority: high)
- Module Pattern and Revealing Module Pattern
- Factory Pattern
- Observer Pattern
- Singleton Pattern
Stage 2 - Advanced patterns (priority: medium)
- Strategy Pattern
- Decorator Pattern
- Command Pattern
- Prototype Pattern
Stage 3 - Architecture and best practices (priority: medium)
- MVC/MVVM
- Dependency Injection
- Repository Pattern
Stage 4 - Practice and integration (study as needed)
- Reactive Programming (RxJS)
- State Management Patterns
- Real-world applications
Tips:
- Absorb the material sequentially, section by section
- After each pattern, take a break and practice it on your own
- Return to more difficult topics after some time
- Don't skip the fundamentals - they are key to understanding the rest
- Advanced patterns can be studied as needed in projects
Remember: it's better to understand the basics well than to skim through everything superficially!
Just as ancient architects used proven construction patterns to build durable temples, modern programmers use design patterns to create solid, scalable code. In this lesson, we will learn four fundamental patterns: Module Pattern, Revealing Module Pattern, Factory Pattern, and Observer Pattern, which form the foundation of many modern JavaScript and TypeScript applications.
The Module Pattern is one of the most important patterns in JavaScript, allowing code encapsulation and the creation of private variables and methods. Before the introduction of ES6 modules, it was the main way to organize code into modules.
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()); // 51// 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 });The Revealing Module Pattern is an evolution of the Module Pattern that provides greater readability and better separation between private and public functions.
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 - 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// 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());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 - exposing public methods
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());The Factory Pattern is a creational pattern that allows creating objects without specifying the concrete class of the object to be created. It is particularly useful when we have different types of objects with a similar structure.
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});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();The Observer Pattern is a behavioral pattern that defines a one-to-many dependency between objects. When the state of one object changes, all dependent objects are automatically notified and updated.
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);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<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 // 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).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 // 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}Module Pattern / Revealing Module Pattern:
Factory Pattern:
Observer Pattern:
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 // 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');The Module, Factory, and Observer design patterns are the foundations of modern JavaScript/TypeScript programming:
Just as ancient builders used proven architectural patterns, modern programmers use these design patterns to create solid, scalable applications. Knowledge of these patterns is essential for every JavaScript/TypeScript programmer.