Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Wzorce zarządzania stanem w JavaScript/TypeScript

Podobnie jak starożytni królowie musieli zarządzać swoimi państwami przy pomocy sprawdzonych systemów administracji, współczesne aplikacje potrzebują skutecznych wzorców zarządzania stanem. W tej lekcji poznamy najważniejsze wzorce: Flux, Event Sourcing i State Machines (maszyny stanów), oraz nauczymy się, jak implementować je w JavaScript/TypeScript dla różnych scenariuszy aplikacji.

Flux Architecture - Jednokierunkowy przepływ danych

Flux to wzorzec architektury aplikacji, który wprowadza jednokierunkowy przepływ danych, co czyni stan aplikacji bardziej przewidywalnym i łatwiejszym do debugowania.

Podstawowe składniki Flux

1// Typy dla Flux
2type Action<T = any> = {
3  type: string;
4  payload?: T;
5};
6
7type Listener = () => void;
8
9// Store - przechowuje stan aplikacji
10class Store<T> {
11  private state: T;
12  private listeners: Listener[] = [];
13
14  constructor(initialState: T) {
15    this.state = initialState;
16  }
17
18  getState(): T {
19    return this.state;
20  }
21
22  protected setState(newState: T): void {
23    this.state = newState;
24    this.emitChange();
25  }
26
27  subscribe(listener: Listener): () => void {
28    this.listeners.push(listener);
29    
30    // Zwracamy funkcję do unsubscribe
31    return () => {
32      const index = this.listeners.indexOf(listener);
33      if (index > -1) {
34        this.listeners.splice(index, 1);
35      }
36    };
37  }
38
39  private emitChange(): void {
40    this.listeners.forEach(listener => listener());
41  }
42}
43
44// Dispatcher - centralny hub dla wszystkich akcji
45class Dispatcher {
46  private callbacks: Array<(action: Action) => void> = [];
47
48  register(callback: (action: Action) => void): string {
49    const id = Math.random().toString(36).substr(2, 9);
50    this.callbacks.push(callback);
51    return id;
52  }
53
54  dispatch(action: Action): void {
55    console.log('Dispatching action:', action);
56    this.callbacks.forEach(callback => {
57      callback(action);
58    });
59  }
60}
61
62// Action Creators - funkcje tworzące akcje
63const TodoActions = {
64  addTodo: (text: string): Action<{ text: string }> => ({
65    type: 'ADD_TODO',
66    payload: { text }
67  }),
68
69  toggleTodo: (id: string): Action<{ id: string }> => ({
70    type: 'TOGGLE_TODO',
71    payload: { id }
72  }),
73
74  deleteTodo: (id: string): Action<{ id: string }> => ({
75    type: 'DELETE_TODO',
76    payload: { id }
77  }),
78
79  setFilter: (filter: 'all' | 'active' | 'completed'): Action<{ filter: string }> => ({
80    type: 'SET_FILTER',
81    payload: { filter }
82  })
83};
84
85// Stan aplikacji
86interface Todo {
87  id: string;
88  text: string;
89  completed: boolean;
90  createdAt: Date;
91}
92
93interface TodoState {
94  todos: Todo[];
95  filter: 'all' | 'active' | 'completed';
96}
97
98// Implementacja TodoStore
99class TodoStore extends Store<TodoState> {
100  constructor(dispatcher: Dispatcher) {
101    super({
102      todos: [],
103      filter: 'all'
104    });
105
106    // Rejestracja w dispatcherze
107    dispatcher.register((action: Action) => {
108      this.handleAction(action);
109    });
110  }
111
112  private handleAction(action: Action): void {
113    const currentState = this.getState();
114
115    switch (action.type) {
116      case 'ADD_TODO':
117        this.setState({
118          ...currentState,
119          todos: [
120            ...currentState.todos,
121            {
122              id: Math.random().toString(36).substr(2, 9),
123              text: action.payload.text,
124              completed: false,
125              createdAt: new Date()
126            }
127          ]
128        });
129        break;
130
131      case 'TOGGLE_TODO':
132        this.setState({
133          ...currentState,
134          todos: currentState.todos.map(todo =>
135            todo.id === action.payload.id
136              ? { ...todo, completed: !todo.completed }
137              : todo
138          )
139        });
140        break;
141
142      case 'DELETE_TODO':
143        this.setState({
144          ...currentState,
145          todos: currentState.todos.filter(todo => todo.id !== action.payload.id)
146        });
147        break;
148
149      case 'SET_FILTER':
150        this.setState({
151          ...currentState,
152          filter: action.payload.filter
153        });
154        break;
155
156      default:
157        // Brak zmian stanu dla nieznanych akcji
158        break;
159    }
160  }
161
162  // Selektory - funkcje do pobierania przetworzonych danych
163  getFilteredTodos(): Todo[] {
164    const { todos, filter } = this.getState();
165
166    switch (filter) {
167      case 'active':
168        return todos.filter(todo => !todo.completed);
169      case 'completed':
170        return todos.filter(todo => todo.completed);
171      default:
172        return todos;
173    }
174  }
175
176  getStats() {
177    const todos = this.getState().todos;
178    return {
179      total: todos.length,
180      completed: todos.filter(todo => todo.completed).length,
181      active: todos.filter(todo => !todo.completed).length
182    };
183  }
184}
185
186// Użycie Flux
187const dispatcher = new Dispatcher();
188const todoStore = new TodoStore(dispatcher);
189
190// Subskrypcja na zmiany
191const unsubscribe = todoStore.subscribe(() => {
192  console.log('Stan się zmienił:', {
193    todos: todoStore.getFilteredTodos(),
194    stats: todoStore.getStats(),
195    filter: todoStore.getState().filter
196  });
197});
198
199// Wykonywanie akcji
200dispatcher.dispatch(TodoActions.addTodo('Nauka Flux'));
201dispatcher.dispatch(TodoActions.addTodo('Implementacja TodoApp'));
202dispatcher.dispatch(TodoActions.toggleTodo(todoStore.getState().todos[0].id));
203dispatcher.dispatch(TodoActions.setFilter('active'));

