← Назад к вопросам

Где использовал Singleton?

1.6 Junior🔥 231 комментариев
#SOLID и паттерны проектирования

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Использование Singleton паттерна

Singleton — это паттерн, который гарантирует, что класс имеет только один экземпляр. Расскажу, где я его использовал и какие проблемы возникают.

1. Singleton в Logging

Синглтоны часто используются для логгирования:

public class Logger {
    private static Logger instance;  // Единственный экземпляр
    private PrintWriter writer;
    
    private Logger() {  // Приватный конструктор
        try {
            writer = new PrintWriter("app.log", StandardCharsets.UTF_8);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
    
    public static synchronized Logger getInstance() {
        if (instance == null) {
            instance = new Logger();
        }
        return instance;
    }
    
    public void log(String message) {
        writer.println("[" + LocalDateTime.now() + "] " + message);
        writer.flush();
    }
}

// Использование
Logger logger = Logger.getInstance();
logger.log("Application started");

Проблемы:

  • Синхронизация может быть узким местом
  • Сложно тестировать (нельзя создать mock)
  • Нарушает Single Responsibility Principle

2. Singleton в Configuration

Для управления конфигурацией приложения:

public class AppConfig {
    private static AppConfig instance;
    private Properties properties;
    private String databaseUrl;
    private String apiKey;
    
    private AppConfig() {
        loadConfiguration();
    }
    
    public static AppConfig getInstance() {
        if (instance == null) {
            synchronized (AppConfig.class) {
                if (instance == null) {  // Double-checked locking
                    instance = new AppConfig();
                }
            }
        }
        return instance;
    }
    
    private void loadConfiguration() {
        properties = new Properties();
        try {
            properties.load(
                new FileInputStream("application.properties")
            );
            databaseUrl = properties.getProperty("db.url");
            apiKey = properties.getProperty("api.key");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    
    public String getDatabaseUrl() {
        return databaseUrl;
    }
    
    public String getApiKey() {
        return apiKey;
    }
}

Проблемы:

  • Double-checked locking имеет свои нюансы с памятью
  • Сложно переконфигурировать при runtime
  • В Spring это работает лучше через @Configuration

3. Thread-safe Singleton (Enum)

Лучший способ реализовать Singleton в Java — через enum:

public enum DatabaseConnection {
    INSTANCE;  // Единственный экземпляр, создаётся автоматически
    
    private final Connection connection;
    
    DatabaseConnection() {
        try {
            this.connection = DriverManager.getConnection(
                "jdbc:postgresql://localhost/mydb", 
                "user", 
                "password"
            );
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
    
    public Connection getConnection() {
        return connection;
    }
}

// Использование
Connection conn = DatabaseConnection.INSTANCE.getConnection();

Преимущества:

  • Автоматически thread-safe
  • Сериализация работает корректно
  • Невозможно создать через reflection
  • Простая и читаемая реализация

4. Singleton через Eager Initialization

Загружается при загрузке класса:

public class RuntimeMetrics {
    private static final RuntimeMetrics INSTANCE = new RuntimeMetrics();
    private final long startTime;
    private long requestCount = 0;
    
    private RuntimeMetrics() {
        this.startTime = System.currentTimeMillis();
    }
    
    public static RuntimeMetrics getInstance() {
        return INSTANCE;
    }
    
    public void incrementRequestCount() {
        requestCount++;
    }
    
    public long getUptime() {
        return System.currentTimeMillis() - startTime;
    }
    
    public long getRequestCount() {
        return requestCount;
    }
}

Преимущества:

  • Простая реализация
  • Thread-safe по умолчанию
  • Инициализируется только один раз

Недостатки:

  • Всегда создаётся, даже если не используется
  • Может замедлить загрузку приложения

5. Singleton через Lazy Initialization (Bill Pugh)

Одна из лучших реализаций для ленивой загрузки:

public class ConnectionPool {
    private static class Holder {
        static final ConnectionPool INSTANCE = new ConnectionPool();
    }
    
    private final List<Connection> pool;
    
    private ConnectionPool() {
        pool = new ArrayList<>();
        // Инициализация пула
        for (int i = 0; i < 10; i++) {
            try {
                pool.add(DriverManager.getConnection("jdbc:postgresql://localhost/db"));
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }
    
    public static ConnectionPool getInstance() {
        return Holder.INSTANCE;
    }
    
    public Connection getConnection() {
        if (pool.isEmpty()) {
            throw new RuntimeException("No available connections");
        }
        return pool.remove(0);
    }
    
    public void releaseConnection(Connection conn) {
        pool.add(conn);
    }
}

Преимущества:

  • Thread-safe
  • Ленивая инициализация
  • Простой и элегантный код

6. Singleton в Production: Spring Beans

В современных приложениях используется Spring:

@Configuration
public class AppConfiguration {
    @Bean
    public DatabaseService databaseService() {  // Это фактически Singleton
        return new DatabaseService(databaseUrl(), dbPassword());
    }
    
    @Bean
    public String databaseUrl() {
        return environment.getProperty("spring.datasource.url");
    }
    
    @Bean
    public String dbPassword() {
        return environment.getProperty("spring.datasource.password");
    }
}

@Service
public class OrderService {
    private final DatabaseService dbService;  // Инъекция Singleton
    
    public OrderService(DatabaseService dbService) {
        this.dbService = dbService;  // Один и тот же экземпляр
    }
}

Spring Beans по умолчанию singleton, но это управляется контейнером, что лучше:

  • Можно переопределить scope (@Scope)
  • Легко тестировать (мокировать)
  • Управление жизненным циклом (init/destroy methods)

7. Проблемы Singleton паттерна

Проблема 1: Тестирование

// Сложно тестировать
public class UserService {
    public void createUser(User user) {
        User saved = user;
        saved.setId(IdGenerator.getInstance().generateId());  // Зависимость от Singleton
        userRepository.save(saved);
    }
}

// Нельзя мокировать IdGenerator без рефлексии
@Test
public void testCreateUser() {
    // Как создать mock IdGenerator?
    UserService service = new UserService();  // Привязан к реальному Singleton
}

Решение: Dependency Injection

public class UserService {
    private final IdGenerator idGenerator;
    
    public UserService(IdGenerator idGenerator) {  // Инъекция зависимости
        this.idGenerator = idGenerator;
    }
    
    public void createUser(User user) {
        user.setId(idGenerator.generateId());
        userRepository.save(user);
    }
}

@Test
public void testCreateUser() {
    IdGenerator mockGenerator = mock(IdGenerator.class);
    when(mockGenerator.generateId()).thenReturn(123L);
    UserService service = new UserService(mockGenerator);  // Легко мокировать
    
    User user = new User("John");
    service.createUser(user);
    
    assertEquals(123L, user.getId());
}

8. Проблема: Многопоточность

// ОПАСНО - не thread-safe
public class Counter {
    private static Counter instance;
    private int count = 0;
    
    private Counter() {}
    
    public static Counter getInstance() {
        if (instance == null) {  // Race condition!
            instance = new Counter();
        }
        return instance;
    }
    
    public void increment() {
        count++;  // Race condition!
    }
    
    public int getCount() {
        return count;  // Может быть неправильное значение
    }
}

9. Когда использовать Singleton

Используй Singleton когда:

  • Нужна одна точка доступа к ресурсу (connection pool, logger)
  • Инициализация дорогая (создание базы данных)
  • Состояние нужно делиться между всеми компонентами

Избегай Singleton когда:

  • Нужна гибкость в конфигурации
  • Пишешь тестируемый код
  • Можно использовать Dependency Injection
  • Нужна возможность создать несколько экземпляров

10. Best Practices

  • Используй enum для простых Singleton
  • Используй Spring @Bean в production коде
  • Избегай статических методов в пользу DI
  • Тестируй Singleton отдельно или используй DI
  • Документируй причину использования Singleton

В целом: Singleton полезен, но в современном Java часто лучше использовать Spring Dependency Injection или явно передавать зависимости через конструктор. Это делает код более гибким, тестируемым и поддерживаемым.

Где использовал Singleton? | PrepBro