Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Strukturyzacja kodu w większym projekcie

Po przejściu przez wszystkie najważniejsze techniki JavaScript, nadszedł czas, aby spojrzeć na szerszy obraz - jak organizować kod w większych projektach. Nieodpowiednia struktura kodu może doprowadzić do katastrofy podobnej do tej, z którą mieliśmy do czynienia w Parku Jurajskim - chaos, błędy i trudność w zarządzaniu systemem.

Dlaczego struktura projektu jest ważna?

Wyobraź sobie, że całą infrastrukturę Parku Jurajskiego zarządzaną przez jeden ogromny system. Wszystkie zabezpieczenia wybiegów, monitorowanie zdrowia dinozaurów, zarządzanie przepływem zwiedzających, gastronomia, laboratoria genetyczne - wszystko w jednym, monolitycznym programie. Katastrofa czeka tuż za rogiem, prawda?

Dobrze zaprojektowana struktura projektu zapewnia:

  1. Utrzymywalność - łatwiejsze znajdowanie i naprawianie błędów
  2. Skalowalność - możliwość dodawania nowych funkcjonalności bez naruszania istniejących
  3. Testowalność - możliwość testowania komponentów w izolacji
  4. Łatwość współpracy - wielu programistów może pracować równolegle nad różnymi częściami systemu
  5. Czytelność - łatwiejsze zrozumienie, jak działa system

Organizacja folderów i plików

Istnieje wiele podejść do organizacji kodu w większych projektach JavaScript, ale przyjrzyjmy się kilku sprawdzonym wzorcom, które sprawdziłyby się w systemie zarządzania Parkiem Jurajskim.

1. Podział według typów (Type-Based)

Jedna z popularnych struktur, szczególnie w mniejszych aplikacjach, to grupowanie plików według ich typu:

1jurassic-park-management/
2├── src/
3│   ├── components/          # Komponenty UI
4│   ├── services/            # Serwisy do komunikacji z API
5│   ├── utils/               # Narzędzia pomocnicze
6│   ├── models/              # Definicje typów i interfejsów
7│   ├── hooks/               # Niestandardowe hooki
8│   ├── constants/           # Stałe używane w aplikacji
9│   ├── assets/              # Obrazy, czcionki, itp.
10│   ├── styles/              # Style CSS/SCSS
11│   └── tests/               # Testy
12└── package.json

W tej strukturze, jeśli chcemy dodać nową funkcjonalność, np. monitorowanie zdrowia dinozaurów, będziemy musieli dodać:

  • Komponenty UI w
    /components
  • Serwis do pobierania danych w
    /services
  • Może nowe modele w
    /models
  • Pomocnicze narzędzia w
    /utils

To podejście jest proste i zrozumiałe, ale w miarę rozrostu projektu może stać się trudniejsze do zarządzania, ponieważ powiązane ze sobą pliki są rozrzucone po różnych katalogach.

2. Podział według funkcjonalności (Feature-Based)

Alternatywne podejście to organizowanie kodu według funkcjonalności biznesowej:

1jurassic-park-management/
2├── src/
3│   ├── common/                     # Współdzielone komponenty, style, utils
4│   ├── features/
5│   │   ├── dinosaur-monitoring/    # Wszystko związane z monitorowaniem dinozaurów
6│   │   │   ├── components/
7│   │   │   ├── services/
8│   │   │   ├── hooks/
9│   │   │   ├── utils/
10│   │   │   └── tests/
11│   │   ├── enclosure-security/     # Wszystko związane z zabezpieczeniami wybiegów
12│   │   │   ├── components/
13│   │   │   ├── services/
14│   │   │   └──
15│   │   ├── visitor-management/     # Wszystko związane z zarządzaniem zwiedzającymi
16│   │   │   ├── components/
17│   │   │   ├── services/
18│   │   │   └──
19│   │   └──
20│   ├── core/                       # Core functionality
21│   │   ├── api/
22│   │   ├── auth/
23│   │   └── config/
24│   └── app.js
25└── package.json

