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.
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ść!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 }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", ... }]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 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));Dr. Wu podsumowuje: "Wzorzec Builder to jak protokół tworzenia dinozaura - krok po kroku, z walidacją na każdym etapie:"
this, umożliwiając łańcuchowe wywołaniabuild() sprawdza poprawność przed utworzeniem obiektuWzorzec Builder jest szczególnie przydatny, gdy: