Wzorce Projektowe w JavaScript - Pytania dla Seniorów 2026

Sławomir Plamowski 22 min czytania
architektura design-patterns interview-questions javascript oop senior wzorce-projektowe

"Opisz różnicę między wzorcem Strategy a State i pokaż kiedy użyłbyś każdego z nich." To pytanie potrafi zaskoczyć nawet doświadczonych developerów. Znajomość wzorców teoretycznie to jedno - wyjaśnienie subtelnych różnic i trade-offów w kontekście realnych projektów to zupełnie inna umiejętność.

Na poziomie seniora znajomość składni to za mało. Rekruterzy sprawdzają, czy rozumiesz architekturę i potrafisz podejmować świadome decyzje projektowe. Kandydaci, którzy znają design patterns nie tylko z nazwy, ale potrafią wyjaśnić kiedy je stosować, a kiedy unikać - wyróżniają się od reszty.

W tym artykule znajdziesz najważniejsze wzorce projektowe pojawiające się na rozmowach na stanowiska senior i lead - wraz z implementacjami w nowoczesnym JavaScript i dyskusją o praktycznych zastosowaniach.

Singleton - Jeden Obiekt Rządzi Wszystkim

Odpowiedź w 30 sekund

Singleton zapewnia, że klasa ma tylko jedną instancję i udostępnia do niej globalny punkt dostępu. W JavaScript najprościej zaimplementować go przez eksport instancji zamiast klasy. Stosuj dla serwisów, które muszą być współdzielone: logger, cache, połączenie z bazą danych.

flowchart LR subgraph Klienci A[Moduł A] B[Moduł B] C[Moduł C] end subgraph Singleton S[getInstance] I[(Jedna Instancja)] end A -->|żądanie| S B -->|żądanie| S C -->|żądanie| S S -->|zwraca tę samą| I style I fill:#e1f5fe style S fill:#fff3e0

Odpowiedź w 2 minuty

Singleton to jeden z najbardziej kontrowersyjnych wzorców. Z jednej strony jest prosty i rozwiązuje realny problem - potrzebę jednej, współdzielonej instancji. Z drugiej strony łatwo go nadużyć i wprowadzić ukryty globalny stan, który utrudnia testowanie.

Pokażę kilka sposobów implementacji w JavaScript:

// Sposób 1: Eksport instancji (najprostszy, zalecany w ES Modules)
class Logger {
  constructor() {
    this.logs = [];
  }

  log(message) {
    const timestamp = new Date().toISOString();
    this.logs.push({ timestamp, message });
    console.log(`[${timestamp}] ${message}`);
  }

  getLogs() {
    return [...this.logs];
  }
}

// Eksportujemy instancję, nie klasę
// Każdy import dostaje tą samą instancję
export const logger = new Logger();

// Sposób 2: Lazy initialization z closure
const DatabaseConnection = (() => {
  let instance = null;

  class Connection {
    constructor(config) {
      this.config = config;
      this.connected = false;
    }

    async connect() {
      if (!this.connected) {
        // Symulacja połączenia
        await new Promise(resolve => setTimeout(resolve, 100));
        this.connected = true;
        console.log('Połączono z bazą danych');
      }
      return this;
    }
  }

  return {
    getInstance(config) {
      if (!instance) {
        instance = new Connection(config);
      }
      return instance;
    }
  };
})();

// Użycie - zawsze ta sama instancja
const db1 = DatabaseConnection.getInstance({ host: 'localhost' });
const db2 = DatabaseConnection.getInstance({ host: 'other-host' });
console.log(db1 === db2); // true

Tu robi się ciekawie: kiedy Singleton jest dobrym wyborem, a kiedy anty-wzorcem? Wzorzec sprawdza się gdy koszt tworzenia obiektu jest wysoki, gdy obiekt zarządza współdzielonym zasobem, lub gdy potrzebujesz pojedynczego punktu koordynacji. Natomiast unikaj go gdy wprowadza ukryty globalny stan, gdy utrudnia testowanie przez niemożność podmienienia instancji, lub gdy jest używany jako "wygodny" sposób na przekazywanie danych między modułami.

Kandydaci, którzy robią wrażenie to ci, którzy wiedzą że w świecie ES Modules sam fakt, że moduły są cache'owane przez runtime sprawia, że często nie potrzebujesz explicite implementować Singletona.

Factory Pattern - Fabryka Obiektów

Odpowiedź w 30 sekund

Factory to wzorzec, który enkapsuluje logikę tworzenia obiektów. Zamiast używać bezpośrednio konstruktora, wywołujesz metodę fabryki, która decyduje jaki typ obiektu utworzyć. Przydatny gdy typ obiektu zależy od parametrów lub konfiguracji.

