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

Singleton: паттерн или антипаттерн

2.0 Middle🔥 181 комментариев
#SOLID и паттерны проектирования

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

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

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

Singleton: паттерн или антипаттерн?

Ответ: Оба верны. Singleton — это валидный паттерн проектирования для специфических случаев, но часто используется неправильно и становится антипаттерном. Ключ в том, когда его применять.

Когда Singleton — паттерн (уместен)

// 1. Конфигурация приложения - должна быть одна
public class ApplicationConfig {
    private static ApplicationConfig instance;
    private Properties properties;
    
    private ApplicationConfig() {
        properties = new Properties();
        // загрузка конфигурации
    }
    
    public static synchronized ApplicationConfig getInstance() {
        if (instance == null) {
            instance = new ApplicationConfig();
        }
        return instance;
    }
    
    public String getProperty(String key) {
        return properties.getProperty(key);
    }
}
// Использование: одна конфигурация на всё приложение
// 2. Логирование - обычно одна система логирования
public class Logger {
    private static final Logger instance = new Logger();
    private PrintWriter writer;
    
    private Logger() {
        try {
            writer = new PrintWriter(new FileWriter("app.log"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public static Logger getInstance() {
        return instance;
    }
    
    public void log(String message) {
        writer.println(message);
    }
}
// Единая точка логирования
// 3. Пул подключений к БД - обычно один пул
public class DatabaseConnectionPool {
    private static final DatabaseConnectionPool instance = new DatabaseConnectionPool();
    private final List<Connection> availableConnections = new ArrayList<>();
    private final List<Connection> usedConnections = new ArrayList<>();
    
    private DatabaseConnectionPool() {
        // инициализация пула подключений
    }
    
    public static DatabaseConnectionPool getInstance() {
        return instance;
    }
    
    public synchronized Connection getConnection() {
        // логика получения подключения
        return null;
    }
}

Когда Singleton — антипаттерн (не уместен)

1. Нарушает принцип Single Responsibility:

// Плохо: Singleton отвечает за создание И бизнес-логику
public class UserService {
    private static final UserService instance = new UserService();
    private UserRepository repository;
    
    public static UserService getInstance() {
        return instance;
    }
    
    public User findUser(int id) {
        return repository.find(id);
    }
}

// Хорошо: Dependency Injection
public class UserService {
    private final UserRepository repository;
    
    public UserService(UserRepository repository) {
        this.repository = repository;
    }
    
    public User findUser(int id) {
        return repository.find(id);
    }
}
// Используется в контексте DI контейнера (Spring, Guice)

2. Затрудняет тестирование:

// Сложно тестировать
public class PaymentProcessor {
    private static final PaymentProcessor instance = new PaymentProcessor();
    private PaymentGateway gateway = new RealPaymentGateway();  // real!
    
    public static PaymentProcessor getInstance() {
        return instance;
    }
    
    public boolean processPayment(double amount) {
        return gateway.charge(amount);
    }
}

// Тест не может использовать mock
@Test
public void testPayment() {
    PaymentProcessor processor = PaymentProcessor.getInstance();
    // gateway это реальный объект, невозможно захватить
    assertThat(processor.processPayment(100)).isTrue();
}

// Правильный подход
public class PaymentProcessor {
    private final PaymentGateway gateway;
    
    public PaymentProcessor(PaymentGateway gateway) {
        this.gateway = gateway;
    }
    
    public boolean processPayment(double amount) {
        return gateway.charge(amount);
    }
}

@Test
public void testPayment() {
    PaymentGateway mockGateway = mock(PaymentGateway.class);
    when(mockGateway.charge(100)).thenReturn(true);
    
    PaymentProcessor processor = new PaymentProcessor(mockGateway);
    assertThat(processor.processPayment(100)).isTrue();
}

3. Скрывает зависимости:

// Плохо: зависимость скрыта в методе
public class OrderService {
    public void createOrder(Order order) {
        // откуда берется DatabaseConnectionPool? Не понятно!
        Connection conn = DatabaseConnectionPool.getInstance().getConnection();
        // сохраняем заказ
    }
}

// Хорошо: зависимость явна
public class OrderService {
    private final DatabaseConnectionPool pool;
    
    public OrderService(DatabaseConnectionPool pool) {
        this.pool = pool;
    }
    
    public void createOrder(Order order) {
        Connection conn = pool.getConnection();
        // сохраняем заказ
    }
}

Правильные реализации Singleton

Eager initialization (потокобезопасно изначально):

public class Singleton {
    private static final Singleton instance = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return instance;
    }
}

Lazy initialization с double-checked locking:

public class Singleton {
    private static volatile Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Bill Pugh Singleton (лучший подход):

public class Singleton {
    private Singleton() {}
    
    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }
    
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}
// Потокобезопасен благодаря classloader
// Ленива инициализация

Enum Singleton (самый безопасный):

public enum Singleton {
    INSTANCE;
    
    public void doSomething() {
        System.out.println("Doing something");
    }
}

// Использование
Singleton.INSTANCE.doSomething();

// Защищен от:
// - Reflexon (не может создать новый экземпляр)
// - Serialization (гарантирует один экземпляр после десериализации)
// - Threading (потокобезопасен по умолчанию)

Вывод

Singleton — паттерн, когда:

  • Логически существует только один экземпляр (конфиг, логирование)
  • Явно требуется глобальная точка доступа
  • Создание объекта дорого

Singleton — антипаттерн, когда:

  • Его можно заменить Dependency Injection
  • Это скрывает зависимости
  • Это затрудняет тестирование
  • В Spring контексте — используй @Bean с scope singleton вместо ручного Singleton

Современный подход: избегай Singleton в пользу DI контейнеров (Spring, Guice). Они управляют жизненным циклом объектов и scope, скрывая детали от разработчика.