W tej strukturze, jeśli pracujemy nad systemem monitorowania dinozaurów, wszystkie potrzebne pliki znajdują się w jednym miejscu. To podejście jest bardziej skalowalne dla dużych projektów, ponieważ funkcjonalności są od siebie odizolowane.

3. Struktura modułowa (Modular Structure)

Jeszcze bardziej zaawansowane podejście to organizacja kodu w postaci modułów, które są w pełni enkapsulowane:

1jurassic-park-management/
2├── src/
3│   ├── modules/
4│   │   ├── dinosaur-monitoring/
5│   │   │   ├── components/
6│   │   │   ├── services/
7│   │   │   ├── store/
8│   │   │   ├── utils/
9│   │   │   ├── index.js       # Eksportuje publiczne API modułu
10│   │   │   └── README.md      # Dokumentacja modułu
11│   │   ├── enclosure-security/
12│   │   │   ├──
13│   │   │   └── index.js
14│   │   └──
15│   ├── shared/                # Współdzielone zasoby
16│   │   ├── components/
17│   │   ├── utils/
18│   │   └── hooks/
19│   ├── core/                  # Core functionality
20│   │   ├── api/
21│   │   ├── router/
22│   │   └── store/
23│   └── app.js
24└── package.json

W tym podejściu każdy moduł jest traktowany jako "mini-aplikacja" z własnym API udostępnianym przez plik

index.js
. Inne moduły mogą korzystać tylko z tego, co jest jawnie eksportowane, co zapewnia lepszą enkapsulację i zmniejsza zależności między modułami.

Architektura aplikacji

Struktura folderów to tylko część zagadnienia. Równie ważne jest zaprojektowanie odpowiedniej architektury aplikacji, która określa, jak różne części kodu współpracują ze sobą.

1. Model-View-Controller (MVC)

Klasyczny wzorzec MVC dzieli aplikację na trzy główne komponenty:

  • Model - zarządza danymi i logiką biznesową
  • View - odpowiada za prezentację danych użytkownikowi
  • Controller - pośredniczy między Modelem a View, obsługując interakcje użytkownika

W kontekście Parku Jurajskiego:

  • Model - dane o dinozaurach, wybiegach, zwiedzających
  • View - interfejs użytkownika dla pracowników parku
  • Controller - obsługa żądań, np. otwarcie/zamknięcie wybiegu, sprawdzenie zdrowia dinozaura

2. Flux / Redux

Flux (i jego implementacja Redux) to architektura oparta na jednokierunkowym przepływie danych, która świetnie sprawdza się w aplikacjach React.

1┌──────────┐       ┌──────────┐       ┌───────────────┐
2│  Action  │──────>│  Store   │──────>│  Components   │
3└──────────┘       └──────────┘       └───────────────┘
456                                              V
7                                       ┌──────────┐
8                                       │ Actions  │
9                                       └──────────┘

W kontekście Parku Jurajskiego:

  • Actions: "OPEN_ENCLOSURE", "FEED_DINOSAUR", "ACTIVATE_EMERGENCY_PROTOCOL"
  • Store: Stan aplikacji zawierający dane o wszystkich dinozaurach, wybiegach, etc.
  • Components: UI dla pracowników parku, pokazujące aktualny stan parku

3. Clean Architecture / Hexagonal Architecture

Te architektury kładą nacisk na oddzielenie logiki biznesowej od infrastruktury (UI, bazy danych, etc.), co ułatwia testowanie i zmiany infrastruktury bez naruszania core'owej funkcjonalności.

1┌────────────────────────────────────────────┐
2│ Core (Domain)                              │
3│  ┌──────────────────────────────────────┐  │
4│  │ Application                          │  │
5│  │  ┌──────────────────────────────┐    │  │
6│  │  │ Interfejs                    │    │  │
7│  │  │ (Controllers, Presenters...) │    │  │
8│  │  └──────────────────────────────┘    │  │
9│  └──────────────────────────────────────┘  │
10└────────────────────────────────────────────┘
11               │                │
12    ┌──────────┘                └──────────┐
13    │                                      │
14┌───▼────────────┐              ┌──────────▼───┐
15│ UI             │              │ Infrastructure│
16│ (React, Vue..)│              │ (DB, API...)  │
17└────────────────┘              └──────────────┘

