Builder (Budowniczy)
Opis: Wzorzec Budowniczy pozwala na tworzenie złożonych obiektów, z wieloma rozbudowanymi konstruktorami, krok po kroku, oddzielając proces budowy od ostatecznej reprezentacji obiektu.
Przykład przed zastosowaniem wzorca Budowniczy
Poniżej przykład klasy Pizza, która pozwala tworzyć pizzę z różnymi składnikami bez zastosowania wzorca Budowniczy:
class Pizza {
private String size;
private boolean cheese;
private boolean pepperoni;
private boolean bacon;
public Pizza(String size, boolean cheese, boolean pepperoni, boolean bacon) {
this.size = size;
this.cheese = cheese;
this.pepperoni = pepperoni;
this.bacon = bacon;
}
@Override
public String toString() {
return "Pizza [size=" + size + ", cheese=" + cheese + ", pepperoni=" + pepperoni + ", bacon=" + bacon + "]";
}
}
public class Main {
public static void main(String[] args) {
Pizza pizza = new Pizza("Large", true, true, false);
System.out.println(pizza);
}
}
Problemy:
- Konstruktor staje się nieczytelny przy wielu parametrach.
- Dodanie nowego składnika wymaga modyfikacji klasy
Pizza. - Tworzenie obiektów z różnymi kombinacjami składników jest trudne do zarządzania.
Przykład po zastosowaniu wzorca Budowniczy
Klasa Pizza - obiekt budowany
class Pizza {
private String size;
private boolean cheese;
private boolean pepperoni;
private boolean bacon;
// Prywatny konstruktor
private Pizza(Builder builder) {
this.size = builder.size;
this.cheese = builder.cheese;
this.pepperoni = builder.pepperoni;
this.bacon = builder.bacon;
}
@Override
public String toString() {
return "Pizza [size=" + size + ", cheese=" + cheese + ", pepperoni=" + pepperoni + ", bacon=" + bacon + "]";
}
// Klasa wewnętrzna Builder
public static class Builder {
private String size;
private boolean cheese;
private boolean pepperoni;
private boolean bacon;
public Builder(String size) {
this.size = size;
}
public Builder addCheese() {
this.cheese = true;
return this;
}
public Builder addPepperoni() {
this.pepperoni = true;
return this;
}
public Builder addBacon() {
this.bacon = true;
return this;
}
public Pizza build() {
return new Pizza(this);
}
}
}
Użycie wzorca Budowniczy
public class Main {
public static void main(String[] args) {
Pizza pizza = new Pizza.Builder("Large")
.addCheese()
.addPepperoni()
.build();
System.out.println(pizza);
}
}
Wnioski
- Czytelność i wygoda: Dzięki wzorcowi Budowniczy tworzenie obiektów z wieloma opcjami jest bardziej przejrzyste i zrozumiałe.
- Łatwość rozbudowy: Można łatwo dodawać nowe składniki do obiektu (np. oliwki, warzywa) bez zmiany istniejącego kodu.
- Fluent Interface: Łańcuchowe wywoływanie metod (
.addCheese().addPepperoni()) poprawia ergonomię kodu. - Oddzielenie konstrukcji od reprezentacji: Możemy mieć różne konfiguracje obiektu
Pizza, korzystając z tego samego procesu budowy.
Factory (Fabryka)
Opis: Wzorzec Fabryka (Factory) służy do tworzenia obiektów bez bezpośredniego używania operatora new. Pozwala na delegowanie logiki tworzenia obiektów do specjalnych metod fabrycznych, co ułatwia zarządzanie kodem i jego rozszerzalność.
Przykład przed zastosowaniem wzorca Fabryka
W tym przypadku każda instancja klasy Animal jest tworzona bezpośrednio przy użyciu new, co sprawia, że kod jest mniej elastyczny.
// Klasa bazowa
abstract class Animal {
abstract void makeSound();
}
// Konkretne klasy
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Meow!");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog();
Animal cat = new Cat();
dog.makeSound();
cat.makeSound();
}
}
Problemy:
- Użycie
neww kodzie głównym (Main) oznacza, że jeśli dodamy nową klasę np.Bird, trzeba będzie edytować kod w wielu miejscach. - Ograniczona skalowalność — trudno zarządzać wieloma typami obiektów.
Przykład po zastosowaniu wzorca Fabryka
Dzięki fabryce, kod staje się bardziej elastyczny, a logika tworzenia obiektów jest oddzielona od kodu aplikacji.
// Klasa bazowa
abstract class Animal {
abstract void makeSound();
}
// Konkretne klasy
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Meow!");
}
}
// Klasa Fabryka
class AnimalFactory {
public static Animal createAnimal(String type) {
if (type.equalsIgnoreCase("dog")) {
return new Dog();
} else if (type.equalsIgnoreCase("cat")) {
return new Cat();
}
throw new IllegalArgumentException("Unknown animal type: " + type);
}
}
// Kod główny
public class Main {
public static void main(String[] args) {
Animal dog = AnimalFactory.createAnimal("dog");
Animal cat = AnimalFactory.createAnimal("cat");
dog.makeSound();
cat.makeSound();
}
}
Wnioski:
- Enkapsulacja procesu tworzenia obiektów – Obiekty są tworzone przez fabrykę, a nie bezpośrednio przez
new, co ułatwia zarządzanie nimi. - Łatwiejsza rozbudowa – Jeśli dodamy nową klasę (
Bird), wystarczy dodać ją w fabryce, zamiast modyfikować kod w wielu miejscach. - Większa elastyczność – Możemy dynamicznie tworzyć różne obiekty na podstawie warunków wejściowych (
String type). - Wzorzec Fabryka jest szczególnie przydatny, gdy chcemy oddzielić logikę tworzenia obiektów od ich użycia i zwiększyć modularność kodu.
Strategy (Strategia)
Opis: Wzorzec Strategia (Strategy) pozwala na dynamiczną zmianę algorytmów w trakcie działania programu. Zamiast stosować instrukcje warunkowe (if-else lub switch), różne warianty algorytmu są implementowane jako osobne klasy i mogą być łatwo zamieniane w czasie działania programu.
Przykład przed zastosowaniem wzorca Strategia
W tym przypadku mamy klasę PaymentProcessor, która obsługuje różne metody płatności za pomocą instrukcji if-else.
// Klasa przetwarzająca płatności
class PaymentProcessor {
public void processPayment(String paymentType, double amount) {
if (paymentType.equalsIgnoreCase("creditCard")) {
System.out.println("Processing credit card payment: $" + amount);
} else if (paymentType.equalsIgnoreCase("paypal")) {
System.out.println("Processing PayPal payment: $" + amount);
} else {
System.out.println("Invalid payment method.");
}
}
}
// Kod główny
public class Main {
public static void main(String[] args) {
PaymentProcessor paymentProcessor = new PaymentProcessor();
paymentProcessor.processPayment("creditCard", 100.0);
paymentProcessor.processPayment("paypal", 50.0);
}
}
Problemy:
- Jeśli dodamy nową metodę płatności (np. Bitcoin), musimy modyfikować
PaymentProcessor, łamiąc zasadę Open/Closed (klasa powinna być otwarta na rozszerzenia, ale zamknięta na modyfikacje). - Trudna skalowalność – kod staje się nieczytelny, gdy pojawia się więcej warunków.
Przykład po zastosowaniu wzorca Strategia
Każda metoda płatności zostaje wydzielona jako osobna klasa implementująca interfejs PaymentStrategy.
// Interfejs strategii płatności
interface PaymentStrategy {
void pay(double amount);
}
// Implementacje różnych metod płatności
class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Processing credit card payment: $" + amount);
}
}
class PayPalPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Processing PayPal payment: $" + amount);
}
}
// Kontekst – klasa wykorzystująca strategię
class PaymentProcessor {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void processPayment(double amount) {
if (paymentStrategy == null) {
System.out.println("No payment strategy set.");
return;
}
paymentStrategy.pay(amount);
}
}
// Kod główny
public class Main {
public static void main(String[] args) {
PaymentProcessor paymentProcessor = new PaymentProcessor();
// Płatność kartą kredytową
paymentProcessor.setPaymentStrategy(new CreditCardPayment());
paymentProcessor.processPayment(100.0);
// Płatność przez PayPal
paymentProcessor.setPaymentStrategy(new PayPalPayment());
paymentProcessor.processPayment(50.0);
}
}
Wnioski:
- Zasada Open/Closed – Możemy łatwo dodać nowe metody płatności (np.
BitcoinPayment), bez modyfikowania istniejącego kodu. - Lepsza organizacja kodu – Każda metoda płatności jest wydzielona do osobnej klasy, co ułatwia zarządzanie kodem.
- Dynamiczna zmiana strategii – Możemy zmieniać sposób płatności w trakcie działania programu bez konieczności modyfikowania kodu
PaymentProcessor. - Wzorzec Strategia sprawdza się w sytuacjach, gdy mamy wiele wariantów algorytmu i chcemy umożliwić ich dynamiczne wybieranie.