Чем пытаются заменить singleton в современных фреймворках?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Замена 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 — это не просто альтернатива, это правильный паттерн для построения масштабируемых и поддерживаемых приложений.