W kontekście Parku Jurajskiego:

  • Core Domain: Główna logika biznesowa - jak monitorować zdrowie dinozaurów, zarządzać wybiegami, etc.
  • Application: Przypadki użycia - karmienie dinozaurów, otwieranie/zamykanie wybiegów, itp.
  • Interface: Adaptery pomiędzy rdzeniem a infrastrukturą
  • Infrastructure: Konkretne implementacje - UI dla pracowników, baza danych z informacjami o dinozaurach, API do systemów zabezpieczeń, itp.

Modularyzacja kodu

Niezależnie od wybranej struktury folderów i architektury, kluczowe jest właściwe podzielenie kodu na mniejsze, spójne moduły.

1. Modularyzacja według odpowiedzialności

Każdy moduł powinien mieć jasno określoną, pojedynczą odpowiedzialność:

1// dinosaurHealthMonitor.js - odpowiedzialny tylko za monitorowanie zdrowia dinozaurów
2export function checkVitalSigns(dinosaurId) {
3  //
4}
5
6export function alertHealthIssue(dinosaurId, issue) {
7  //
8}
9
10// enclosureSecurity.js - odpowiedzialny tylko za bezpieczeństwo wybiegów
11export function checkEnclosureSecurity(enclosureId) {
12  //
13}
14
15export function activateEmergencyProtocol(enclosureId) {
16  //
17}

2. Czyste interfejsy między modułami

Moduły powinny komunikować się ze sobą przez czyste, dobrze zdefiniowane interfejsy:

1// Moduł wyższego poziomu korzysta z interfejsów modułów niższego poziomu
2import { checkVitalSigns } from './dinosaurHealthMonitor';
3import { checkEnclosureSecurity } from './enclosureSecurity';
4
5// parkSafetySystem.js
6export async function performSafetyCheck() {
7  const dinosaurs = await fetchAllDinosaurs();
8  const enclosures = await fetchAllEnclosures();
9
10  // Używamy interfejsów innych modułów
11  const healthIssues = dinosaurs.map(checkVitalSigns);
12  const securityIssues = enclosures.map(checkEnclosureSecurity);
13
14  return {
15    healthIssues: healthIssues.filter(issue => issue !== null),
16    securityIssues: securityIssues.filter(issue => issue !== null)
17  };
18}

3. Dependency Injection

Wstrzykiwanie zależności to technika, która pozwala na odwrócenie zależności między modułami, co ułatwia testowanie i zmiany:

1// Bez DI
2function DinosaurMonitor() {
3  // Bezpośrednia zależność
4  const vitalSignsMonitor = new VitalSignsMonitor();
5
6  return {
7    checkDinosaur(dinosaur) {
8      return vitalSignsMonitor.check(dinosaur);
9    }
10  };
11}
12
13// Z DI
14function DinosaurMonitor(vitalSignsMonitor) {
15  return {
16    checkDinosaur(dinosaur) {
17      return vitalSignsMonitor.check(dinosaur);
18    }
19  };
20}
21
22// Możemy teraz wstrzyknąć różne implementacje
23const realMonitor = DinosaurMonitor(new VitalSignsMonitor());
24const testMonitor = DinosaurMonitor(new MockVitalSignsMonitor());

Zarządzanie importami

W dużych projektach zarządzanie importami może stać się problematyczne. Oto kilka technik, które pomagają utrzymać je w porządku:

1. Indeksy baryłkowe (Barrel exports)

Pliki indeksowe, które re-eksportują wszystkie publiczne elementy z danego katalogu:

1// dinosaurMonitoring/index.js
2export * from './vitalSigns';
3export * from './behaviorAnalysis';
4export * from './healthAlerts';
5
6// W innym module
7import { checkVitalSigns, analyzeBehavior } from './dinosaurMonitoring';
8// Zamiast
9import { checkVitalSigns } from './dinosaurMonitoring/vitalSigns';
10import { analyzeBehavior } from './dinosaurMonitoring/behaviorAnalysis';

