Poznaj praktyczne zastosowania najważniejszych wzorców projektowych — prosto, przystępnie, z przykładami w TypeScript oraz z zadaniami do samodzielnego wykonania! 🏗️🚀
Ten przewodnik to nie tylko teoria, ale również konkretne przykłady, wskazówki i gotowe rozwiązania do typowych problemów programistycznych, zarówno po stronie frontendu jak i backendu.
>>Spis treści
- Czym są wzorce projektowe?
- Jak je dzielimy?
- Wzorce kreacyjne
- Wzorce strukturalne
- Wzorce behawioralne
- Zastosowania wzorców w praktyce
- Przydatne narzędzia i materiały
- Zadania do wykonania
>>Czym są wzorce projektowe?
Wzorce projektowe to sprawdzone, uniwersalne rozwiązania dla często spotykanych problemów w programowaniu. Możesz potraktować je jak gotowe "przepisy" — nie musisz wymyślać wszystkiego od nowa, tylko korzystasz z doświadczenia innych.
>>>Dlaczego warto ich używać?
- 🤝 Ułatwiają komunikację w zespole (wszyscy rozumieją, o czym mowa)
- 🏗️ Poprawiają strukturę, elastyczność i czytelność kodu
- ⚡ Przyspieszają projektowanie i rozwój systemów
- 🐞 Pozwalają unikać typowych błędów
>>Jak je dzielimy?
Wzorce projektowe dzielimy na trzy główne kategorie:
- Kreacyjne — dotyczą sposobów tworzenia obiektów (np. Singleton, Factory, Builder)
- Strukturalne — pokazują jak łączyć obiekty i klasy (np. Facade, Adapter)
- Behawioralne — opisują interakcje i przepływ informacji (np. Observer, Strategy)
Każda kategoria rozwiązuje inne typy problemów, dlatego tak ważne jest, by znać przynajmniej po jednym wzorcu z każdej grupy.
>>Wzorce kreacyjne
>>>Singleton
Zapewnia, że dana klasa posiada tylko jedną instancję i zapewnia do niej globalny dostęp.
class Singleton {
private static instance: Singleton;
private constructor() {}
static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}
Zastosowania: konfiguracja aplikacji, połączenie z bazą danych, logger.
>>>Factory
Pozwala tworzyć obiekty bez określania ich dokładnych klas — klient nie musi znać szczegółów implementacji.
interface Button {
render(): void;
}
class WindowsButton implements Button {
render() {
console.log("Render Windows Button");
}
}
class MacButton implements Button {
render() {
console.log("Render Mac Button");
}
}
class ButtonFactory {
static createButton(os: string): Button {
if (os === "Windows") return new WindowsButton();
return new MacButton();
}
}
const button = ButtonFactory.createButton("Windows");
button.render();
Zastosowania: dynamiczne UI, obsługa wielu platform.
>>>Builder
Ułatwia tworzenie złożonych obiektów krok po kroku — szczególnie przy wielu opcjonalnych parametrach.
class Burger {
constructor(
public bun: string,
public meat: string,
public extras?: string[]
) {}
}
class BurgerBuilder {
private bun = "classic";
private meat = "beef";
private extras: string[] = [];
setBun(bun: string) {
this.bun = bun;
return this;
}
setMeat(meat: string) {
this.meat = meat;
return this;
}
addExtra(extra: string) {
this.extras.push(extra);
return this;
}
build(): Burger {
return new Burger(this.bun, this.meat, this.extras);
}
}
const burger = new BurgerBuilder()
.setMeat("chicken")
.addExtra("cheese")
.build();
Zastosowania: konfiguratory, kreatory złożonych obiektów, generatory formularzy.
>>Wzorce strukturalne
>>>Facade
Upraszcza korzystanie ze złożonych systemów, oferując prosty interfejs do wielu operacji.
class AudioSystem {
turnOn() {}
setVolume(level: number) {}
}
class VideoSystem {
turnOn() {}
setResolution(res: string) {}
}
class HomeTheaterFacade {
private audio = new AudioSystem();
private video = new VideoSystem();
startMovie() {
this.audio.turnOn();
this.audio.setVolume(5);
this.video.turnOn();
this.video.setResolution("1080p");
}
}
const theater = new HomeTheaterFacade();
theater.startMovie();
Zastosowania: uproszczone API, integracja wielu zależności.
>>>Adapter
Pozwala współpracować obiektom z niekompatybilnymi interfejsami — "tłumaczy" jeden interfejs na inny.
class OldPrinter {
printText(text: string) {
console.log("Old Printer: " + text);
}
}
interface NewPrinter {
print(content: string): void;
}
class PrinterAdapter implements NewPrinter {
constructor(private oldPrinter: OldPrinter) {}
print(content: string) {
this.oldPrinter.printText(content);
}
}
const adapter = new PrinterAdapter(new OldPrinter());
adapter.print("Hello");
Zastosowania: integracja z zewnętrznymi bibliotekami, starszym kodem, migracje.
>>Wzorce behawioralne
>>>Observer
Obiekt "subject" powiadamia inne obiekty (obserwatorów) o zmianach stanu — bez ścisłego powiązania.
interface Observer {
update(data: any): void;
}
class Subject {
private observers: Observer[] = [];
add(observer: Observer) {
this.observers.push(observer);
}
notify(data: any) {
for (const obs of this.observers) {
obs.update(data);
}
}
}
class Logger implements Observer {
update(data: any) {
console.log("Log:", data);
}
}
const subject = new Subject();
subject.add(new Logger());
subject.notify("Dane się zmieniły");
Zastosowania: systemy notyfikacji, reactive programming, event-driven.
>>>Strategy
Pozwala zamieniać algorytmy w trakcie działania aplikacji, bez zmiany jej kodu.
interface PaymentStrategy {
pay(amount: number): void;
}
class PayPal implements PaymentStrategy {
pay(amount: number) {
console.log(`PayPal: Paid ${amount}`);
}
}
class CreditCard implements PaymentStrategy {
pay(amount: number) {
console.log(`Card: Paid ${amount}`);
}
}
class Checkout {
constructor(private strategy: PaymentStrategy) {}
processPayment(amount: number) {
this.strategy.pay(amount);
}
}
const checkout = new Checkout(new PayPal());
checkout.processPayment(100);
Zastosowania: płatności, logika decyzyjna, AI.
>>Zastosowania wzorców w praktyce
- Frontend: React, Angular czy Vue często korzystają z Observera (np. Redux, RxJS), Strategy (dynamiczny wybór komponentów), Factory (tworzenie widgetów), Facade (warstwa usług API).
- Backend: Singleton dla połączeń do bazy danych, Builder przy generowaniu zapytań, Adapter do integracji z zewnętrznymi serwisami.
>>Przydatne narzędzia i materiały
- Refactoring.guru – wzorce projektowe po polsku i angielsku
- TypeScript Playground
- Wzorce projektowe – Wikipedia
- [Książka „Wzorce projektowe. Elementy oprogramowania obiektowego” – Gamma, Helm, Johnson, Vlissides]
>>Zadania do wykonania
>>>Zadanie 1: Singleton w praktyce
Zaimplementuj klasę Logger, która realizuje wzorzec Singleton i umożliwia logowanie wiadomości do konsoli. Upewnij się, że niezależnie od liczby wywołań zawsze używana jest ta sama instancja loggera.
Pokaż rozwiązanie
class Logger {
private static instance: Logger;
private constructor() {}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
log(message: string) {
console.log(message);
}
}
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
console.log(logger1 === logger2); // true
logger1.log("Wiadomość testowa");
>>>Zadanie 2: Factory dla przycisków
Zaimplementuj prostą fabrykę (Factory), która w zależności od przekazanego typu zwróci przycisk HTML (<button>
) lub SVG (<svg>
). Dodaj odpowiednie klasy ButtonHtml i ButtonSvg.
Pokaż rozwiązanie
interface Button {
render(): string;
}
class ButtonHtml implements Button {
render() {
return "<button>Przycisk HTML</button>";
}
}
class ButtonSvg implements Button {
render() {
return "<svg><rect width='100' height='30'/></svg>";
}
}
class ButtonFactory {
static createButton(type: string): Button {
if (type === "html") return new ButtonHtml();
return new ButtonSvg();
}
}
const btn = ButtonFactory.createButton("svg");
console.log(btn.render());
>>>Zadanie 3: Builder do pizzy
Stwórz klasę PizzaBuilder, umożliwiającą tworzenie pizzy z różnymi składnikami (np. ser, szynka, pieczarki) oraz rodzajem ciasta.
Pokaż rozwiązanie
class Pizza {
constructor(public dough: string, public ingredients: string[]) {}
}
class PizzaBuilder {
private dough = "classic";
private ingredients: string[] = [];
setDough(type: string) {
this.dough = type;
return this;
}
addIngredient(ingredient: string) {
this.ingredients.push(ingredient);
return this;
}
build(): Pizza {
return new Pizza(this.dough, this.ingredients);
}
}
const pizza = new PizzaBuilder()
.setDough("thin")
.addIngredient("cheese")
.addIngredient("ham")
.build();
console.log(pizza);
>>>Zadanie 4: Adapter — stare i nowe API
Załóż, że masz starą klasę ApiV1 z metodą getUserData(), a chcesz korzystać z nowego interfejsu NewApi z metodą fetchUser(). Napisz adapter.
Pokaż rozwiązanie
class ApiV1 {
getUserData() {
return { name: "Jan", age: 30 };
}
}
interface NewApi {
fetchUser(): object;
}
class ApiAdapter implements NewApi {
constructor(private oldApi: ApiV1) {}
fetchUser() {
return this.oldApi.getUserData();
}
}
const adapter = new ApiAdapter(new ApiV1());
console.log(adapter.fetchUser());
>>>Zadanie 5: Strategy — wybór algorytmu płatności
Zaimplementuj dwie strategie płatności: przelew i BLIK (obie wypisują kwotę w konsoli). Stwórz klasę, która umożliwi wybór strategii w trakcie działania programu.
Pokaż rozwiązanie
interface PaymentStrategy {
pay(amount: number): void;
}
class Transfer implements PaymentStrategy {
pay(amount: number) {
console.log(`Płatność przelewem: ${amount} zł`);
}
}
class Blik implements PaymentStrategy {
pay(amount: number) {
console.log(`Płatność BLIK: ${amount} zł`);
}
}
class PaymentProcessor {
constructor(private strategy: PaymentStrategy) {}
setStrategy(strategy: PaymentStrategy) {
this.strategy = strategy;
}
process(amount: number) {
this.strategy.pay(amount);
}
}
const processor = new PaymentProcessor(new Transfer());
processor.process(50);
processor.setStrategy(new Blik());
processor.process(75);
To dopiero początek! Każdy z tych wzorców ma swoje warianty, rozbudowane zastosowania i pułapki, których warto unikać.