Wzorce projektowe w Javie – Singleton i Factory

TL;DR: Singleton zapewnia jedną instancję klasy w aplikacji, Factory tworzy obiekty bez określania ich konkretnej klasy. Oba wzorce są fundamentalne w Javie, ale wymagają ostrożności w implementacji – Singleton może być problematyczny w wielowątkowym środowisku, a Factory dodaje abstrakcję kosztem złożoności.

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
Wymagania wstępne: Znajomość podstaw OOP w Javie, wielowątkowości (synchronized, volatile), podstaw JUnit. Doświadczenie z projektami większymi niż „Hello World”.

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;
    }
}
Uwaga: Ta implementacja NIE jest thread-safe! W aplikacji wielowątkowej może powstać więcej niż jedna instancja.

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;
    }
}
Pro tip: Słowo kluczowe volatile jest kluczowe w double-checked locking – zapewnia że zmiany instance są widoczne we wszystkich wątkach.

**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();
    }
}
Abstract Factory używamy gdy musimy zapewnić spójność w rodzinie tworzonych obiektów – wszystkie komponenty UI muszą być w tym samym stylu (Windows lub MacOS).

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:**

Pułapka: Początkujący programiści często tworzą Factory dla każdej klasy, nawet prostych DTO czy Value Objects. Factory ma sens gdy logika tworzenia jest złożona lub może się zmieniać.

**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);
    }
}
Pro tip: W aplikacjach Spring rzadko implementujemy Singleton ręcznie – framework zarządza lifecycle obiektów za nas.

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ć!
    // ...
}

Kiedy używać Singleton a kiedy unikać?

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ść.

Czy enum Singleton jest naprawdę lepszy?

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 vs Abstract Factory – kiedy co?

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).

Czy wzorce projektowe spowalniają aplikację?

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.

Jak Singleton zachowuje się w klastrze aplikacji?

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.

Czy można łamać Singleton przez reflection?

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 vs Builder – jaka różnica?

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:

🚀 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!

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry