Dlaczego Prototype Pattern to ważne
Wyobraź sobie, że masz obiekt, którego utworzenie zajmuje dużo czasu – na przykład ładuje dane z bazy, wykonuje skomplikowane obliczenia lub łączy się z zewnętrznym API. Co jeśli potrzebujesz wielu podobnych obiektów? Zamiast tworzyć każdy od zera, możesz po prostu skopiować już istniejący!
Co się nauczysz:
- Czym jest Prototype Pattern i kiedy go używać
- Jak zaimplementować interfejs Cloneable w Javie
- Różnica między płytkim a głębokim kopiowaniem
- Typowe błędy przy klonowaniu obiektów
- Praktyczne przykłady użycia wzorca
Wymagania wstępne:
- Podstawowa znajomość programowania obiektowego w Javie
- Znajomość koncepcji klas i obiektów
- Rozumienie pojęcia interfejsów
Czym jest Prototype Pattern?
Prototype Pattern to wzorzec projektowy, który pozwala na tworzenie nowych obiektów poprzez kopiowanie istniejących. Zamiast używać konstruktora, „klonujemy” obiekt który już mamy.
Kiedy używać Prototype Pattern?
- Kosztowne tworzenie obiektów: Gdy inicjalizacja obiektu jest czasochłonna lub zasobożerna
- Złożone konfiguracje: Gdy obiekt ma wiele parametrów i stanów
- Podobne obiekty: Gdy potrzebujesz wielu obiektów o podobnej strukturze
- Unikanie dziedziczenia: Gdy chcesz uniknąć tworzenia wielu podklas
Implementacja w Javie – interfejs Cloneable
W Javie Prototype Pattern implementuje się głównie przez interfejs Cloneable i metodę clone().
Podstawowa implementacja
public class Student implements Cloneable { private String name; private int age; private String major; public Student(String name, int age, String major) { this.name = name; this.age = age; this.major = major; } // Implementacja clone() - płytkie kopiowanie @Override public Student clone() { try { return (Student) super.clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException("Klonowanie nie jest obsługiwane", e); } } // Gettery i settery public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getMajor() { return major; } public void setMajor(String major) { this.major = major; } @Override public String toString() { return "Student{name='" + name + "', age=" + age + ", major='" + major + "'}"; } }
Użycie prototypu
public class PrototypeExample { public static void main(String[] args) { // Tworzymy prototyp Student originalStudent = new Student("Jan Kowalski", 20, "Informatyka"); // Klonujemy prototyp Student clonedStudent = originalStudent.clone(); // Modyfikujemy sklonowany obiekt clonedStudent.setName("Anna Nowak"); clonedStudent.setAge(22); System.out.println("Oryginał: " + originalStudent); System.out.println("Klon: " + clonedStudent); // Sprawdzamy czy to różne obiekty System.out.println("Czy to ten sam obiekt? " + (originalStudent == clonedStudent)); } }
Płytkie vs Głębokie kopiowanie
Płytkie kopiowanie (Shallow Copy)
Domyślna implementacja clone() wykonuje płytkie kopiowanie – kopiuje wartości pól, ale nie kopiuje obiektów na które te pola wskazują.
public class Course { private String name; private int credits; public Course(String name, int credits) { this.name = name; this.credits = credits; } // Gettery i settery public String getName() { return name; } public void setName(String name) { this.name = name; } public int getCredits() { return credits; } public void setCredits(int credits) { this.credits = credits; } @Override public String toString() { return "Course{name='" + name + "', credits=" + credits + "}"; } }
public class StudentWithCourse implements Cloneable { private String name; private Course course; // Referencja do innego obiektu public StudentWithCourse(String name, Course course) { this.name = name; this.course = course; } @Override public StudentWithCourse clone() { try { // Płytkie kopiowanie - course będzie współdzielone! return (StudentWithCourse) super.clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException("Klonowanie nie jest obsługiwane", e); } } public String getName() { return name; } public void setName(String name) { this.name = name; } public Course getCourse() { return course; } public void setCourse(Course course) { this.course = course; } @Override public String toString() { return "StudentWithCourse{name='" + name + "', course=" + course + "}"; } }
Głębokie kopiowanie (Deep Copy)
Aby uniknąć współdzielenia obiektów, musimy zaimplementować głębokie kopiowanie:
public class StudentWithDeepCopy implements Cloneable { private String name; private Course course; public StudentWithDeepCopy(String name, Course course) { this.name = name; this.course = course; } @Override public StudentWithDeepCopy clone() { try { StudentWithDeepCopy cloned = (StudentWithDeepCopy) super.clone(); // Głębokie kopiowanie - tworzymy nowy obiekt Course if (this.course != null) { cloned.course = new Course(this.course.getName(), this.course.getCredits()); } return cloned; } catch (CloneNotSupportedException e) { throw new RuntimeException("Klonowanie nie jest obsługiwane", e); } } // Gettery i settery... }
Prototype Registry – zarządzanie prototypami
W praktyce często używa się rejestru prototypów, który przechowuje gotowe wzorce obiektów:
import java.util.HashMap; import java.util.Map; public class StudentPrototypeRegistry { private Mapprototypes = new HashMap<>(); public StudentPrototypeRegistry() { // Inicjalizujemy podstawowe prototypy prototypes.put("informatyka", new Student("Szablon", 20, "Informatyka")); prototypes.put("matematyka", new Student("Szablon", 21, "Matematyka")); prototypes.put("fizyka", new Student("Szablon", 19, "Fizyka")); } public Student createStudent(String type, String name, int age) { Student prototype = prototypes.get(type); if (prototype == null) { throw new IllegalArgumentException("Nieznany typ studenta: " + type); } Student cloned = prototype.clone(); cloned.setName(name); cloned.setAge(age); return cloned; } public void addPrototype(String key, Student prototype) { prototypes.put(key, prototype); } }
Użycie rejestru prototypów
public class RegistryExample { public static void main(String[] args) { StudentPrototypeRegistry registry = new StudentPrototypeRegistry(); // Szybko tworzymy studentów różnych kierunków Student student1 = registry.createStudent("informatyka", "Jan Kowalski", 22); Student student2 = registry.createStudent("matematyka", "Anna Nowak", 20); Student student3 = registry.createStudent("fizyka", "Piotr Wiśniewski", 21); System.out.println(student1); System.out.println(student2); System.out.println(student3); } }
Typowe błędy i pułapki
Najczęstsze problemy:
- Brak implementacji Cloneable: Wyrzuci CloneNotSupportedException
- Płytkie kopiowanie gdy potrzebne głębokie: Współdzielenie referencji
- Zapominanie o final fields: Nie można ich zmienić po klonowaniu
- Problemy z dziedziczeniem: Klasy potomne muszą też implementować clone()
Alternatywy dla Cloneable
Copy Constructor
public class Student { private String name; private int age; private String major; // Konstruktor normalny public Student(String name, int age, String major) { this.name = name; this.age = age; this.major = major; } // Copy constructor - często lepszy niż clone() public Student(Student other) { this.name = other.name; this.age = other.age; this.major = other.major; } // Reszta klasy... }
Używaj Prototype gdy masz już skonfigurowany obiekt i chcesz go skopiować. Factory Pattern używaj gdy chcesz tworzyć nowe obiekty z różnymi konfiguracjami od zera.
Nie, standardowa implementacja clone() nie jest thread-safe. Jeśli potrzebujesz thread-safety, musisz dodać synchronizację lub użyć concurrent collections.
Marker interface to interfejs bez metod, który służy tylko do oznaczenia klasy. Cloneable informuje JVM, że obiekt może być klonowany.
To mechanizm bezpieczeństwa. Klasa musi jawnie zaimplementować Cloneable, żeby pokazać że programista świadomie włączył możliwość klonowania.
Tak, ale musisz pamiętać że final fields będą miały te same wartości co w oryginale. Nie można ich zmienić po klonowaniu.
Tak! Przykłady to Object.clone(), ArrayList.clone(), HashMap.clone(), StringBuilder.clone() i wiele innych klas z biblioteki standardowej.
Ogólnie copy constructor jest lepszy – jest bardziej czytelny, type-safe i nie wymaga rzucania wyjątków. Joshua Bloch w „Effective Java” zaleca unikanie clone().
🚀 Zadanie dla Ciebie
Stwórz klasę Document z polami title, content i author. Zaimplementuj Prototype Pattern z głębokim kopiowaniem. Dodaj też registry prototypów dla różnych typów dokumentów (email, raport, memo). Przetestuj czy klonowanie działa poprawnie i czy zmiany w jednym dokumencie nie wpływają na drugi.