При изучении 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 является вполне естественным исходя из его динамической типизации.