IoC контейнеры и Python


При изучении OOП на Python, Inversion of Control (IoC) является одной важных тем, понимание которой позволяет писать хороший ООП код. IoC это еще один из принципов, который выражает идею о том, что объекты не должны зависеть от конкретной реализации. Если внутри класса создаётся конкретный экземпляр другого, то это явная зависимость – теперь поведение класса зависит от этого объекта, и его корректирование требует явного вмешательства в внутреннее устройство класса. Это наружение классического ООП подхода - “программировать на уровне интерфейсов, а не реализации”. Принцип IoC звучит очень обобщенно, при этом гораздо чаще говоря IoC подразумевается Dependency Injection (DI) – классическое решение описанной выше проблемы.

Рассмотрим стандартный пример:

# Поведение проверки орфографии явно зависит от реализации класса SpellChecker

class TextEditor:
    def __init__(self) -> None:
        self._spell_checker = SpellChecker()
from typing import Protocol

class ISpellChecker(Protocol):
    def check_spelling(self) -> None: ...


# Поведение проверки орфографии теперь зависит от интерфейса ISpellChecker
class TextEditor:
    def __init__(self, spell_checker: ISpellChecker) -> None:
        self._spell_checker = spell_checker

DI паттерн позволяет писать более “слабосвязанный” код, легко подменяя при необходимости реализацию SpellChecker на другую.

Всё это очень здорово, однако теперь у нас новая проблема. Мы забрали ответственность у TextEditor за создание SpellChecker’а, однако кто-то в конце концов должен сказать нашему редактору, как проверять орфографию. А если таких объектов у нас много? И полей у них может быть гораздо больше.

Тут на помощь приходит паттерн, который называется “IoC-контейнер”. IoC контейнер позволяет создать своего рода единый клиент, который будет решать, как именно “инжектировать” зависимости в наши объекты:

Пример использования IoC фреймворка на C#, взят отсюда:

interface IScheduleManager
{
	Schedule GetSchedule();
}

class ScheduleManager : IScheduleManager
{
	public Schedule GetSchedule()
	{
		// Do Something by init schedule...        
	}
}

class ScheduleViewer
{
	private IScheduleManager _scheduleManager;
	public ScheduleViewer(IScheduleManager scheduleManager)
	{
		_scheduleManager = scheduleManager;
	}
	public void RenderSchedule()
	{
		_scheduleManager.GetSchedule();
		// Do Something by render schedule... 
	}
}

class SimpleConfigModule : NinjectModule
{
	public override void Load()
	{
		Bind<IScheduleManager>().To<ScheduleManager>();
		// нижняя строка необязательна, это поведение стоит по умолчанию:
		// т.е. класс подставляет сам себя
		Bind<ScheduleViewer>().ToSelf();
	}
}

IKernel ninjectKernel = new StandardKernel(new SimpleConfigModule());
// там где нужно создать экземпляр ScheduleViewer мы вместо new, делаем так:
ScheduleViewer scheduleViewer = ninjectKernel.Get<ScheduleViewer>();

В конфигурационном классе мы связываем интерфейс с конкретным классом ScheduleManager, и теперь можно запрашивать создание объектов, использующих ScheduleManager у этого класса, который автоматически свяжет объект с указанной реализацией и даст нам нужный экземпляр.

И здесь наступает интересный момент: несмотря на то, что Python считается полноценным ООП языком, и принцип DI явно имеет место при написании кода на Python в ООП стиле, я едва ли часто слышал упоминание IoC-контейнеров в контексте Питона, в то время как для языков вроде C# и Java эти контейнеры являются неотъемлемой частью их фреймворков, таких как Spring.

Нет, беглый поиск позволяет найти решения вроде проекта python-dependency-injector, но их едва ли можно назвать очень популярными.

Забавное обсуждение в этом вопросе на StackOverflow, которое в комментариях к ответам не очень хорошо приходит к какому-то общему мнению (скорее к взаимным обвинениям, что кто-то не понимает принципы IoC/DI). И всё же оттуда можно прикинуть какой-то общий вывод: DI определенно имеет место в Python программировании. IoC это слишком базовый принцип, в конце концов его можно свести к тому, чтобы “зависить только от того, что нужно, и не более”, его при желании можно распространить и на функциональное программирование. Но что касается контейнеров, видимо динамическая природа Python позволять внедрять эти зависимости разными способами, и в разное время (для Java программистов, наверное, ужасно звучит мысль что можно создавать у классов новые поля и присваивать им значения прямо в рантайме:), поэтому, возможно, Python программисты просто не замечают здесь какого-то обособленного паттерна, который нуждается в своём названии. Так, например, в Django файл settings.py является одним большим контейнером с зависимостями для всего приложения. IoC-контейнеры, в конце концов, предлагают некоторое “динамическое” решение для сборки зависимостей (кто-то даже сравнивает это с Make-файлами), что для Python является вполне естественным исходя из его динамической типизации.