flowchart TB C[Klient] -->|create 'email'| F[NotificationFactory] F -->|typ = email| E[EmailNotification] F -->|typ = sms| S[SMSNotification] F -->|typ = push| P[PushNotification] subgraph Produkty E S P end style F fill:#fff3e0 style E fill:#e8f5e9 style S fill:#e8f5e9 style P fill:#e8f5e9

Odpowiedź w 2 minuty

Factory rozwiązuje problem: "mam wiele podobnych typów obiektów i chcę scentralizować logikę ich tworzenia". Zamiast rozrzuconych po kodzie instrukcji switch/if decydujących który konstruktor wywołać, masz jedno miejsce odpowiedzialne za tę decyzję.

// Przykład: System powiadomień z różnymi kanałami
class EmailNotification {
  constructor(recipient, message) {
    this.recipient = recipient;
    this.message = message;
    this.type = 'email';
  }

  send() {
    console.log(`Wysyłam email do ${this.recipient}: ${this.message}`);
    // Logika wysyłania emaila
  }
}

class SMSNotification {
  constructor(recipient, message) {
    this.recipient = recipient;
    this.message = message;
    this.type = 'sms';
  }

  send() {
    console.log(`Wysyłam SMS do ${this.recipient}: ${this.message}`);
    // Logika wysyłania SMS
  }
}

class PushNotification {
  constructor(recipient, message) {
    this.recipient = recipient;
    this.message = message;
    this.type = 'push';
  }

  send() {
    console.log(`Wysyłam push do ${this.recipient}: ${this.message}`);
    // Logika wysyłania push notification
  }
}

// Factory - centralizuje logikę tworzenia
class NotificationFactory {
  static create(type, recipient, message) {
    const notifications = {
      email: EmailNotification,
      sms: SMSNotification,
      push: PushNotification
    };

    const NotificationClass = notifications[type];

    if (!NotificationClass) {
      throw new Error(`Nieznany typ powiadomienia: ${type}`);
    }

    return new NotificationClass(recipient, message);
  }

  // Metoda pomocnicza do tworzenia wielu powiadomień
  static createBatch(configs) {
    return configs.map(({ type, recipient, message }) =>
      this.create(type, recipient, message)
    );
  }
}

// Użycie - kod klienta nie zna konkretnych klas
const notification = NotificationFactory.create(
  'email',
  'user@example.com',
  'Witaj w systemie!'
);
notification.send();

// Łatwe dodawanie nowych typów - wystarczy rozszerzyć mapę w Factory

Wzorzec, który mi się sprawdził: łącz Factory z konfiguracją. Zamiast hardkodować mapę typów, wczytuj ją z pliku konfiguracyjnego. Dzięki temu możesz dodawać nowe typy powiadomień bez modyfikacji kodu Factory.

Różnica między Factory Method a Abstract Factory jest subtelna ale ważna. Factory Method tworzy jeden typ produktu z różnymi wariantami. Abstract Factory tworzy rodziny powiązanych produktów. Przykładowo, Abstract Factory dla UI mogłaby tworzyć cały zestaw: Button, Input, Modal - wszystkie w spójnym stylu (Material, Bootstrap, custom).

Observer Pattern - Subskrypcja i Powiadomienia

Odpowiedź w 30 sekund

Observer definiuje relację jeden-do-wielu między obiektami. Gdy Subject zmienia stan, wszyscy zarejestrowani Observers są automatycznie powiadamiani. W JavaScript to podstawa EventEmitter, RxJS, i systemów reaktywnych.

sequenceDiagram participant O1 as Observer 1 participant O2 as Observer 2 participant S as Subject (Store) O1->>S: subscribe('change') O2->>S: subscribe('change') Note over S: Stan się zmienia S->>O1: notify(newState) S->>O2: notify(newState) O1->>S: unsubscribe() Note over S: Stan się zmienia ponownie S->>O2: notify(newState) Note over O1: Nie otrzymuje powiadomienia

Odpowiedź w 2 minuty

Observer jest wszechobecny w JavaScript - od zdarzeń DOM, przez Node.js EventEmitter, po biblioteki reaktywne. Zrozumienie tego wzorca to klucz do pracy z asynchronicznym kodem i architekturą event-driven.

// Własna implementacja Observer Pattern
class EventEmitter {
  constructor() {
    this.events = new Map();
  }

  // Subskrypcja - rejestracja obserwatora
  on(event, callback) {
    if (!this.events.has(event)) {
      this.events.set(event, new Set());
    }
    this.events.get(event).add(callback);

    // Zwracamy funkcję do unsubscribe (przydatne!)
    return () => this.off(event, callback);
  }

  // Jednorazowa subskrypcja
  once(event, callback) {
    const wrapper = (...args) => {
      callback(...args);
      this.off(event, wrapper);
    };
    return this.on(event, wrapper);
  }