Zaawansowany Flux z Middleware

1// Middleware dla Flux
2type Middleware = (action: Action, next: (action: Action) => void) => void;
3
4class EnhancedDispatcher extends Dispatcher {
5  private middlewares: Middleware[] = [];
6
7  use(middleware: Middleware): void {
8    this.middlewares.push(middleware);
9  }
10
11  dispatch(action: Action): void {
12    let index = 0;
13
14    const next = (currentAction: Action) => {
15      if (index < this.middlewares.length) {
16        const middleware = this.middlewares[index++];
17        middleware(currentAction, next);
18      } else {
19        super.dispatch(currentAction);
20      }
21    };
22
23    next(action);
24  }
25}
26
27// Middleware do logowania
28const loggingMiddleware: Middleware = (action, next) => {
29  console.group(`Action: ${action.type}`);
30  console.log('Payload:', action.payload);
31  console.time('Action Duration');
32  
33  next(action);
34  
35  console.timeEnd('Action Duration');
36  console.groupEnd();
37};
38
39// Middleware do debugowania
40const debugMiddleware: Middleware = (action, next) => {
41  if (process.env.NODE_ENV === 'development') {
42    console.log('Debug: Before action', action);
43  }
44  
45  next(action);
46  
47  if (process.env.NODE_ENV === 'development') {
48    console.log('Debug: After action', action);
49  }
50};
51
52// Middleware do obsługi async akcji
53const asyncMiddleware: Middleware = (action, next) => {
54  if (typeof action.payload === 'function') {
55    // To jest async action
56    action.payload((asyncAction: Action) => {
57      next(asyncAction);
58    });
59  } else {
60    next(action);
61  }
62};
63
64// Async Action Creator
65const AsyncTodoActions = {
66  loadTodos: () => ({
67    type: 'ASYNC_ACTION',
68    payload: (dispatch: (action: Action) => void) => {
69      // Symulacja API call
70      setTimeout(() => {
71        dispatch({
72          type: 'LOAD_TODOS_SUCCESS',
73          payload: {
74            todos: [
75              { id: '1', text: 'Todo z API', completed: false, createdAt: new Date() }
76            ]
77          }
78        });
79      }, 1000);
80    }
81  })
82};
83
84// Użycie Enhanced Dispatcher
85const enhancedDispatcher = new EnhancedDispatcher();
86enhancedDispatcher.use(loggingMiddleware);
87enhancedDispatcher.use(debugMiddleware);
88enhancedDispatcher.use(asyncMiddleware);
89
90const enhancedTodoStore = new TodoStore(enhancedDispatcher);

