Dlaczego wzorce projektowe są ważne w 2015 roku
W erze rosnącej złożoności aplikacji Java EE i frameworków takich jak Spring 4.0, wzorce projektowe stają się kluczowe dla tworzenia maintainable kodu. Singleton pomaga zarządzać współdzielonymi zasobami jak connection pools czy cache, podczas gdy Factory upraszcza tworzenie obiektów w złożonych hierarchiach klas.
Co się nauczysz:
- Implementacji thread-safe wzorca Singleton w Javie 8
- Kiedy używać Factory Method vs Abstract Factory
- Pułapek i problemów każdego wzorca w produkcji
- Integracji wzorców z frameworkami jak Spring
- Testowania klas używających te wzorce
Wzorzec Singleton – jedna instancja w całej aplikacji
Singleton to wzorzec kreacyjny zapewniający, że klasa ma tylko jedną instancję i globalny punkt dostępu do niej. W aplikacjach Java często używamy go dla logger-ów, connection pool-ów czy konfiguracji.
Klasyczna implementacja Singleton
public class DatabaseConnection { private static DatabaseConnection instance; private Connection connection; // Prywatny konstruktor zapobiega tworzeniu instancji z zewnątrz private DatabaseConnection() { try { this.connection = DriverManager.getConnection( "jdbc:mysql://localhost:3306/mydb", "user", "password" ); } catch (SQLException e) { throw new RuntimeException("Failed to create database connection", e); } } // Metoda dostarczająca globalny punkt dostępu public static DatabaseConnection getInstance() { if (instance == null) { instance = new DatabaseConnection(); } return instance; } public Connection getConnection() { return connection; } }
Thread-safe implementacje Singleton
**Synchronized method (wolna ale bezpieczna):**
public class ThreadSafeSingleton { private static ThreadSafeSingleton instance; private ThreadSafeSingleton() {} // Synchronizacja całej metody - wolne! public static synchronized ThreadSafeSingleton getInstance() { if (instance == null) { instance = new ThreadSafeSingleton(); } return instance; } }
**Double-checked locking (optymalna wydajność):**
public class OptimizedSingleton { private static volatile OptimizedSingleton instance; private OptimizedSingleton() {} public static OptimizedSingleton getInstance() { // Pierwszy check bez synchronizacji if (instance == null) { synchronized (OptimizedSingleton.class) { // Drugi check wewnątrz synchronized block if (instance == null) { instance = new OptimizedSingleton(); } } } return instance; } }
**Enum Singleton (rekomendowane przez Joshua Bloch):**
public enum ConfigurationManager { INSTANCE; private Properties config; ConfigurationManager() { config = new Properties(); try { config.load(getClass().getResourceAsStream("/config.properties")); } catch (IOException e) { throw new RuntimeException("Cannot load configuration", e); } } public String getProperty(String key) { return config.getProperty(key); } } // Użycie: String dbUrl = ConfigurationManager.INSTANCE.getProperty("db.url");
Wzorzec Factory – elastyczne tworzenie obiektów
Factory to wzorzec kreacyjny który enkapsuluje logikę tworzenia obiektów. Zamiast używać new bezpośrednio, delegujemy tworzenie do dedykowanej klasy lub metody.
Simple Factory Pattern
// Interfejs produktu public interface DatabaseDriver { Connection createConnection(String url, String user, String password); } // Konkretne implementacje public class MySQLDriver implements DatabaseDriver { @Override public Connection createConnection(String url, String user, String password) { // MySQL-specific connection logic return DriverManager.getConnection("jdbc:mysql:" + url, user, password); } } public class PostgreSQLDriver implements DatabaseDriver { @Override public Connection createConnection(String url, String user, String password) { // PostgreSQL-specific connection logic return DriverManager.getConnection("jdbc:postgresql:" + url, user, password); } } // Simple Factory public class DatabaseDriverFactory { public static DatabaseDriver createDriver(String databaseType) { switch (databaseType.toLowerCase()) { case "mysql": return new MySQLDriver(); case "postgresql": return new PostgreSQLDriver(); default: throw new IllegalArgumentException("Unsupported database type: " + databaseType); } } }
Factory Method Pattern
Factory Method przenosi logikę tworzenia do podklas, pozwalając na większą elastyczność:
// Abstrakcyjna klasa Creator public abstract class NotificationSender { // Factory Method - do implementacji w podklasach protected abstract Notification createNotification(); // Metoda używająca Factory Method public void sendNotification(String message) { Notification notification = createNotification(); notification.setMessage(message); notification.send(); } } // Konkretni Creator-zy public class EmailNotificationSender extends NotificationSender { @Override protected Notification createNotification() { return new EmailNotification(); } } public class SMSNotificationSender extends NotificationSender { @Override protected Notification createNotification() { return new SMSNotification(); } } // Użycie NotificationSender emailSender = new EmailNotificationSender(); emailSender.sendNotification("Welcome to our service!");
Abstract Factory Pattern
Gdy potrzebujemy tworzyć rodziny powiązanych obiektów:
// Abstract Factory public interface UIComponentFactory { Button createButton(); TextField createTextField(); Checkbox createCheckbox(); } // Concrete Factory dla Windows public class WindowsUIFactory implements UIComponentFactory { @Override public Button createButton() { return new WindowsButton(); } @Override public TextField createTextField() { return new WindowsTextField(); } @Override public Checkbox createCheckbox() { return new WindowsCheckbox(); } } // Concrete Factory dla MacOS public class MacOSUIFactory implements UIComponentFactory { @Override public Button createButton() { return new MacOSButton(); } @Override public TextField createTextField() { return new MacOSTextField(); } @Override public Checkbox createCheckbox() { return new MacOSCheckbox(); } }
Pułapki i problemy w praktyce
Problemy z Singleton
**1. Testowanie – globalne stany utrudniają unit testy:**
// Problematyczne testowanie @Test public void testUserService() { // Test może być zależny od stanu Singleton z poprzedniego testu UserService service = new UserService(); // DatabaseConnection.getInstance() może mieć "brudne" dane service.createUser("John"); // Test może się nie powieść z powodu stanu z poprzedniego testu }
**2. Naruszenie Single Responsibility Principle – Singleton kontroluje zarówno swoją unikalność jak i logikę biznesową.**
**3. Problemy z Dependency Injection – trudno mockować w testach.**
Problemy z Factory
**1. Over-engineering – nie każde new potrzebuje Factory:**
**2. Dodatkowa złożoność – więcej klas i interfejsów do utrzymania.**
Integracja ze Spring Framework 4.0
Spring eliminuje wiele problemów tradycyjnych wzorców:
// Singleton w Spring - automatyczny @Component @Scope("singleton") // domyślnie public class DatabaseService { // Spring gwarantuje jedną instancję } // Factory w Spring @Configuration public class DatabaseConfig { @Bean public DatabaseDriver databaseDriver(@Value("${db.type}") String dbType) { // Spring jako Factory return DatabaseDriverFactory.createDriver(dbType); } }
Testowanie wzorców projektowych
**Testowanie Factory:**
@Test public void shouldCreateCorrectDriverType() { // Given String databaseType = "mysql"; // When DatabaseDriver driver = DatabaseDriverFactory.createDriver(databaseType); // Then assertThat(driver).isInstanceOf(MySQLDriver.class); } @Test(expected = IllegalArgumentException.class) public void shouldThrowExceptionForUnsupportedDatabase() { DatabaseDriverFactory.createDriver("oracle"); }
**Testowanie z Singleton (problematyczne):**
// Lepsze podejście - Dependency Injection zamiast Singleton public class UserService { private final DatabaseConnection dbConnection; // Constructor injection zamiast getInstance() public UserService(DatabaseConnection dbConnection) { this.dbConnection = dbConnection; } } @Test public void shouldCreateUser() { // Given DatabaseConnection mockConnection = mock(DatabaseConnection.class); UserService service = new UserService(mockConnection); // When & Then - teraz możemy mockować! // ... }
Używaj Singleton dla zasobów które muszą być współdzielone (cache, connection pool, logger) i są kosztowne w tworzeniu. Unikaj gdy potrzebujesz testów jednostkowych, dependency injection lub gdy klasa ma więcej niż jedną odpowiedzialność.
Tak – enum Singleton jest thread-safe z natury, odporny na reflection attacks i serialization issues. Joshua Bloch w „Effective Java” nazywa go najlepszym sposobem implementacji Singleton w Javie.
Factory Method gdy tworzysz jeden typ produktu z różnymi implementacjami. Abstract Factory gdy potrzebujesz tworzyć rodziny powiązanych obiektów (np. cały UI theme – przyciski, pola, ikonki w jednym stylu).
Singleton może nieznacznie przyspieszyć (jedna instancja), ale double-checked locking dodaje overhead. Factory dodaje warstwę abstrakcji – koszt minimalny vs korzyści z maintainability. W praktyce różnica jest pomijalalna.
Singleton to jedna instancja per JVM, nie per klaster! W środowisku rozproszonym każdy serwer ma swoją instancję. Do współdzielenia stanu między serwerami użyj zewnętrznych systemów jak Redis czy Hazelcast.
Tak – reflection może wywołać prywatny konstruktor i stworzyć dodatkowe instancje. Enum Singleton jest odporny na to, klasyczne implementacje można zabezpieczyć rzucając wyjątek w konstruktorze jeśli instancja już istnieje.
Factory skupia się na TYPIE tworzonego obiektu (MySQL vs PostgreSQL driver). Builder skupia się na KONFIGURACJI tego samego typu (User z różnymi polami). Factory zwraca gotowy obiekt, Builder pozwala na step-by-step construction.
Przydatne zasoby:
- Java 8 API Documentation
- Spring Framework 4.0 Reference
- „Effective Java” by Joshua Bloch – rozdział o Singleton
- „Head First Design Patterns” – praktyczne implementacje
- JUnit 4 Documentation
🚀 Zadanie dla Ciebie
Zaimplementuj system logowania:
1. Stwórz thread-safe Logger Singleton z enum
2. Użyj Factory do tworzenia różnych formatów logów (JSON, XML, plain text)
3. Napisz testy jednostkowe używając Mockito
4. Porównaj wydajność różnych implementacji Singleton (synchronized vs double-checked locking)
Bonus: Zintegruj rozwiązanie ze Spring 4.0 i zobacz jak framework zarządza lifecycle obiektów.
Czy zastanawiałeś się kiedyś, dlaczego Spring tak bardzo ułatwia zarządzanie obiektami? W następnym artykule zbadamy jak Dependency Injection eliminuje problemy tradycyjnych wzorców kreacyjnych!