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

Wzorzec Builder w JavaScript

W Parku Jurajskim tworzenie nowego dinozaura to złożony proces. Dr. Henry Wu nie może po prostu wrzucić wszystkich parametrów do jednego konstruktora - sekwencja genetyczna, dieta, habitat, poziom zagrożenia, wymagania temperaturowe... To dziesiątki parametrów! Na szczęście istnieje wzorzec Builder - elegancki sposób na budowanie złożonych obiektów krok po kroku.

Problem: Zbyt wiele parametrów

Wyobraź sobie konstruktor dinozaura z wieloma parametrami:

1// Konstruktor z wieloma parametrami - nieczytelne i podatne na błędy
2const dino = new Dinosaur(
3  "Rexy",          // name
4  "Tyrannosaurus",  // species
5  "carnivore",      // diet
6  8,                // age
7  12000,            // weight
8  "Sector A",       // habitat
9  10,               // dangerLevel
10  true,             // isActive
11  38,               // optimalTemp
12  null,             // mate
13  ["hunt", "roar"]  // abilities
14);
15// Który parametr jest który? Łatwo pomylić kolejność!

Wzorzec Builder - rozwiązanie

Builder pozwala budować obiekt krok po kroku, z czytelnym API opartym na łańcuchach wywołań (method chaining):

1class DinosaurBuilder {
2  constructor(name, species) {
3    // Wymagane parametry w konstruktorze
4    this.dinosaur = {
5      name,
6      species,
7      abilities: []
8    };
9  }
10
11  // Każda metoda ustawia właściwość i zwraca this (builder)
12  setDiet(diet) {
13    this.dinosaur.diet = diet;
14    return this; // Kluczowe! Zwraca builder, umożliwiając chaining
15  }
16
17  setAge(age) {
18    this.dinosaur.age = age;
19    return this;
20  }
21
22  setWeight(weight) {
23    this.dinosaur.weight = weight;
24    return this;
25  }
26
27  setHabitat(habitat) {
28    this.dinosaur.habitat = habitat;
29    return this;
30  }
31
32  setDangerLevel(level) {
33    this.dinosaur.dangerLevel = level;
34    return this;
35  }
36
37  setActive(isActive) {
38    this.dinosaur.isActive = isActive;
39    return this;
40  }
41
42  setOptimalTemperature(temp) {
43    this.dinosaur.optimalTemp = temp;
44    return this;
45  }
46
47  addAbility(ability) {
48    this.dinosaur.abilities.push(ability);
49    return this;
50  }
51
52  // Metoda build() tworzy finalny obiekt
53  build() {
54    // Walidacja przed budowaniem
55    if (!this.dinosaur.diet) {
56      throw new Error("Dieta jest wymagana!");
57    }
58    return { ...this.dinosaur };
59  }
60}
61
62// Użycie - czytelne i samodokumentujące się
63const rexy = new DinosaurBuilder("Rexy", "Tyrannosaurus")
64  .setDiet("carnivore")
65  .setAge(8)
66  .setWeight(12000)
67  .setHabitat("Sector A")
68  .setDangerLevel(10)
69  .setActive(true)
70  .setOptimalTemperature(38)
71  .addAbility("hunt")
72  .addAbility("roar")
73  .build();
74
75console.log(rexy);
76// { name: "Rexy", species: "Tyrannosaurus", abilities: ["hunt", "roar"],
77//   diet: "carnivore", age: 8, weight: 12000, habitat: "Sector A",
78//   dangerLevel: 10, isActive: true, optimalTemp: 38 }

Fluent API - method chaining w praktyce

Kluczem do wzorca Builder jest Fluent API - każda metoda zwraca

this
, co pozwala łączyć wywołania w łańcuch:

1// Fluent API do budowania zapytań o dinozaury
2class DinoQueryBuilder {
3  constructor() {
4    this.query = {
5      filters: {},
6      sort: null,
7      limit: 100,
8      offset: 0
9    };
10  }
11
12  whereSpecies(species) {
13    this.query.filters.species = species;
14    return this;
15  }
16
17  whereDiet(diet) {
18    this.query.filters.diet = diet;
19    return this;
20  }
21
22  whereDangerAbove(level) {
23    this.query.filters.minDanger = level;
24    return this;
25  }
26
27  whereActive(isActive = true) {
28    this.query.filters.isActive = isActive;
29    return this;
30  }
31
32  sortBy(field, direction = "asc") {
33    this.query.sort = { field, direction };
34    return this;
35  }
36
37  limitTo(count) {
38    this.query.limit = count;
39    return this;
40  }
41
42  skip(count) {
43    this.query.offset = count;
44    return this;
45  }
46
47  build() {
48    return { ...this.query };
49  }
50
51  // Symulacja wykonania zapytania
52  execute(database) {
53    const query = this.build();
54    console.log("Wykonuję zapytanie:", JSON.stringify(query, null, 2));
55
56    let results = [...database];
57
58    // Filtrowanie
59    if (query.filters.species) {
60      results = results.filter(d => d.species === query.filters.species);
61    }
62    if (query.filters.diet) {
63      results = results.filter(d => d.diet === query.filters.diet);
64    }
65    if (query.filters.minDanger) {
66      results = results.filter(d => d.dangerLevel >= query.filters.minDanger);
67    }
68    if (query.filters.isActive !== undefined) {
69      results = results.filter(d => d.isActive === query.filters.isActive);
70    }
71
72    // Sortowanie
73    if (query.sort) {
74      results.sort((a, b) => {
75        const dir = query.sort.direction === "asc" ? 1 : -1;
76        return a[query.sort.field] > b[query.sort.field] ? dir : -dir;
77      });
78    }
79
80    // Paginacja
81    results = results.slice(query.offset, query.offset + query.limit);
82
83    return results;
84  }
85}
86
87// Przykładowa baza danych
88const dinoDatabase = [
89  { name: "Rexy", species: "Tyrannosaurus", diet: "carnivore", dangerLevel: 10, isActive: true },
90  { name: "Blue", species: "Velociraptor", diet: "carnivore", dangerLevel: 8, isActive: true },
91  { name: "Spike", species: "Triceratops", diet: "herbivore", dangerLevel: 4, isActive: true },
92  { name: "Echo", species: "Velociraptor", diet: "carnivore", dangerLevel: 7, isActive: false },
93  { name: "Trike", species: "Triceratops", diet: "herbivore", dangerLevel: 3, isActive: true }
94];
95
96// Budowanie zapytania z fluent API
97const dangerousCarnivores = new DinoQueryBuilder()
98  .whereDiet("carnivore")
99  .whereDangerAbove(7)
100  .whereActive()
101  .sortBy("dangerLevel", "desc")
102  .limitTo(5)
103  .execute(dinoDatabase);
104
105console.log("Niebezpieczne mięsożerne:", dangerousCarnivores);
106// [{ name: "Rexy", ... }, { name: "Blue", ... }]

Builder z walidacją i wartościami domyślnymi