Event Sourcing - Historia zdarzeń jako źródło prawdy

Event Sourcing to wzorzec, w którym stan aplikacji jest określany przez sekwencję zdarzeń, zamiast przechowywania aktualnego stanu bezpośrednio.

Podstawowa implementacja Event Sourcing

1// Typy dla Event Sourcing
2interface Event {
3  id: string;
4  type: string;
5  data: any;
6  timestamp: Date;
7  version: number;
8}
9
10interface Aggregate {
11  id: string;
12  version: number;
13  apply(event: Event): void;
14}
15
16// Event Store - przechowuje wszystkie eventy
17class EventStore {
18  private events: Map<string, Event[]> = new Map();
19
20  async saveEvents(aggregateId: string, events: Event[], expectedVersion: number): Promise<void> {
21    const existingEvents = this.events.get(aggregateId) || [];
22    
23    if (existingEvents.length !== expectedVersion) {
24      throw new Error('Concurrency conflict: unexpected version');
25    }
26
27    // Dodawanie nowych eventów
28    const newEvents = events.map((event, index) => ({
29      ...event,
30      version: expectedVersion + index + 1,
31      timestamp: new Date()
32    }));
33
34    this.events.set(aggregateId, [...existingEvents, ...newEvents]);
35    console.log(`Saved ${newEvents.length} events for aggregate ${aggregateId}`);
36  }
37
38  async getEvents(aggregateId: string, fromVersion: number = 0): Promise<Event[]> {
39    const events = this.events.get(aggregateId) || [];
40    return events.filter(event => event.version > fromVersion);
41  }
42
43  async getAllEvents(): Promise<Event[]> {
44    const allEvents: Event[] = [];
45    for (const events of this.events.values()) {
46      allEvents.push(...events);
47    }
48    return allEvents.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
49  }
50}
51
52// Definicje eventów
53const UserEvents = {
54  UserCreated: (data: { name: string; email: string }) => ({
55    type: 'UserCreated',
56    data
57  }),
58  
59  UserEmailChanged: (data: { email: string }) => ({
60    type: 'UserEmailChanged',
61    data
62  }),
63  
64  UserDeactivated: (data: { reason: string }) => ({
65    type: 'UserDeactivated',
66    data
67  }),
68  
69  UserReactivated: () => ({
70    type: 'UserReactivated',
71    data: {}
72  })
73};
74
75// User Aggregate
76class User implements Aggregate {
77  id: string;
78  version: number = 0;
79  name: string = '';
80  email: string = '';
81  isActive: boolean = true;
82  createdAt?: Date;
83  deactivatedAt?: Date;
84
85  constructor(id: string) {
86    this.id = id;
87  }
88
89  // Metody do tworzenia eventów (commands)
90  static create(id: string, name: string, email: string): { user: User; events: any[] } {
91    const user = new User(id);
92    const events = [UserEvents.UserCreated({ name, email })];
93    return { user, events };
94  }
95
96  changeEmail(newEmail: string): any[] {
97    if (this.email === newEmail) {
98      return []; // Brak zmian
99    }
100    
101    if (!this.isActive) {
102      throw new Error('Cannot change email for deactivated user');
103    }
104
105    return [UserEvents.UserEmailChanged({ email: newEmail })];
106  }
107
108  deactivate(reason: string): any[] {
109    if (!this.isActive) {
110      return []; // Już nieaktywny
111    }
112
113    return [UserEvents.UserDeactivated({ reason })];
114  }
115
116  reactivate(): any[] {
117    if (this.isActive) {
118      return []; // Już aktywny
119    }
120
121    return [UserEvents.UserReactivated()];
122  }
123
124  // Aplikowanie eventów do zmiany stanu
125  apply(event: Event): void {
126    switch (event.type) {
127      case 'UserCreated':
128        this.name = event.data.name;
129        this.email = event.data.email;
130        this.createdAt = event.timestamp;
131        this.isActive = true;
132        break;
133
134      case 'UserEmailChanged':
135        this.email = event.data.email;
136        break;
137
138      case 'UserDeactivated':
139        this.isActive = false;
140        this.deactivatedAt = event.timestamp;
141        break;
142
143      case 'UserReactivated':
144        this.isActive = true;
145        this.deactivatedAt = undefined;
146        break;
147
148      default:
149        throw new Error(`Unknown event type: ${event.type}`);
150    }
151
152    this.version = event.version;
153  }
154
155  // Odtwarzanie stanu z eventów
156  static fromHistory(id: string, events: Event[]): User {
157    const user = new User(id);
158    events.forEach(event => user.apply(event));
159    return user;
160  }
161}
162
163// Repository dla User Aggregate
164class UserRepository {
165  constructor(private eventStore: EventStore) {}
166
167  async save(user: User, newEvents: any[]): Promise<void> {
168    if (newEvents.length === 0) return;
169
170    const events = newEvents.map(eventData => ({
171      id: Math.random().toString(36).substr(2, 9),
172      type: eventData.type,
173      data: eventData.data,
174      timestamp: new Date(),
175      version: 0 // Zostanie ustawiona w EventStore
176    }));
177
178    await this.eventStore.saveEvents(user.id, events, user.version);
179    
180    // Aplikowanie eventów do aggregate
181    events.forEach(event => user.apply(event));
182  }
183
184  async getById(id: string): Promise<User | null> {
185    const events = await this.eventStore.getEvents(id);
186    
187    if (events.length === 0) {
188      return null;
189    }
190
191    return User.fromHistory(id, events);
192  }
193}
194
195// Użycie Event Sourcing
196async function demonstrateEventSourcing() {
197  const eventStore = new EventStore();
198  const userRepository = new UserRepository(eventStore);
199
200  // Tworzenie użytkownika
201  const { user, events } = User.create('user1', 'Jan Kowalski', 'jan@example.com');
202  await userRepository.save(user, events);
203
204  console.log('User po utworzeniu:', user);
205
206  // Zmiana emaila
207  const emailChangeEvents = user.changeEmail('jan.kowalski@example.com');
208  await userRepository.save(user, emailChangeEvents);
209
210  console.log('User po zmianie emaila:', user);
211
212  // Deaktywacja
213  const deactivateEvents = user.deactivate('User requested account deletion');
214  await userRepository.save(user, deactivateEvents);
215
216  console.log('User po deaktywacji:', user);
217
218  // Reaktywacja
219  const reactivateEvents = user.reactivate();
220  await userRepository.save(user, reactivateEvents);
221
222  console.log('User po reaktywacji:', user);
223
224  // Odtworzenie stanu z eventów
225  const reconstructedUser = await userRepository.getById('user1');
226  console.log('Odtworzony user:', reconstructedUser);
227
228  // Historia wszystkich eventów
229  const allEvents = await eventStore.getAllEvents();
230  console.log('Historia eventów:', allEvents);
231}
232
233demonstrateEventSourcing();