2. Aliasy importów

Wiele narzędzi bundlujących (jak webpack, Vite) pozwala na definiowanie aliasów dla ścieżek importów:

1// webpack.config.js
2module.exports = {
3  resolve: {
4    alias: {
5      '@core': path.resolve(__dirname, 'src/core'),
6      '@features': path.resolve(__dirname, 'src/features'),
7      '@shared': path.resolve(__dirname, 'src/shared'),
8    }
9  }
10};
11
12// W kodzie
13import { activateProtocol } from '@core/security';
14import { DinosaurCard } from '@features/dinosaurs/components';

To eliminuje problem względnych ścieżek (../../..) i sprawia, że importy są bardziej czytelne.

Zarządzanie stanem aplikacji

W większych aplikacjach zarządzanie stanem staje się kluczowym wyzwaniem. Oto kilka podejść:

1. Centralne zarządzanie stanem (Redux, MobX)

Przechowywanie całego stanu aplikacji w jednym miejscu:

1// Akcje Redux dla systemu monitorowania dinozaurów
2const UPDATE_DINOSAUR_HEALTH = 'UPDATE_DINOSAUR_HEALTH';
3const ALERT_HEALTH_ISSUE = 'ALERT_HEALTH_ISSUE';
4
5// Reducer
6function dinosaurHealthReducer(state = initialState, action) {
7  switch (action.type) {
8    case UPDATE_DINOSAUR_HEALTH:
9      return {
10        ...state,
11        dinosaurs: state.dinosaurs.map(dino =>
12          dino.id === action.payload.id
13            ? { ...dino, health: action.payload.health }
14            : dino
15        )
16      };
17    case ALERT_HEALTH_ISSUE:
18      return {
19        ...state,
20        alerts: [...state.alerts, action.payload]
21      };
22    default:
23      return state;
24  }
25}

2. Zmodularyzowany stan (Redux Toolkit, Zustand)

Podzielenie stanu na mniejsze, niezależne "plasterki":

1// Slice stanu odpowiedzialny za dinozaury
2const dinosaurSlice = createSlice({
3  name: 'dinosaurs',
4  initialState: { list: [], loading: false, error: null },
5  reducers: {
6    fetchStart: (state) => {
7      state.loading = true;
8    },
9    fetchSuccess: (state, action) => {
10      state.list = action.payload;
11      state.loading = false;
12    },
13    updateHealth: (state, action) => {
14      const { id, health } = action.payload;
15      const dinosaur = state.list.find(d => d.id === id);
16      if (dinosaur) dinosaur.health = health;
17    }
18  }
19});
20
21// Slice stanu odpowiedzialny za wybiegi
22const enclosureSlice = createSlice({
23  name: 'enclosures',
24  initialState: { list: [], securityStatus: {} },
25  reducers: {
26    //
27  }
28});

3. Server State vs. Client State

Rozróżnienie między danymi pochodzącymi z serwera a stanem UI:

1// Dane z serwera - obsługiwane przez React Query
2const { data: dinosaurs, isLoading, error } = useQuery(
3  'dinosaurs',
4  () => fetchDinosaurs()
5);
6
7// Stan UI - obsługiwany lokalnie
8const [selectedDinosaurId, setSelectedDinosaurId] = useState(null);

Zarządzanie efektami ubocznymi

W dużych aplikacjach, zarządzanie efektami ubocznymi (side effects) takimi jak żądania API, operacje I/O, etc. staje się skomplikowane. Oto kilka podejść:

1. Scentralizowane zarządzanie (Redux Saga, Redux Thunk)

1// Redux Saga dla monitorowania zdrowia dinozaurów
2function* monitorDinosaurHealthSaga() {
3  while (true) {
4    try {
5      // Pobierz wszystkie dinozaury
6      const dinosaurs = yield call(api.fetchAllDinosaurs);
7
8      // Sprawdź zdrowie każdego dinozaura
9      for (const dinosaur of dinosaurs) {
10        const healthData = yield call(api.fetchDinosaurHealth, dinosaur.id);
11
12        // Aktualizuj stan
13        yield put(updateDinosaurHealth(dinosaur.id, healthData));
14
15        // Jeśli zdrowie jest krytyczne, zgłoś alert
16        if (healthData.status === 'critical') {
17          yield put(alertHealthIssue(dinosaur.id, healthData));
18        }
19      }
20
21      // Poczekaj 5 minut przed kolejnym sprawdzeniem
22      yield delay(5 * 60 * 1000);
23    } catch (error) {
24      console.error('Health monitoring error:', error);
25      yield put(monitoringError(error));
26      // Poczekaj 1 minutę przed ponowną próbą
27      yield delay(60 * 1000);
28    }
29  }
30}

2. Hooki efektów (React Query, SWR)

1// Hook React Query do zarządzania danymi dinosaura i ich zdrowiem
2function useDinosaurHealth(dinosaurId) {
3  return useQuery(
4    ['dinosaurHealth', dinosaurId],
5    () => api.fetchDinosaurHealth(dinosaurId),
6    {
7      // Odświeżaj dane co 1 minutę
8      refetchInterval: 60 * 1000,
9      // Reaguj na zmiany zdrowia
10      onSuccess: (data) => {
11        if (data.status === 'critical') {
12          notifyHealthIssue(dinosaurId, data);
13        }
14      }
15    }
16  );
17}
18
19// W komponencie
20function DinosaurMonitor({ dinosaurId }) {
21  const { data, isLoading, error } = useDinosaurHealth(dinosaurId);
22
23  if (isLoading) return <Loading />;
24  if (error) return <Error message={error.message} />;
25
26  return (
27    <div className={`health-status ${data.status}`}>
28      <h2>Zdrowie dinozaura: {data.status}</h2>
29      <div>Temperatura: {data.temperature}°C</div>
30      <div>Tętno: {data.heartRate} BPM</div>
31      <div>Aktualizacja: {formatTime(data.timestamp)}</div>
32    </div>
33  );
34}

Testowanie dużych aplikacji

W dużych projektach kompleksowe testowanie jest kluczowe. Oto kilka poziomów testów, które należy rozważyć:

1. Testy jednostkowe (Unit Tests)

Testowanie pojedynczych modułów w izolacji:

1// Test funkcji checkVitalSigns z modułu dinosaurHealthMonitor
2describe('dinosaurHealthMonitor', () => {
3  describe('checkVitalSigns', () => {
4    it('should correctly identify healthy dinosaurs', () => {
5      const healthyDino = {
6        id: 'trex1',
7        temperature: 38,
8        heartRate: 70,
9        bloodOxygen: 95
10      };
11
12      const result = checkVitalSigns(healthyDino);
13      expect(result.status).toBe('healthy');
14      expect(result.issues).toHaveLength(0);
15    });
16
17    it('should detect high temperature', () => {
18      const feverishDino = {
19        id: 'trex1',
20        temperature: 42, // Too high
21        heartRate: 70,
22        bloodOxygen: 95
23      };
24
25      const result = checkVitalSigns(feverishDino);
26      expect(result.status).toBe('issue');
27      expect(result.issues).toContain('high temperature');
28    });
29  });
30});

2. Testy integracyjne (Integration Tests)

Testowanie współpracy między modułami:

1// Test współpracy między modułem zdrowia i modułem alertów
2describe('Health monitoring integration', () => {
3  it('should generate alerts for health issues', async () => {
4    // Przygotuj mocki
5    const mockAlertSystem = {
6      sendAlert: jest.fn()
7    };
8
9    const mockDinosaurData = {
10      id: 'raptor2',
11      temperature: 43, // Krytycznie wysoka
12      heartRate: 120,  // Przyspieszone tętno
13      bloodOxygen: 85  // Niski poziom tlenu
14    };
15
16    // Utwórz system monitorowania z zamockowanym systemem alertów
17    const healthMonitor = createHealthMonitor(mockAlertSystem);
18
19    // Wywołaj funkcję monitorującą
20    await healthMonitor.checkAndAlert(mockDinosaurData);
21
22    // Sprawdź, czy alert został wysłany
23    expect(mockAlertSystem.sendAlert).toHaveBeenCalledWith(
24      expect.objectContaining({
25        dinosaurId: 'raptor2',
26        severity: 'critical',
27        issues: expect.arrayContaining([
28          expect.stringMatching(/temperature/i),
29          expect.stringMatching(/heart rate/i),
30          expect.stringMatching(/oxygen/i)
31        ])
32      })
33    );
34  });
35});