1class EnclosureBuilder {
2  constructor(name) {
3    this.enclosure = {
4      name,
5      type: "standard",
6      fenceVoltage: 10000,
7      size: 1000,
8      features: [],
9      maxCapacity: 5,
10      currentDinosaurs: []
11    };
12    this.errors = [];
13  }
14
15  setType(type) {
16    const validTypes = ["standard", "aquatic", "aviary", "high-security"];
17    if (!validTypes.includes(type)) {
18      this.errors.push(`Nieprawidłowy typ wybiegu: ${type}`);
19    }
20    this.enclosure.type = type;
21    return this;
22  }
23
24  setFenceVoltage(voltage) {
25    if (voltage < 5000) {
26      this.errors.push("Napięcie ogrodzenia musi być >= 5000V");
27    }
28    this.enclosure.fenceVoltage = voltage;
29    return this;
30  }
31
32  setSize(squareMeters) {
33    if (squareMeters < 100) {
34      this.errors.push("Minimalna powierzchnia to 100 m²");
35    }
36    this.enclosure.size = squareMeters;
37    return this;
38  }
39
40  setMaxCapacity(capacity) {
41    this.enclosure.maxCapacity = capacity;
42    return this;
43  }
44
45  addFeature(feature) {
46    this.enclosure.features.push(feature);
47    return this;
48  }
49
50  addDinosaur(dinosaur) {
51    if (this.enclosure.currentDinosaurs.length >= this.enclosure.maxCapacity) {
52      this.errors.push(`Wybieg pełny! Maks: ${this.enclosure.maxCapacity}`);
53    }
54    this.enclosure.currentDinosaurs.push(dinosaur);
55    return this;
56  }
57
58  build() {
59    if (this.errors.length > 0) {
60      throw new Error(
61        `Błędy budowania wybiegu:\n${this.errors.join("\n")}`
62      );
63    }
64    return Object.freeze({ ...this.enclosure }); // Zamrożony obiekt
65  }
66}
67
68// Budowanie wybiegu
69const raptorPaddock = new EnclosureBuilder("Raptor Paddock")
70  .setType("high-security")
71  .setFenceVoltage(25000)
72  .setSize(5000)
73  .setMaxCapacity(4)
74  .addFeature("electrified-fence")
75  .addFeature("motion-sensors")
76  .addFeature("reinforced-gates")
77  .addDinosaur("Blue")
78  .addDinosaur("Charlie")
79  .addDinosaur("Delta")
80  .build();
81
82console.log(raptorPaddock);
83// { name: "Raptor Paddock", type: "high-security", fenceVoltage: 25000,
84//   size: 5000, features: [...], maxCapacity: 4, currentDinosaurs: [...] }

Builder z funkcjami (bez klas)

Builder nie wymaga klas - możemy użyć prostych funkcji:

1function createDinoReport(name) {
2  const report = { name, sections: [] };
3
4  const builder = {
5    addVitals(heartRate, temperature) {
6      report.sections.push({
7        type: "vitals",
8        heartRate,
9        temperature
10      });
11      return builder;
12    },
13
14    addBehavior(description, threatLevel) {
15      report.sections.push({
16        type: "behavior",
17        description,
18        threatLevel
19      });
20      return builder;
21    },
22
23    addNote(text) {
24      report.sections.push({
25        type: "note",
26        text,
27        timestamp: new Date().toISOString()
28      });
29      return builder;
30    },
31
32    build() {
33      return {
34        ...report,
35        generatedAt: new Date().toISOString(),
36        totalSections: report.sections.length
37      };
38    }
39  };
40
41  return builder;
42}
43
44// Budowanie raportu
45const report = createDinoReport("Rexy")
46  .addVitals(65, 38.2)
47  .addBehavior("Spokojny, je regularnie", "low")
48  .addNote("Zaobserwowano nowy wzorzec polowania")
49  .addVitals(120, 39.1)
50  .addBehavior("Pobudzony po burzy", "medium")
51  .build();
52
53console.log(JSON.stringify(report, null, 2));

Podsumowanie

Dr. Wu podsumowuje: "Wzorzec Builder to jak protokół tworzenia dinozaura - krok po kroku, z walidacją na każdym etapie:"

  1. Builder - buduje złożone obiekty krok po kroku zamiast jednego ogromnego konstruktora
  2. Fluent API / Method chaining - każda metoda zwraca
    this
    , umożliwiając łańcuchowe wywołania
  3. Walidacja -
    build()
    sprawdza poprawność przed utworzeniem obiektu
  4. Czytelność - kod jest samodokumentujący się, każdy parametr ma nazwaną metodę

Wzorzec Builder jest szczególnie przydatny, gdy:

  • Obiekt ma wiele opcjonalnych parametrów
  • Chcesz walidować dane przed utworzeniem obiektu
  • Budujesz zapytania, konfiguracje lub skomplikowane struktury danych
Przejdź do CodeWorlds