Projektory i Read Models

1// Projektor - tworzy read modele z eventów
2abstract class Projector {
3  abstract getProjectionName(): string;
4  abstract handle(event: Event): Promise<void>;
5}
6
7// Read Model dla statystyk użytkowników
8interface UserStats {
9  totalUsers: number;
10  activeUsers: number;
11  deactivatedUsers: number;
12  emailChanges: number;
13}
14
15class UserStatsProjector extends Projector {
16  private stats: UserStats = {
17    totalUsers: 0,
18    activeUsers: 0,
19    deactivatedUsers: 0,
20    emailChanges: 0
21  };
22
23  getProjectionName(): string {
24    return 'UserStats';
25  }
26
27  async handle(event: Event): Promise<void> {
28    switch (event.type) {
29      case 'UserCreated':
30        this.stats.totalUsers++;
31        this.stats.activeUsers++;
32        break;
33
34      case 'UserEmailChanged':
35        this.stats.emailChanges++;
36        break;
37
38      case 'UserDeactivated':
39        this.stats.activeUsers--;
40        this.stats.deactivatedUsers++;
41        break;
42
43      case 'UserReactivated':
44        this.stats.activeUsers++;
45        this.stats.deactivatedUsers--;
46        break;
47    }
48
49    console.log(`[UserStatsProjector] Updated stats:`, this.stats);
50  }
51
52  getStats(): UserStats {
53    return { ...this.stats };
54  }
55}
56
57// Event Bus dla projektorów
58class EventBus {
59  private projectors: Projector[] = [];
60
61  registerProjector(projector: Projector): void {
62    this.projectors.push(projector);
63  }
64
65  async publishEvent(event: Event): Promise<void> {
66    console.log(`Publishing event: ${event.type}`);
67    
68    const promises = this.projectors.map(projector => 
69      projector.handle(event).catch(error => {
70        console.error(`Error in projector ${projector.getProjectionName()}:`, error);
71      })
72    );
73
74    await Promise.all(promises);
75  }
76}
77
78// Enhanced Event Store z Event Bus
79class EnhancedEventStore extends EventStore {
80  constructor(private eventBus: EventBus) {
81    super();
82  }
83
84  async saveEvents(aggregateId: string, events: Event[], expectedVersion: number): Promise<void> {
85    await super.saveEvents(aggregateId, events, expectedVersion);
86
87    // Publikowanie eventów do projektorów
88    const savedEvents = await this.getEvents(aggregateId, expectedVersion);
89    for (const event of savedEvents) {
90      await this.eventBus.publishEvent(event);
91    }
92  }
93}
94
95// Użycie z projektorami
96async function demonstrateProjectors() {
97  const eventBus = new EventBus();
98  const userStatsProjector = new UserStatsProjector();
99  
100  eventBus.registerProjector(userStatsProjector);
101
102  const enhancedEventStore = new EnhancedEventStore(eventBus);
103  const userRepository = new UserRepository(enhancedEventStore);
104
105  // Wykonanie operacji - projektory będą automatycznie aktualizowane
106  const { user, events } = User.create('user2', 'Anna Nowak', 'anna@example.com');
107  await userRepository.save(user, events);
108
109  const emailChangeEvents = user.changeEmail('anna.nowak@example.com');
110  await userRepository.save(user, emailChangeEvents);
111
112  console.log('Finalne statystyki:', userStatsProjector.getStats());
113}
114
115demonstrateProjectors();

State Machines (Maszyny stanów) z XState

Maszyny stanów pozwalają na modelowanie złożonych przepływów stanów w sposób deklaratywny i przewidywalny.

Podstawowa maszyna stanów

1// Implementacja prostej maszyny stanów (bez XState)
2interface StateDefinition {
3  on: Record<string, string>;
4  entry?: () => void;
5  exit?: () => void;
6}
7
8interface MachineConfig {
9  id: string;
10  initial: string;
11  states: Record<string, StateDefinition>;
12}
13
14class StateMachine {
15  private currentState: string;
16  private config: MachineConfig;
17  private listeners: Array<(state: string) => void> = [];
18
19  constructor(config: MachineConfig) {
20    this.config = config;
21    this.currentState = config.initial;
22    this.executeEntry();
23  }
24
25  getCurrentState(): string {
26    return this.currentState;
27  }
28
29  send(event: string): void {
30    const currentStateConfig = this.config.states[this.currentState];
31    const nextState = currentStateConfig.on[event];
32
33    if (!nextState) {
34      console.warn(`Event "${event}" not handled in state "${this.currentState}"`);
35      return;
36    }
37
38    this.transition(nextState);
39  }
40
41  private transition(nextState: string): void {
42    if (!this.config.states[nextState]) {
43      throw new Error(`State "${nextState}" not defined`);
44    }
45
46    console.log(`Transition: ${this.currentState} -> ${nextState}`);
47
48    // Exit current state
49    this.executeExit();
50
51    // Change state
52    const previousState = this.currentState;
53    this.currentState = nextState;
54
55    // Entry new state
56    this.executeEntry();
57
58    // Notify listeners
59    this.listeners.forEach(listener => listener(this.currentState));
60  }
61
62  private executeEntry(): void {
63    const stateConfig = this.config.states[this.currentState];
64    if (stateConfig.entry) {
65      stateConfig.entry();
66    }
67  }
68
69  private executeExit(): void {
70    const stateConfig = this.config.states[this.currentState];
71    if (stateConfig.exit) {
72      stateConfig.exit();
73    }
74  }
75
76  subscribe(listener: (state: string) => void): () => void {
77    this.listeners.push(listener);
78    return () => {
79      const index = this.listeners.indexOf(listener);
80      if (index > -1) {
81        this.listeners.splice(index, 1);
82      }
83    };
84  }
85}
86
87// Przykład: Maszyna stanów dla procesu zamówienia
88const orderMachine = new StateMachine({
89  id: 'order',
90  initial: 'cart',
91  states: {
92    cart: {
93      on: {
94        CHECKOUT: 'checkout'
95      },
96      entry: () => console.log('Entered cart state'),
97      exit: () => console.log('Exited cart state')
98    },
99    checkout: {
100      on: {
101        PAYMENT: 'payment',
102        BACK_TO_CART: 'cart'
103      },
104      entry: () => console.log('Entered checkout state')
105    },
106    payment: {
107      on: {
108        SUCCESS: 'confirmed',
109        FAILURE: 'payment_failed',
110        BACK_TO_CHECKOUT: 'checkout'
111      },
112      entry: () => console.log('Processing payment...')
113    },
114    payment_failed: {
115      on: {
116        RETRY: 'payment',
117        BACK_TO_CHECKOUT: 'checkout'
118      },
119      entry: () => console.log('Payment failed!')
120    },
121    confirmed: {
122      on: {
123        FULFILL: 'shipped'
124      },
125      entry: () => console.log('Order confirmed!')
126    },
127    shipped: {
128      on: {
129        DELIVER: 'delivered'
130      },
131      entry: () => console.log('Order shipped!')
132    },
133    delivered: {
134      on: {},
135      entry: () => console.log('Order delivered!')
136    }
137  }
138});
139
140// Subskrypcja na zmiany stanu
141orderMachine.subscribe((state) => {
142  console.log(`Current state: ${state}`);
143});
144
145// Symulacja przepływu zamówienia
146console.log('=== Order Flow Simulation ===');
147orderMachine.send('CHECKOUT');
148orderMachine.send('PAYMENT');
149orderMachine.send('SUCCESS');
150orderMachine.send('FULFILL');
151orderMachine.send('DELIVER');

