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

Чем пытаются заменить singleton в современных фреймворках?

1.6 Junior🔥 211 комментариев
#DevOps и инфраструктура#Django

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

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

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

Замена Singleton в современных фреймворках

Singleton долгое время был популярным паттерном, но в современных фреймворках его почти полностью вытеснили Dependency Injection контейнеры и Scoped instances. Это произошло потому, что Singleton имеет серьёзные проблемы с тестируемостью и масштабируемостью.

Почему Singleton — это «анти-паттерн»?

# ❌ Проблемный Singleton
class DatabaseConnection:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
    
    def query(self, sql):
        # ...
        pass

# Проблемы:
# 1. Глобальное состояние — hard to test
# 2. Скрытые зависимости — код зависит от глобуса
# 3. Потокобезопасность — нужны локи
# 4. Сложно мокировать в тестах
# 5. Невозможно иметь разные инстансы для разных контекстов

Решение 1: Dependency Injection (DI)

Модерные фреймворки (FastAPI, Django, Flask с расширениями) используют DI контейнеры:

# ✅ Вместо Singleton — внедрение зависимостей
from typing import Annotated
from fastapi import FastAPI, Depends

app = FastAPI()

class DatabaseConnection:
    def __init__(self, url: str):
        self.url = url
    
    def query(self, sql: str):
        # ...
        pass

# Создаём единственный инстанс в DI контейнере
db = DatabaseConnection("postgresql://localhost/mydb")

async def get_db() -> DatabaseConnection:
    return db

# Внедряем где нужно
@app.get("/users")
async def get_users(db: Annotated[DatabaseConnection, Depends(get_db)]):
    return db.query("SELECT * FROM users")

# Преимущества:
# - Явные зависимости (видно из сигнатуры функции)
# - Легко тестировать (подаём mock)
# - Поддерживается несколько инстансов для разных контекстов
# - Нет глобального состояния

Решение 2: Scoped Instances

В некоторых сценариях нужен один инстанс на request/scope, а не глобально один:

# ✅ Scoped instance (по одному на request)
from fastapi import FastAPI, Request
from contextlib import asynccontextmanager

app = FastAPI()

class RequestContext:
    """Существует только во время одного request"""
    def __init__(self, request_id: str):
        self.request_id = request_id

@app.middleware("http")
async def add_request_context(request: Request, call_next):
    request.state.context = RequestContext(request_id="unique-id")
    response = await call_next(request)
    return response

@app.get("/data")
async def get_data(request: Request):
    # RequestContext существует только для этого request
    context = request.state.context
    return {"request_id": context.request_id}

Решение 3: Service Locator Pattern

Это промежуточное решение между Singleton и полноценным DI:

# ✅ Service Locator (более гибкий, чем Singleton)
class ServiceLocator:
    """Реестр сервисов вместо глобального инстанса"""
    _services = {}
    
    @classmethod
    def register(cls, name: str, service):
        cls._services[name] = service
    
    @classmethod
    def get(cls, name: str):
        return cls._services.get(name)

# Регистрируем сервисы
db = DatabaseConnection("postgresql://localhost/mydb")
ServiceLocator.register("db", db)
cache = RedisCache()
ServiceLocator.register("cache", cache)

# Используем
def get_user(user_id: int):
    db = ServiceLocator.get("db")
    return db.query(f"SELECT * FROM users WHERE id = {user_id}")

# Лучше, чем Singleton, но всё ещё глобальное состояние

Решение 4: Factory Pattern

# ✅ Factory для создания инстансов
class DatabaseConnectionFactory:
    _connections = {}
    
    @classmethod
    def create(cls, config: str) -> DatabaseConnection:
        """Создаёт или переиспользует connection"""
        if config not in cls._connections:
            cls._connections[config] = DatabaseConnection(config)
        return cls._connections[config]

# Позволяет контролировать создание инстансов
db_prod = DatabaseConnectionFactory.create("prod")
db_test = DatabaseConnectionFactory.create("test")

Решение 5: Собственный контейнер (как в современных фреймворках)

Django, FastAPI и другие фреймворки имеют встроенные DI контейнеры:

# ✅ Django с внедрением зависимостей (Django 3.1+)
from django.views import View
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt

class UserService:
    def get_user(self, user_id: int):
        return User.objects.get(id=user_id)

class UserView(View):
    def __init__(self, service: UserService = None):
        self.service = service or UserService()
    
    def get(self, request, user_id):
        user = self.service.get_user(user_id)
        return JsonResponse(user.to_dict())

# Или с использованием containers (более современный подход)
from dependency_injector import containers, providers

class Container(containers.DeclarativeContainer):
    """Контейнер для управления зависимостями"""
    db_connection = providers.Singleton(
        DatabaseConnection,
        url="postgresql://localhost/mydb"
    )
    user_service = providers.Factory(
        UserService,
        db=db_connection
    )

# Использование
container = Container()
user_service = container.user_service()

Сравнение подходов

┌─────────────────┬──────────┬────────────┬──────────┬─────────┐
│ Подход          │ Тесты    │ Явность    │ Гибкость │ Сложность│
├─────────────────┼──────────┼────────────┼──────────┼─────────┤
│ Singleton       │ ❌ Плохо │ ❌ Плохо   │ ❌ Нет   │ ✅ Мало │
│ Service Locator │ ⚠️ Хорошо│ ⚠️ Среднее │ ✅ Да    │ ✅ Мало │
│ Factory         │ ✅ Отлично│ ✅ Хорошо  │ ✅ Да    │ ⚠️ Среднее│
│ DI Container    │ ✅ Отлично│ ✅ Хорошо  │ ✅ Да    │ ⚠️ Среднее│
│ Scoped Instance │ ✅ Отлично│ ✅ Хорошо  │ ✅ Да    │ ✅ Мало │
└─────────────────┴──────────┴────────────┴──────────┴─────────┘

Практический пример с тестированием

# Singleton — тестировать невозможно
class BadSingleton:
    _instance = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
    
    def fetch_data(self):
        return requests.get("https://api.example.com/data")

def test_bad_singleton():
    # Как мокировать? Нельзя!
    bad = BadSingleton()
    result = bad.fetch_data()  # Делает реальный HTTP запрос

# DI — легко тестировать
class DataProvider:
    def __init__(self, http_client=None):
        self.http_client = http_client or requests
    
    def fetch_data(self):
        return self.http_client.get("https://api.example.com/data")

def test_with_di():
    mock_client = MagicMock()
    mock_client.get.return_value.json.return_value = {"data": "test"}
    
    provider = DataProvider(http_client=mock_client)
    result = provider.fetch_data()
    
    assert result == {"data": "test"}
    mock_client.get.assert_called_once()

Заключение

Модерные фреймворки заменяют Singleton на Dependency Injection контейнеры потому что они обеспечивают лучшую тестируемость, явность зависимостей и гибкость. DI — это не просто альтернатива, это правильный паттерн для построения масштабируемых и поддерживаемых приложений.

Чем пытаются заменить singleton в современных фреймворках? | PrepBro