  // Usunięcie subskrypcji
  off(event, callback) {
    const callbacks = this.events.get(event);
    if (callbacks) {
      callbacks.delete(callback);
    }
  }

  // Powiadomienie wszystkich obserwatorów
  emit(event, ...args) {
    const callbacks = this.events.get(event);
    if (callbacks) {
      callbacks.forEach(callback => {
        try {
          callback(...args);
        } catch (error) {
          console.error(`Błąd w obserwatorze dla ${event}:`, error);
        }
      });
    }
  }

  // Ile obserwatorów nasłuchuje?
  listenerCount(event) {
    return this.events.get(event)?.size || 0;
  }
}

// Praktyczne użycie: Store z powiadomieniami o zmianach
class Store extends EventEmitter {
  constructor(initialState = {}) {
    super();
    this.state = initialState;
  }

  getState() {
    return { ...this.state };
  }

  setState(updates) {
    const prevState = this.state;
    this.state = { ...this.state, ...updates };

    // Powiadom wszystkich obserwatorów o zmianie
    this.emit('change', this.state, prevState);

    // Powiadom o konkretnych zmianach
    Object.keys(updates).forEach(key => {
      if (prevState[key] !== updates[key]) {
        this.emit(`change:${key}`, updates[key], prevState[key]);
      }
    });
  }
}

// Użycie
const userStore = new Store({ name: '', isLoggedIn: false });

// Subskrypcja na wszystkie zmiany
const unsubscribe = userStore.on('change', (newState, oldState) => {
  console.log('Stan zmieniony:', newState);
});

// Subskrypcja na konkretne pole
userStore.on('change:isLoggedIn', (newValue, oldValue) => {
  console.log(`Status logowania: ${oldValue} -> ${newValue}`);
});

userStore.setState({ name: 'Jan', isLoggedIn: true });
// Wywoła oba callbacki

// Pamiętaj o cleanup!
unsubscribe();

Klasyczny problem na rozmowie: "Jak uniknąć memory leaks w Observer pattern?". Odpowiedź: zawsze pamiętaj o unsubscribe, używaj WeakMap do przechowywania referencji gdy to możliwe, i rozważ wzorzec z automatycznym cleanup przy unmount komponentu.

Module Pattern - Enkapsulacja przed ES6

Odpowiedź w 30 sekund

Module Pattern wykorzystuje closure i IIFE do tworzenia prywatnych zmiennych i eksportowania publicznego API. Przed ES6 był jedynym sposobem na enkapsulację w JavaScript. Dziś używamy natywnych ES Modules, ale wzorzec nadal spotykany w legacy code.

Odpowiedź w 2 minuty

Module Pattern to kawałek historii JavaScript, który warto znać. Zanim pojawiły się natywne moduły ES6, developerzy musieli symulować prywatność za pomocą closure. Ten wzorzec pokazuje głębokie zrozumienie jak działa JavaScript.

// Klasyczny Module Pattern z IIFE
const Calculator = (function() {
  // Prywatne zmienne - niedostępne z zewnątrz
  let result = 0;
  const history = [];

  // Prywatna funkcja pomocnicza
  function addToHistory(operation) {
    history.push({
      operation,
      result,
      timestamp: Date.now()
    });
  }

  // Publiczne API - zwracane przez IIFE
  return {
    add(value) {
      result += value;
      addToHistory(`+ ${value}`);
      return this; // Chainowanie
    },

    subtract(value) {
      result -= value;
      addToHistory(`- ${value}`);
      return this;
    },

    multiply(value) {
      result *= value;
      addToHistory(`* ${value}`);
      return this;
    },

    getResult() {
      return result;
    },

    getHistory() {
      // Zwracamy kopię, nie referencję
      return [...history];
    },

    reset() {
      result = 0;
      addToHistory('reset');
      return this;
    }
  };
})();

// Użycie - tylko publiczne metody są dostępne
Calculator.add(5).multiply(2).subtract(3);
console.log(Calculator.getResult()); // 7
console.log(Calculator.history); // undefined - prywatne!
console.log(Calculator.getHistory()); // [...] - przez publiczną metodę

// Revealing Module Pattern - wariant z jaśniejszą strukturą
const UserService = (function() {
  // Wszystkie funkcje definiujemy jako prywatne
  let users = [];

  function findById(id) {
    return users.find(user => user.id === id);
  }

  function add(user) {
    users.push({ ...user, id: Date.now() });
    return users[users.length - 1];
  }

  function remove(id) {
    const index = users.findIndex(user => user.id === id);
    if (index !== -1) {
      return users.splice(index, 1)[0];
    }
    return null;
  }

  function getAll() {
    return [...users];
  }

  // Ujawniamy tylko wybrane funkcje
  return {
    findById,
    add,
    remove,
    getAll
    // users nie jest ujawnione - prawdziwie prywatne
  };
})();

Nowoczesny odpowiednik z ES Modules wygląda znacznie czyściej:

// userService.js - nowoczesne podejście z ES Modules
let users = []; // Prywatne - niedostępne poza modułem

export function findById(id) {
  return users.find(user => user.id === id);
}

export function add(user) {
  users.push({ ...user, id: Date.now() });
  return users[users.length - 1];
}

// Możemy też eksportować domyślnie obiekt jak w Module Pattern
export default {
  findById,
  add
};

Decorator Pattern - Rozszerzanie bez Dziedziczenia

Odpowiedź w 30 sekund

Decorator dynamicznie dodaje funkcjonalność do obiektów bez modyfikacji oryginalnej klasy. W JavaScript implementujemy go przez higher-order functions lub dekoratory ES7. Przydatny gdy potrzebujesz elastycznych kombinacji rozszerzeń.

flowchart LR subgraph Kompozycja Dekoratorów direction LR B[BasicLogger] --> T[withTimestamp] T --> L[withLevel] L --> F[withFileOutput] end C[Klient] -->|log| F F -->|dodaje buffer| L L -->|dodaje poziom| T T -->|dodaje czas| B B -->|console.log| O[Output] style B fill:#e3f2fd style T fill:#fff3e0 style L fill:#fff3e0 style F fill:#fff3e0

Odpowiedź w 2 minuty

Decorator rozwiązuje problem "eksplozji klas" - gdy masz wiele opcjonalnych funkcjonalności i każda ich kombinacja wymagałaby osobnej klasy. Zamiast tego opakowujesz obiekt w warstwy dodające kolejne funkcje.

// Przykład: System logowania z różnymi rozszerzeniami

// Bazowa klasa - podstawowe logowanie
class BasicLogger {
  log(message) {
    console.log(message);
  }
}

// Decorator dodający timestamp
function withTimestamp(logger) {
  return {
    log(message) {
      const timestamp = new Date().toISOString();
      logger.log(`[${timestamp}] ${message}`);
    }
  };
}

// Decorator dodający poziom logowania
function withLevel(logger, defaultLevel = 'INFO') {
  return {
    log(message, level = defaultLevel) {
      logger.log(`[${level}] ${message}`);
    },
    info(message) {
      this.log(message, 'INFO');
    },
    warn(message) {
      this.log(message, 'WARN');
    },
    error(message) {
      this.log(message, 'ERROR');
    }
  };
}

// Decorator dodający zapis do pliku (symulacja)
function withFileOutput(logger, filename) {
  const buffer = [];

  return {
    ...logger,
    log(message, ...args) {
      buffer.push(message);
      logger.log(message, ...args);
    },
    flush() {
      console.log(`Zapisuję ${buffer.length} wpisów do ${filename}`);
      buffer.length = 0;
    }
  };
}

// Kompozycja dekoratorów - elastyczne łączenie funkcjonalności
const simpleLogger = new BasicLogger();
const timestampLogger = withTimestamp(simpleLogger);
const fullLogger = withFileOutput(
  withLevel(withTimestamp(simpleLogger)),
  'app.log'
);

fullLogger.info('Aplikacja uruchomiona');
fullLogger.warn('Niski poziom pamięci');
fullLogger.flush();

// ES7 Decorators (wymaga transpilacji)
function logMethod(target, name, descriptor) {
  const original = descriptor.value;

  descriptor.value = function(...args) {
    console.log(`Wywołanie ${name} z argumentami:`, args);
    const result = original.apply(this, args);
    console.log(`${name} zwróciło:`, result);
    return result;
  };

  return descriptor;
}

function memoize(target, name, descriptor) {
  const original = descriptor.value;
  const cache = new Map();

  descriptor.value = function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      console.log(`Cache hit dla ${name}`);
      return cache.get(key);
    }
    const result = original.apply(this, args);
    cache.set(key, result);
    return result;
  };

  return descriptor;
}

class MathService {
  @logMethod
  @memoize
  fibonacci(n) {
    if (n <= 1) return n;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

Wzorzec, który mi się sprawdził: używaj Decorator gdy masz "przekrojowe" funkcjonalności (cross-cutting concerns) jak logowanie, caching, walidacja. Zamiast wrzucać tę logikę do każdej metody, opakowujesz całą klasę lub funkcję w dekorator.

Strategy Pattern - Wymienne Algorytmy

Odpowiedź w 30 sekund

Strategy definiuje rodzinę algorytmów, enkapsuluje każdy z nich i czyni je wymiennymi. Pozwala zmieniać algorytm niezależnie od klientów, którzy go używają. W JavaScript często implementowany przez przekazywanie funkcji.

flowchart TB subgraph Kontekst P[PaymentProcessor] end subgraph Strategie CC[CreditCard Strategy] PP[PayPal Strategy] BT[BankTransfer Strategy] end P -->|setStrategy| CC P -.->|lub| PP P -.->|lub| BT CC -->|process| R1[Bramka kart] PP -->|process| R2[PayPal API] BT -->|process| R3[Bank API] style P fill:#fff3e0 style CC fill:#e8f5e9 style PP fill:#e8f5e9 style BT fill:#e8f5e9

Odpowiedź w 2 minuty

Strategy to eleganckie rozwiązanie gdy masz wiele sposobów wykonania tego samego zadania i chcesz móc je zmieniać dynamicznie. Klasyczny przykład to różne strategie sortowania czy walidacji.

// Przykład: System płatności z różnymi metodami

// Strategie płatności
const paymentStrategies = {
  creditCard: {
    validate(data) {
      return data.cardNumber && data.cvv && data.expiry;
    },
    async process(amount, data) {
      console.log(`Przetwarzam płatność kartą ${data.cardNumber.slice(-4)}`);
      // Integracja z bramką płatności
      return { success: true, transactionId: `CC-${Date.now()}` };
    },
    getFee(amount) {
      return amount * 0.029; // 2.9% prowizji
    }
  },

  paypal: {
    validate(data) {
      return data.email;
    },
    async process(amount, data) {
      console.log(`Przetwarzam płatność PayPal dla ${data.email}`);
      return { success: true, transactionId: `PP-${Date.now()}` };
    },
    getFee(amount) {
      return amount * 0.034 + 0.35; // 3.4% + 35gr
    }
  },

  bankTransfer: {
    validate(data) {
      return data.accountNumber && data.bankCode;
    },
    async process(amount, data) {
      console.log(`Inicjuję przelew na konto ${data.accountNumber}`);
      return { success: true, transactionId: `BT-${Date.now()}` };
    },
    getFee(amount) {
      return 0; // Bez prowizji
    }
  }
};

// Kontekst używający strategii
class PaymentProcessor {
  constructor(strategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  async processPayment(amount, paymentData) {
    if (!this.strategy.validate(paymentData)) {
      throw new Error('Nieprawidłowe dane płatności');
    }

    const fee = this.strategy.getFee(amount);
    const totalAmount = amount + fee;

    console.log(`Kwota: ${amount}, Prowizja: ${fee}, Razem: ${totalAmount}`);

    return this.strategy.process(totalAmount, paymentData);
  }
}

// Użycie - łatwa zmiana strategii
const processor = new PaymentProcessor(paymentStrategies.creditCard);

// Płatność kartą
await processor.processPayment(100, {
  cardNumber: '4111111111111111',
  cvv: '123',
  expiry: '12/25'
});

// Zmiana na PayPal w runtime
processor.setStrategy(paymentStrategies.paypal);
await processor.processPayment(100, { email: 'user@example.com' });

// Strategy dla walidacji formularzy
const validationStrategies = {
  email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
  phone: (value) => /^\+?[0-9]{9,15}$/.test(value),
  required: (value) => value !== null && value !== undefined && value !== '',
  minLength: (min) => (value) => value.length >= min,
  maxLength: (max) => (value) => value.length <= max,
  pattern: (regex) => (value) => regex.test(value)
};

// Kompozycja strategii walidacji
function createValidator(rules) {
  return (value) => {
    for (const [ruleName, ruleConfig] of Object.entries(rules)) {
      const strategy = typeof ruleConfig === 'function'
        ? ruleConfig
        : validationStrategies[ruleName];

      if (strategy && !strategy(value)) {
        return { valid: false, error: ruleName };
      }
    }
    return { valid: true };
  };
}

const emailValidator = createValidator({
  required: true,
  email: true
});

console.log(emailValidator('test@example.com')); // { valid: true }
console.log(emailValidator('')); // { valid: false, error: 'required' }

Dependency Injection - Fundament Testowalnego Kodu

Odpowiedź w 30 sekund

Dependency Injection polega na przekazywaniu zależności do klasy z zewnątrz zamiast tworzenia ich wewnątrz. Dzięki temu możesz podmienić zależności na mocki w testach. To nie framework - to zasada projektowania.

flowchart TB subgraph "❌ Bez DI - Tight Coupling" US1[UserService] US1 -->|tworzy wewnątrz| DB1[PostgresDatabase] US1 -->|tworzy wewnątrz| EM1[SendGridEmail] end subgraph "✅ Z DI - Loose Coupling" C[Container/Main] C -->|wstrzykuje| US2[UserService] C -->|tworzy| DB2[PostgresDatabase] C -->|tworzy| EM2[SendGridEmail] DB2 -.->|interfejs| US2 EM2 -.->|interfejs| US2 end subgraph "🧪 W Testach" T[Test] T -->|wstrzykuje mocki| US3[UserService] T -->|tworzy| MDB[MockDatabase] T -->|tworzy| MEM[MockEmail] MDB -.->|ten sam interfejs| US3 MEM -.->|ten sam interfejs| US3 end style US1 fill:#ffcdd2 style US2 fill:#c8e6c9 style US3 fill:#c8e6c9

Odpowiedź w 2 minuty

Dependency Injection to chyba najważniejsza praktyka dla kodu produkcyjnego. Bez niej testowanie jednostkowe jest praktycznie niemożliwe. Pokażę różnicę między kodem bez DI i z DI:

// ZŁY przykład - bez Dependency Injection
class UserService {
  constructor() {
    // Tworzenie zależności wewnątrz klasy
    this.database = new PostgresDatabase();
    this.emailService = new SendGridEmailService();
    this.logger = new FileLogger('/var/log/app.log');
  }

  async createUser(userData) {
    // Jak to przetestować bez prawdziwej bazy i emaila?
    const user = await this.database.insert('users', userData);
    await this.emailService.send(user.email, 'Witaj!');
    this.logger.log(`Utworzono użytkownika ${user.id}`);
    return user;
  }
}

// DOBRY przykład - z Dependency Injection
class UserService {
  constructor(database, emailService, logger) {
    // Zależności przekazane z zewnątrz
    this.database = database;
    this.emailService = emailService;
    this.logger = logger;
  }

  async createUser(userData) {
    const user = await this.database.insert('users', userData);
    await this.emailService.send(user.email, 'Witaj!');
    this.logger.log(`Utworzono użytkownika ${user.id}`);
    return user;
  }
}

// W produkcji - prawdziwe implementacje
const userService = new UserService(
  new PostgresDatabase(),
  new SendGridEmailService(),
  new FileLogger('/var/log/app.log')
);

// W testach - mocki
const mockDatabase = {
  insert: jest.fn().mockResolvedValue({ id: 1, email: 'test@test.com' })
};
const mockEmailService = {
  send: jest.fn().mockResolvedValue(true)
};
const mockLogger = {
  log: jest.fn()
};

const testUserService = new UserService(
  mockDatabase,
  mockEmailService,
  mockLogger
);

// Teraz możemy testować bez zewnętrznych zależności!
test('createUser wysyła email powitalny', async () => {
  await testUserService.createUser({ email: 'new@user.com' });

  expect(mockEmailService.send).toHaveBeenCalledWith(
    'test@test.com',
    'Witaj!'
  );
});

Wzorzec, który mi się sprawdził: stwórz prosty kontener DI, który zarządza tworzeniem i wstrzykiwaniem zależności:

// Prosty kontener Dependency Injection
class Container {
  constructor() {
    this.services = new Map();
    this.singletons = new Map();
  }

  // Rejestracja factory function
  register(name, factory, { singleton = false } = {}) {
    this.services.set(name, { factory, singleton });
    return this;
  }

  // Pobranie instancji serwisu
  resolve(name) {
    const service = this.services.get(name);
    if (!service) {
      throw new Error(`Serwis "${name}" nie został zarejestrowany`);
    }

    if (service.singleton) {
      if (!this.singletons.has(name)) {
        this.singletons.set(name, service.factory(this));
      }
      return this.singletons.get(name);
    }

    return service.factory(this);
  }
}

// Konfiguracja kontenera
const container = new Container();

container
  .register('logger', () => new ConsoleLogger(), { singleton: true })
  .register('database', () => new PostgresDatabase(), { singleton: true })
  .register('emailService', () => new SendGridEmailService())
  .register('userService', (c) => new UserService(
    c.resolve('database'),
    c.resolve('emailService'),
    c.resolve('logger')
  ));

// Użycie
const userService = container.resolve('userService');

Proxy Pattern - Kontrola Dostępu

Odpowiedź w 30 sekund

Proxy działa jako pośrednik kontrolujący dostęp do obiektu. Może dodać logowanie, caching, walidację lub lazy loading bez modyfikacji oryginalnego obiektu. JavaScript ma wbudowany obiekt Proxy idealny do tego celu.

flowchart LR C[Klient] -->|get/set| P[Proxy] P -->|walidacja| V{Walidacja OK?} V -->|tak| O[Oryginalny Obiekt] V -->|nie| E[Error] P -->|logowanie| L[(Logi)] subgraph Proxy Handler P V L end style P fill:#fff3e0 style O fill:#e8f5e9 style E fill:#ffcdd2

Odpowiedź w 2 minuty

Proxy w JavaScript (ES6) to potężne narzędzie, które pozwala przechwytywać i modyfikować fundamentalne operacje na obiektach. To nie tylko wzorzec projektowy, ale wbudowana funkcjonalność języka.

// Proxy do walidacji i logowania dostępu
const user = {
  name: 'Jan',
  email: 'jan@example.com',
  age: 25,
  _password: 'secret123' // Konwencja: prywatne
};

const userProxy = new Proxy(user, {
  get(target, property) {
    // Blokuj dostęp do "prywatnych" właściwości
    if (property.startsWith('_')) {
      throw new Error(`Dostęp do ${property} jest zabroniony`);
    }

    console.log(`Odczyt: ${property}`);
    return target[property];
  },

  set(target, property, value) {
    // Walidacja przed zapisem
    if (property === 'age' && (typeof value !== 'number' || value < 0)) {
      throw new Error('Wiek musi być liczbą dodatnią');
    }

    if (property === 'email' && !value.includes('@')) {
      throw new Error('Nieprawidłowy format email');
    }

    console.log(`Zapis: ${property} = ${value}`);
    target[property] = value;
    return true;
  },

  deleteProperty(target, property) {
    if (property.startsWith('_')) {
      throw new Error(`Nie można usunąć ${property}`);
    }
    delete target[property];
    return true;
  }
});

userProxy.name; // Odczyt: name -> 'Jan'
userProxy.age = 26; // Zapis: age = 26
userProxy._password; // Error: Dostęp zabroniony
userProxy.age = -5; // Error: Wiek musi być liczbą dodatnią

// Proxy do lazy loading
function createLazyLoader(loader) {
  let cache = null;
  let loaded = false;

  return new Proxy({}, {
    get(target, property) {
      if (!loaded) {
        console.log('Ładowanie danych...');
        cache = loader();
        loaded = true;
      }
      return cache[property];
    }
  });
}

const heavyData = createLazyLoader(() => {
  // Symulacja ciężkiej operacji
  console.log('Wykonuję kosztowne obliczenia...');
  return { result: 42, computed: true };
});

// Dane ładowane dopiero przy pierwszym dostępie
console.log(heavyData.result); // Ładowanie... Obliczenia... 42
console.log(heavyData.computed); // true (z cache)

// Proxy do reaktywności (jak w Vue.js)
function reactive(obj, onChange) {
  return new Proxy(obj, {
    set(target, property, value) {
      const oldValue = target[property];
      target[property] = value;

      if (oldValue !== value) {
        onChange(property, value, oldValue);
      }

      return true;
    }
  });
}

const state = reactive({ count: 0 }, (prop, newVal, oldVal) => {
  console.log(`${prop} zmienione: ${oldVal} -> ${newVal}`);
  // Tu możesz wywołać re-render
});

state.count++; // count zmienione: 0 -> 1

Command Pattern - Enkapsulacja Akcji

Odpowiedź w 30 sekund

Command enkapsuluje żądanie jako obiekt, pozwalając parametryzować klientów różnymi żądaniami, kolejkować lub logować żądania, oraz implementować operacje undo. Każde polecenie jest samodzielnym obiektem z metodą execute().

flowchart TB subgraph Historia direction LR H1[Command 1] --> H2[Command 2] --> H3[Command 3] end subgraph RedoStack direction LR R1[Command 4] end U[Użytkownik] -->|type text| CM[CommandManager] CM -->|execute| CMD[AddTextCommand] CMD -->|insertAt| E[Editor] CM -->|push| Historia U2[Użytkownik] -->|undo| CM CM -->|pop z Historia| H3 H3 -->|undo/deleteAt| E CM -->|push do Redo| RedoStack style CM fill:#fff3e0 style CMD fill:#e8f5e9 style H1 fill:#e3f2fd style H2 fill:#e3f2fd style H3 fill:#e3f2fd

Odpowiedź w 2 minuty

Command Pattern świetnie sprawdza się w edytorach tekstu, grach, oraz wszędzie gdzie potrzebujesz historii akcji i możliwości cofania.

// System z historią i undo/redo
class Command {
  execute() {
    throw new Error('Metoda execute() musi być zaimplementowana');
  }

  undo() {
    throw new Error('Metoda undo() musi być zaimplementowana');
  }
}

class AddTextCommand extends Command {
  constructor(editor, text, position) {
    super();
    this.editor = editor;
    this.text = text;
    this.position = position;
  }

  execute() {
    this.editor.insertAt(this.position, this.text);
  }

  undo() {
    this.editor.deleteAt(this.position, this.text.length);
  }
}

class DeleteTextCommand extends Command {
  constructor(editor, position, length) {
    super();
    this.editor = editor;
    this.position = position;
    this.length = length;
    this.deletedText = null;
  }

  execute() {
    this.deletedText = this.editor.getTextAt(this.position, this.length);
    this.editor.deleteAt(this.position, this.length);
  }

  undo() {
    this.editor.insertAt(this.position, this.deletedText);
  }
}

// Invoker - zarządza wykonywaniem i historią komend
class CommandManager {
  constructor() {
    this.history = [];
    this.redoStack = [];
  }

  execute(command) {
    command.execute();
    this.history.push(command);
    this.redoStack = []; // Czyścimy redo po nowej akcji
  }

  undo() {
    const command = this.history.pop();
    if (command) {
      command.undo();
      this.redoStack.push(command);
    }
  }

  redo() {
    const command = this.redoStack.pop();
    if (command) {
      command.execute();
      this.history.push(command);
    }
  }

  canUndo() {
    return this.history.length > 0;
  }

  canRedo() {
    return this.redoStack.length > 0;
  }
}

// Prosty edytor
class TextEditor {
  constructor() {
    this.content = '';
    this.commandManager = new CommandManager();
  }

  insertAt(position, text) {
    this.content =
      this.content.slice(0, position) +
      text +
      this.content.slice(position);
  }

  deleteAt(position, length) {
    this.content =
      this.content.slice(0, position) +
      this.content.slice(position + length);
  }

  getTextAt(position, length) {
    return this.content.slice(position, position + length);
  }

  // API dla użytkownika
  type(text) {
    const position = this.content.length;
    const command = new AddTextCommand(this, text, position);
    this.commandManager.execute(command);
  }

  undo() {
    this.commandManager.undo();
  }

  redo() {
    this.commandManager.redo();
  }

  getContent() {
    return this.content;
  }
}

// Użycie
const editor = new TextEditor();
editor.type('Witaj ');
editor.type('świecie!');
console.log(editor.getContent()); // 'Witaj świecie!'

editor.undo();
console.log(editor.getContent()); // 'Witaj '

editor.redo();
console.log(editor.getContent()); // 'Witaj świecie!'

Na Co Rekruterzy Naprawdę Zwracają Uwagę

Po przeprowadzeniu wielu rozmów na stanowiska seniorskie, mogę powiedzieć że rekruterzy szukają konkretnych sygnałów świadczących o dojrzałości architektonicznej.

Pierwszą kwestią jest zrozumienie trade-offów. Senior nie powinien mówić "Singleton jest dobry" albo "Singleton jest zły". Powinien wyjaśnić kiedy ma sens, a kiedy jest anty-wzorcem. Każdy wzorzec ma swoje miejsce i każdy może być nadużyty.

Drugą sprawą jest praktyczne doświadczenie. Teoria to za mało. Rekruter chce usłyszeć o konkretnych sytuacjach: "Użyłem Observer Pattern w systemie powiadomień, bo potrzebowaliśmy luźnego coupling między modułami. Alternatywą był polling, ale Observer był efektywniejszy przy częstych zmianach."

Trzecim elementem jest znajomość nowoczesnych alternatyw. Wiele klasycznych wzorców GoF ma nowoczesne odpowiedniki w JavaScript. Module Pattern zastąpiły ES Modules, Observer jest wbudowany w RxJS i frameworki reaktywne, Strategy często implementujemy przez proste funkcje zamiast klas. Senior powinien znać oba światy.

Ostatnią kwestią jest testowalność. Na rozmowie senior to ktoś, kto pisze testowalny kod. Dependency Injection, separation of concerns, pure functions - to nie tylko "ładne" wzorce, to praktyki umożliwiające skuteczne testowanie.


Praktyka na Koniec

Przed rozmową spróbuj odpowiedzieć na te pytania:

Zaimplementuj prosty system cache'owania z wykorzystaniem wzorców Proxy i Strategy. Cache powinien wspierać różne strategie wygasania: LRU, TTL, i brak wygasania.

Zaprojektuj system pluginów dla edytora tekstu. Każdy plugin powinien móc rejestrować własne komendy, reagować na zdarzenia edytora, i być ładowany dynamicznie.

Masz aplikację e-commerce z różnymi typami produktów (fizyczne, cyfrowe, subskrypcje). Każdy typ ma inną logikę dostawy, wyceny i zwrotów. Które wzorce użyjesz i dlaczego?

Wyjaśnij jak zaimplementowałbyś funkcjonalność undo/redo dla aplikacji do rysowania z wieloma narzędziami (pędzel, gumka, kształty).


Zobacz też


Chcesz Więcej Pytań dla Seniorów?

Ten artykuł to tylko fragment wiedzy potrzebnej na rozmowę senior level. Nasze fiszki online zawierają zaawansowane pytania z JavaScript, architektury, wzorców projektowych i system design - wszystko czego potrzebujesz żeby przejść rozmowę na stanowisko seniora.

Sprawdź Fiszki Online - JavaScript i Architektura

Możesz też najpierw zobaczyć przykładowe pytania w naszym bezpłatnym preview:

Bezpłatny Preview - Pytania JavaScript


Artykuł napisany na podstawie ponad 15 lat doświadczenia w programowaniu JavaScript i TypeScript oraz setek przeprowadzonych rozmów rekrutacyjnych na stanowiska senior i lead developer.

Chcesz więcej pytań rekrutacyjnych?

To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.

Kup pełny dostęp Zobacz bezpłatny podgląd
Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.