Integracja z XState (konceptualna)

1// Przykład jak wyglądałaby maszyna stanów z XState
2// (kod konceptualny - wymaga instalacji XState)
3
4/*
5import { createMachine, interpret } from 'xstate';
6
7const orderMachineXState = createMachine({
8  id: 'order',
9  initial: 'cart',
10  context: {
11    items: [],
12    total: 0,
13    customer: null
14  },
15  states: {
16    cart: {
17      on: {
18        ADD_ITEM: {
19          actions: 'addItem'
20        },
21        REMOVE_ITEM: {
22          actions: 'removeItem'
23        },
24        CHECKOUT: {
25          target: 'checkout',
26          cond: 'hasItems'
27        }
28      }
29    },
30    checkout: {
31      entry: 'calculateTotal',
32      on: {
33        PAYMENT: 'payment',
34        BACK_TO_CART: 'cart'
35      }
36    },
37    payment: {
38      invoke: {
39        id: 'paymentService',
40        src: 'processPayment',
41        onDone: {
42          target: 'confirmed',
43          actions: 'savePaymentInfo'
44        },
45        onError: {
46          target: 'payment_failed',
47          actions: 'savePaymentError'
48        }
49      },
50      on: {
51        BACK_TO_CHECKOUT: 'checkout'
52      }
53    },
54    payment_failed: {
55      on: {
56        RETRY: 'payment',
57        BACK_TO_CHECKOUT: 'checkout'
58      }
59    },
60    confirmed: {
61      entry: 'sendConfirmationEmail',
62      on: {
63        FULFILL: 'shipped'
64      }
65    },
66    shipped: {
67      entry: 'sendShippingNotification',
68      on: {
69        DELIVER: 'delivered'
70      }
71    },
72    delivered: {
73      entry: 'sendDeliveryConfirmation',
74      type: 'final'
75    }
76  }
77}, {
78  actions: {
79    addItem: (context, event) => {
80      // Dodaj item do koszyka
81    },
82    removeItem: (context, event) => {
83      // Usuń item z koszyka
84    },
85    calculateTotal: (context) => {
86      // Oblicz total
87    },
88    savePaymentInfo: (context, event) => {
89      // Zapisz info o płatności
90    }
91  },
92  guards: {
93    hasItems: (context) => context.items.length > 0
94  },
95  services: {
96    processPayment: (context) => {
97      // Zwraca Promise z procesem płatności
98      return fetch('/api/payment', {
99        method: 'POST',
100        body: JSON.stringify(context)
101      });
102    }
103  }
104});
105*/

Zaawansowane maszyny stanów z hierarchią

1// Hierarchiczna maszyna stanów dla odtwarzacza muzyki
2interface PlayerContext {
3  currentTrack: string | null;
4  volume: number;
5  position: number;
6  playlist: string[];
7}
8
9class HierarchicalStateMachine extends StateMachine {
10  private context: any = {};
11
12  constructor(config: MachineConfig, initialContext: any = {}) {
13    super(config);
14    this.context = { ...initialContext };
15  }
16
17  getContext(): any {
18    return { ...this.context };
19  }
20
21  updateContext(updates: any): void {
22    this.context = { ...this.context, ...updates };
23  }
24}
25
26const musicPlayerMachine = new HierarchicalStateMachine({
27  id: 'musicPlayer',
28  initial: 'stopped',
29  states: {
30    stopped: {
31      on: {
32        PLAY: 'playing',
33        LOAD: 'loading'
34      },
35      entry: () => console.log('Player stopped')
36    },
37    loading: {
38      on: {
39        LOADED: 'playing',
40        ERROR: 'error'
41      },
42      entry: () => console.log('Loading track...')
43    },
44    playing: {
45      on: {
46        PAUSE: 'paused',
47        STOP: 'stopped',
48        NEXT: 'loading',
49        PREVIOUS: 'loading'
50      },
51      entry: () => console.log('Playing music'),
52      exit: () => console.log('Stopped playing')
53    },
54    paused: {
55      on: {
56        PLAY: 'playing',
57        STOP: 'stopped'
58      },
59      entry: () => console.log('Music paused')
60    },
61    error: {
62      on: {
63        RETRY: 'loading',
64        STOP: 'stopped'
65      },
66      entry: () => console.log('Error loading track')
67    }
68  }
69}, {
70  currentTrack: null,
71  volume: 50,
72  position: 0,
73  playlist: ['track1.mp3', 'track2.mp3']
74});
75
76// Symulacja odtwarzacza
77console.log('=== Music Player Simulation ===');
78musicPlayerMachine.send('LOAD');
79musicPlayerMachine.send('LOADED');
80musicPlayerMachine.send('PAUSE');
81musicPlayerMachine.send('PLAY');
82musicPlayerMachine.send('NEXT');

Porównanie wzorców zarządzania stanem

Kiedy używać którego wzorca?

1// Przykład aplikacji używającej wszystkich trzech wzorców
2
3// 1. Flux - dla prostego state managementu UI
4class UIStateManager {
5  // Zarządzanie stanem komponentów UI, formularzy, modali
6}
7
8// 2. Event Sourcing - dla business logic z historią
9class OrderEventStore {
10  // Zarządzanie zamówieniami z pełną historią zmian
11}
12
13// 3. State Machines - dla złożonych przepływów
14class WorkflowStateMachine {
15  // Zarządzanie przepływami biznesowymi (approval workflows, etc.)
16}
17
18// Kombinacja wzorców w jednej aplikacji
19class ApplicationStateManager {
20  private uiState: UIStateManager;
21  private orderEvents: OrderEventStore;
22  private workflows: Map<string, WorkflowStateMachine>;
23
24  constructor() {
25    this.uiState = new UIStateManager();
26    this.orderEvents = new OrderEventStore();
27    this.workflows = new Map();
28  }
29
30  // Flux dla UI
31  updateUI(action: Action): void {
32    // this.uiState.dispatch(action);
33  }
34
35  // Event Sourcing dla business events
36  processBusinessEvent(event: Event): void {
37    // this.orderEvents.append(event);
38  }
39
40  // State Machines dla workflows
41  advanceWorkflow(workflowId: string, event: string): void {
42    const workflow = this.workflows.get(workflowId);
43    if (workflow) {
44      workflow.send(event);
45    }
46  }
47}

Najlepsze praktyki

1// 1. Separation of Concerns - rozdzielenie odpowiedzialności
2interface StatePattern {
3  // UI State - Flux/Redux
4  uiState: {
5    loading: boolean;
6    errors: string[];
7    notifications: any[];
8  };
9
10  // Business State - Event Sourcing
11  businessEvents: Event[];
12
13  // Workflow State - State Machines
14  workflows: Record<string, string>; // workflowId -> currentState
15}
16
17// 2. Testowanie wzorców
18class StatePatternTests {
19  testFluxActions(): void {
20    // Test action creators, reducers, stores
21  }
22
23  testEventSourcing(): void {
24    // Test event application, projectors
25  }
26
27  testStateMachines(): void {
28    // Test state transitions, guards, actions
29  }
30}
31
32// 3. Performance considerations
33class PerformanceOptimizations {
34  // Memoization dla Flux selektorów
35  memoizedSelectors = new Map();
36
37  // Snapshotting dla Event Sourcing
38  snapshots = new Map();
39
40  // State Machine caching
41  machineInstances = new Map();
42}

Podsumowanie wzorców zarządzania stanem

Każdy wzorzec ma swoje miejsce w nowoczesnych aplikacjach:

Flux:

  • ✅ Prosty jednokierunkowy przepływ danych
  • ✅ Łatwy debugging i śledzenie zmian
  • ✅ Idealny dla UI state management
  • ❌ Może być over-engineering dla prostych aplikacji

Event Sourcing:

  • ✅ Pełna historia zmian
  • ✅ Możliwość odtworzenia stanu w dowolnym momencie
  • ✅ Naturalne audytowanie
  • ❌ Większa złożoność implementacji
  • ❌ Wymaga więcej miejsca na dysku

State Machines:

  • ✅ Jasne modelowanie złożonych przepływów
  • ✅ Niemożliwe stany nieprawidłowe
  • ✅ Łatwe wizualizowanie i testowanie
  • ❌ Learning curve dla zespołu
  • ❌ Może być przesadą dla prostych stanów

Rekomendacje:

  • Małe aplikacje: podstawowy state (useState, signals)
  • Średnie aplikacje: Flux/Redux dla UI state
  • Duże aplikacje: kombinacja wzorców według potrzeb
  • Enterprise: Event Sourcing + State Machines dla krytycznych procesów

Podobnie jak starożytni administratorzy używali różnych systemów zarządzania w zależności od skali i potrzeb, współcześni programiści powinni wybierać wzorce zarządzania stanem dostosowane do specyfiki swoich aplikacji.

Ir a CodeWorlds