3. Testy end-to-end (E2E Tests)

Testowanie pełnych ścieżek użytkownika:

1// Test E2E procesu reagowania na awarię wybiegu
2describe('Enclosure breach response', () => {
3  it('should activate emergency protocol on fence failure', async () => {
4    // Zaloguj się jako administrator systemu
5    await login('admin', 'password');
6
7    // Przejdź do panelu zarządzania wybiegami
8    await navigateTo('enclosures');
9
10    // Symuluj awarię płotu
11    await simulateFenceFailure('enclosure-b12');
12
13    // Sprawdź, czy alarm został aktywowany
14    const alarmStatus = await getElement('.alarm-status');
15    expect(alarmStatus).toHaveClass('active');
16
17    // Sprawdź, czy protokół awaryjny został uruchomiony
18    const protocolStatus = await getElement('.emergency-protocol');
19    expect(protocolStatus.textContent).toContain('Aktywny');
20
21    // Sprawdź, czy powiadomienia zostały wysłane
22    const notifications = await getElements('.notification');
23    expect(notifications).toHaveLength(2); // Do zespołu bezpieczeństwa i kierownictwa
24
25    // Symuluj naprawę płotu
26    await repairFence('enclosure-b12');
27
28    // Sprawdź, czy alarm został wyłączony
29    const updatedAlarmStatus = await getElement('.alarm-status');
30    expect(updatedAlarmStatus).not.toHaveClass('active');
31  });
32});

Dokumentacja kodu

W dużych projektach dokumentacja staje się niezbędna. Oto różne poziomy dokumentacji, które należy rozważyć:

1. Dokumentacja kodu (komentarze, JSDoc)

1/**
2 * Sprawdza i ocenia zdrowie dinozaura na podstawie jego parametrów życiowych.
3 *
4 * @param {Object} dinosaur - Obiekt dinozaura do sprawdzenia
5 * @param {string} dinosaur.id - Unikalny identyfikator dinozaura
6 * @param {number} dinosaur.temperature - Temperatura ciała dinozaura w stopniach Celsjusza
7 * @param {number} dinosaur.heartRate - Tętno dinozaura w uderzeniach na minutę
8 * @param {number} dinosaur.bloodOxygen - Poziom tlenu we krwi dinozaura (0-100%)
9 *
10 * @returns {Object} Obiekt z oceną zdrowia
11 * @returns {string} returns.status - Status zdrowia: 'healthy', 'issue', 'critical'
12 * @returns {Array<string>} returns.issues - Lista wykrytych problemów zdrowotnych
13 *
14 * @example
15 * const healthAssessment = checkVitalSigns({
16 *   id: 'trex1',
17 *   temperature: 38.5,
18 *   heartRate: 75,
19 *   bloodOxygen: 92
20 * });
21 * // { status: 'healthy', issues: [] }
22 */
23function checkVitalSigns(dinosaur) {
24  // implementacja...
25}

2. Dokumentacja modułów (README.md)

Każdy moduł powinien mieć swój własny plik README.md, który opisuje jego funkcjonalność, interfejs i użycie:

1# Moduł monitorowania zdrowia dinozaurów
2
3Ten moduł odpowiada za monitorowanie i ocenę stanu zdrowia dinozaurów w Parku Jurajskim.
4
5## Funkcjonalności
6
7- Regularne sprawdzanie parametrów życiowych dinozaurów
8- Wykrywanie problemów zdrowotnych i generowanie alertów
9- Historyczne śledzenie stanu zdrowia i trendów
10- Automatyczne powiadamianie personelu weterynaryjnego
11
12## API
13
14### `checkVitalSigns(dinosaur)`
15
16Sprawdza zdrowie dinozaura na podstawie jego parametrów życiowych.
17
18### `monitorHealth(dinosaurId, interval)`
19
20Rozpoczyna cykliczne monitorowanie zdrowia dinozaura.
21
22### `stopMonitoring(dinosaurId)`
23
24Zatrzymuje monitorowanie zdrowia dinozaura.
25
26## Przykłady użycia
27
28~~~js
29// Jednorazowe sprawdzenie zdrowia
30const healthStatus = checkVitalSigns(dinosaur);
31
32// Rozpoczęcie ciągłego monitorowania
33const stopMonitoring = monitorHealth('trex1', 60000); // co minutę
34
35// Zatrzymanie monitorowania
36stopMonitoring();

Integracja z innymi modułami

Ten moduł korzysta z:

  • API systemów sensorycznych (src/modules/sensors)
  • Systemu alertów (src/modules/alerts)

Jest używany przez:

  • Dashboard zarządzania parkiem (src/modules/dashboard)
  • System bezpieczeństwa (src/modules/security)
1
2### 3. Dokumentacja architektury (diagragram, ADR)
3
4Dokumentowanie decyzji architektonicznych jest kluczowe dla zrozumienia, dlaczego system został zaprojektowany w określony sposób. Pomocne są tutaj:
5
6- **Diagramy architektury** (C4, UML)
7- **Architecture Decision Records (ADR)** - dokumenty opisujące ważne decyzje architektoniczne, ich kontekst i konsekwencje
8
9~~~text
10# ADR 001: Wybór Redux jako systemu zarządzania stanem
11
12## Kontekst
13
14Aplikacja zarządzania Parkiem Jurajskim potrzebuje mechanizmu zarządzania stanem, który obsłuży:
15- Złożony stan globalnz (dane o dinozaurach, wybiegach, zwiedzających)
16- Asynchroniczne operacje (komunikacja z systemami w parku)
17- Śledzenie historii zmian stanu (audyt bezpieczeństwa)
18
19## Decyzja
20
21Zdecydowaliśmy się użyć Redux z Redux Toolkit i Redux Saga jako głównego mechanizmu zarządzania stanem.
22
23## Uzasadnienie
24
25- **Przewidywalność**: Redux zapewnia jednokierunkowy przepływ danych
26- **Middleware**: Redux Saga ułatwia zarządzanie złożonymi operacjami asynchronicznymi
27- **DevTools**: Narzędzia deweloperskie Redux ułatwiają debugowanie
28- **Ekosystem**: Bogate ekosystem bibliotek i narzędzi
29- **Time Travel Debugging**: Możliwość śledzenia historii zmian stanu
30
31## Konsekwencje
32
33### Pozytywne
34- Jasny przepływ danych w aplikacji
35- Łatwiejsze debugowanie problemów ze stanem
36- Możliwość audytu działań użytkowników
37
38### Negatywne
39- Zwiększona ilość kodu (actions, reducers, selectors)
40- Wyższa krzywa uczenia dla nowych członków zespołu
41- Potencjalny overhead wydajnościowy przy niewłaściwym użyciu

Dobre praktyki dla dużych projektów JavaScript

1. Ustanów standardy kodowania

Zdefiniuj i egzekwuj standardy kodowania za pomocą narzędzi jak ESLint, Prettier:

1// .eslintrc.json
2{
3  "extends": [
4    "eslint:recommended",
5    "plugin:react/recommended"
6  ],
7  "rules": {
8    "react/prop-types": "error",
9    "no-unused-vars": "error",
10    "no-console": ["warn", { "allow": ["warn", "error"] }],
11    "prefer-const": "error"
12  }
13}

2. Używaj typowania (TypeScript, PropTypes)

1// typescript
2interface DinosaurVitalSigns {
3  id: string;
4  temperature: number;
5  heartRate: number;
6  bloodOxygen: number;
7}
8
9interface HealthAssessment {
10  status: 'healthy' | 'issue' | 'critical';
11  issues: string[];
12  lastChecked: Date;
13}
14
15function checkVitalSigns(dinosaur: DinosaurVitalSigns): HealthAssessment {
16  // implementacja...
17}

