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

Как будешь внедрять зависимости без DI в Python?

1.8 Middle🔥 181 комментариев
#Архитектура и паттерны

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

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

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

Как внедрять зависимости без DI контейнера

Dependency Injection (DI) — это паттерн, где объект получает зависимости извне, а не создаёт их сам. Хотя DI контейнеры (как Dependency Injector) помогают масштабировать, есть простые и эффективные способы без них.

Проблема: Жёсткие зависимости

# ❌ Плохо: Класс сам создаёт зависимости (tight coupling)
class UserRepository:
    def __init__(self):
        self.db = PostgresConnection()  # Жёстко привязано
    
    def get_user(self, user_id):
        return self.db.query(f"SELECT * FROM users WHERE id={user_id}")

class UserService:
    def __init__(self):
        self.repo = UserRepository()  # Создаёт сам
    
    def get_user_email(self, user_id):
        user = self.repo.get_user(user_id)
        return user.email

# Проблемы:
# - Нельзя тестировать (всегда используется реальная БД)
# - Нельзя заменить на другую реализацию
# - Изменение сложно (цепочка зависимостей)

Способ 1: Constructor Injection (Через конструктор)

# ✅ Хорошо: Зависимости передаются в конструктор
from abc import ABC, abstractmethod

class Repository(ABC):
    @abstractmethod
    def get_user(self, user_id):
        pass

class PostgresRepository(Repository):
    def __init__(self, connection_string):
        self.connection_string = connection_string
    
    def get_user(self, user_id):
        return {"id": user_id, "email": "user@example.com"}

class MockRepository(Repository):
    def get_user(self, user_id):
        return {"id": user_id, "email": "mock@example.com"}

class UserService:
    def __init__(self, repository: Repository):
        self.repository = repository
    
    def get_user_email(self, user_id):
        user = self.repository.get_user(user_id)
        return user["email"]

# Использование в продакшене
postgres_repo = PostgresRepository("postgres://...")
service = UserService(postgres_repo)

# Использование в тестах
mock_repo = MockRepository()
service_test = UserService(mock_repo)
assert service_test.get_user_email(1) == "mock@example.com"

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

  • Явно видны зависимости
  • Легко тестировать
  • Минимум кода

Способ 2: Property/Attribute Injection (Через атрибуты)

# Когда зависимость опциональна или должна меняться
class UserService:
    def __init__(self, repository=None):
        self.repository = repository or PostgresRepository()
    
    def set_repository(self, repository: Repository):
        self.repository = repository
    
    def get_user_email(self, user_id):
        return self.repository.get_user(user_id)["email"]

# Использование
service = UserService()
service.set_repository(MockRepository())  # Заменяем зависимость

assert service.get_user_email(1) == "mock@example.com"

Когда использовать: когда нужно менять зависимость во время выполнения (например, разные конфиги для разных окружений).

Способ 3: Factory Pattern (Фабрика)

# Фабрика создаёт объекты с нужными зависимостями
class UserServiceFactory:
    @staticmethod
    def create(env: str = "prod") -> UserService:
        if env == "test":
            repository = MockRepository()
        elif env == "dev":
            repository = PostgresRepository("dev://localhost")
        else:
            repository = PostgresRepository("prod://prod-server")
        
        return UserService(repository)

# Использование
service = UserServiceFactory.create("test")
service_prod = UserServiceFactory.create("prod")

Преимущества: централизованное создание объектов, легко менять логику.

Способ 4: Service Locator (Реестр сервисов)

# ❌ Анти-паттерн, но иногда полезен
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)

# Регистрация в конфиге приложения
ServiceLocator.register("repository", PostgresRepository("prod://..."))

# Использование в коде
class UserService:
    def get_user_email(self, user_id):
        repo = ServiceLocator.get("repository")
        return repo.get_user(user_id)["email"]

Осторожно: Service Locator скрывает зависимости. Используй Constructor Injection если возможно.

Способ 5: Global Configuration (Конфиг при запуске)

# Конфиг передаётся один раз при старте приложения
class Config:
    ENVIRONMENT = "production"
    DATABASE_URL = "postgres://..."

class UserRepository:
    def __init__(self):
        if Config.ENVIRONMENT == "test":
            self.connection = MockConnection()
        else:
            self.connection = PostgresConnection(Config.DATABASE_URL)

# Для тестов меняем Config перед тестом
def test_user_service():
    Config.ENVIRONMENT = "test"
    service = UserService()
    # тест...
    Config.ENVIRONMENT = "production"  # Восстанавливаем

Когда использовать: простые приложения, одна конфигурация на весь процесс.

Способ 6: Dependency Inversion через наследование

# Базовый класс определяет интерфейс
class ApplicationBase:
    def create_repository(self) -> Repository:
        return PostgresRepository()

# Приложение получает готовую иерархию
class Application(ApplicationBase):
    def run(self):
        repo = self.create_repository()
        service = UserService(repo)
        return service.get_user_email(1)

# Для тестов переопределяем метод
class TestApplication(ApplicationBase):
    def create_repository(self) -> Repository:
        return MockRepository()

# Использование
app = Application()
result = app.run()

test_app = TestApplication()
result_test = test_app.run()

Практический пример: Django приложение без DI контейнера

# settings.py
DATABASE_URL = "postgres://..."
SERVICES = {
    "user_repository": "myapp.repositories.PostgresUserRepository",
    "user_service": "myapp.services.UserService"
}

# services.py
from django.conf import settings
import importlib

class ServiceRegistry:
    @classmethod
    def get(cls, name):
        service_path = settings.SERVICES.get(name)
        module_path, class_name = service_path.rsplit(".", 1)
        module = importlib.import_module(module_path)
        return getattr(module, class_name)()

# views.py
class UserDetailView(View):
    def get(self, request, user_id):
        service = ServiceRegistry.get("user_service")
        user = service.get_user(user_id)
        return JsonResponse(user)

Рекомендации по выбору

МетодКогда использоватьСложность
Constructor InjectionВсегда, по умолчаниюНизкая
Factory PatternСложная логика созданияСредняя
Service LocatorБыстрый прототип (не в prod)Низкая (но плохие привычки)
Config-basedПростые приложения, одна конфигНизкая
DI контейнерСотни сервисов, сложные графыВысокая

Best Practice

# ✅ Правило большого пальца
# 1. Используй Constructor Injection для явности
# 2. Используй Factory для сложной логики создания
# 3. Избегай Service Locator в production
# 4. Одна конфигурация при старте приложения
# 5. Делай сервисы stateless где возможно

class UserService:
    def __init__(self, repository: Repository):
        self.repository = repository  # Явно
    
    def get_user_email(self, user_id: int) -> str:
        user = self.repository.get_user(user_id)
        return user.email

Без DI контейнера — это просто и работает для большинства приложений на Python.

Как будешь внедрять зависимости без DI в Python? | PrepBro