3. Automatyzuj testy i wdrożenia (CI/CD)

Skonfiguruj potoki CI/CD, które automatycznie uruchamiają testy, analizę statyczną kodu i wdrażają aplikację:

1# .github/workflows/ci.yml
2name: CI/CD
3
4on:
5  push:
6    branches: [ main, develop ]
7  pull_request:
8    branches: [ main, develop ]
9
10jobs:
11  test:
12    runs-on: ubuntu-latest
13    steps:
14      - uses: actions/checkout@v2
15      - name: Set up Node.js
16        uses: actions/setup-node@v2
17        with:
18          node-version: '14'
19      - name: Install dependencies
20        run: npm ci
21      - name: Run tests
22        run: npm test
23      - name: Run linting
24        run: npm run lint
25      - name: Build
26        run: npm run build
27
28  deploy:
29    needs: test
30    if: github.ref == 'refs/heads/main'
31    runs-on: ubuntu-latest
32    steps:
33      - uses: actions/checkout@v2
34      # ... kroki wdrożenia

4. Stosuj Code Reviews

Ustanów proces Code Review, który zapewni, że kod jest sprawdzany przez co najmniej jednego innego programistę przed włączeniem do głównej gałęzi.

5. Monitoruj wydajność

Implementuj monitorowanie wydajności aplikacji, aby szybko wykrywać i naprawiać problemy:

1// Przykład konfiguracji własnego narzędzia do monitorowania wydajności
2const performanceMonitor = {
3  eventTiming: {},
4
5  startTiming(eventName) {
6    this.eventTiming[eventName] = {
7      start: performance.now()
8    };
9  },
10
11  endTiming(eventName) {
12    if (this.eventTiming[eventName]) {
13      const start = this.eventTiming[eventName].start;
14      const duration = performance.now() - start;
15
16      // Loguj do serwera monitorowania
17      logPerformanceMetric(eventName, duration);
18
19      // Generuj alert, jeśli operacja trwała zbyt długo
20      if (duration > performanceThresholds[eventName]) {
21        alertPerformanceIssue(eventName, duration);
22      }
23
24      delete this.eventTiming[eventName];
25    }
26  }
27};
28
29// Użycie
30function loadDinosaurData(id) {
31  performanceMonitor.startTiming('loadDinosaurData');
32
33  return fetchDinosaurData(id)
34    .then(data => {
35      performanceMonitor.endTiming('loadDinosaurData');
36      return data;
37    })
38    .catch(error => {
39      performanceMonitor.endTiming('loadDinosaurData');
40      throw error;
41    });
42}

Podsumowanie

Strukturyzacja kodu w większych projektach JavaScript to złożone zagadnienie, które wymaga przemyślanego podejścia. Kluczowe aspekty to:

  1. Organizacja folderów i plików - wybór odpowiedniej struktury zależnej od rozmiaru i charakteru projektu
  2. Architektura aplikacji - zdefiniowanie, jak różne części aplikacji współpracują ze sobą
  3. Modularyzacja kodu - dzielenie kodu na mniejsze, spójne moduły o jasno określonych odpowiedzialnościach
  4. Zarządzanie importami - utrzymywanie czytelnych i efektywnych importów
  5. Zarządzanie stanem aplikacji - wybór odpowiedniego podejścia do zarządzania stanem
  6. Zarządzanie efektami ubocznymi - organizacja operacji asynchronicznych i innych efektów ubocznych
  7. Testowanie - wdrożenie kompleksowej strategii testowania
  8. Dokumentacja - zapewnienie dobrze udokumentowanego kodu i architektury

Stosując te zasady, możesz uniknąć chaosu, który mógłby prowadzić do "katastrofy" w twoim projekcie JavaScript - podobnie jak odpowiednia organizacja i zarządzanie pomogłyby uniknąć katastrofy w Parku Jurajskim!

Przejdź do CodeWorlds