Начнем издалека — создание объекта инстанцированием класса плохо совместимо с идеями ООП. Они гласят, что код должен зависеть от интерфейсов, а не от реализаций. До тех пор пока на вход нашему коду приходят готовые объекты — все хорошо. Он будет с готовностью принимать любые типы, реализующие требуемый интерфейс, но как только мы начинаем создавать новые объекты ситуация меняется. Теперь код зависит от конкретного класса, что усложняет следующие задачи:
- Изменение класса на другой, хоть и реализующий тот же интерфейс. Приходится вручную менять все точки инстанцирования, и, возможно, перекомпилировать код;
- Выбор конкретного класса на основе внешних условий или точки инстанцирования;
- Использование уже готового объекта — взятого из пула или какого то конкретного (синглетон);
- Построение объекта с большим количеством зависимостей — приходиться передавать в точку конструирования все данные для построения множества взаимосвязанных объектов;
- Не классическая проблема для ICC, но из той-же области:
class A(object):
def __init__(self, val):
self.val = val
def __add__(self, val):
return A(self.val + val)
class B(A):
pass
print B(1) + 1 # <__main__.A object at 0x18877d0>
class A(object):
def __init__(self, val):
self.val = val
def __add__(self, val):
return A(self.val + val)
class B(A):
pass
print B(1) + 1 # <__main__.A object at 0x18877d0>
А хотелось бы получить экземпляр В.
Все эти проблемы связаны общей причиной — код делает работу, которую он делать не должен — инстанцирование конкретных классов. На самом деле чаще всего нам не нужен фиксированный класс. Нам нужен класс, предоставляющий определенный интерфейс. Нужно отвязаться от явного указания класса и передать его создание стороннему коду. Фактически мы хотим «виртуализовать» инстанцирование.
В самом простом случае можно воспользоваться фабричной функцией(ФФ). Если же мы хотим конфигурировать поведение ФФ, или сохранять состояние между вызовами (синглетон, пул объектов, etc), то логично сделать ФФ методом класса, в экземпляре которого будут храниться настройки. Такой класс может быть синглетоном(если конфигурация глобальная), или передаваться образом по цепочке вызовов во все точки, где нужно инстанцирование. Этот класс как раз и называется Inversion of Control Container (ICC дальше).
Для его использования нужно заменить прямое инстанцирование классов на вызов метода ICC. Параметрами метода будут требуемый интерфейс, и, возможно, контекст вызова и часть параметров для конструктора (последнее применяется редко). ICC возвращает готовый экземпляр. Конкретный класс для инстанцирования и параметры конструктора настраиваются програмно или берутся из конфигурационного файла.
Типичный пример — создание виртуальной машины в libvirt. Основная функция API принимает xml строку, описывающую виртуальную машину. Эта строка чаще всего берется вызывающим кодом из внешнего источника, потому как в большинстве случаев ему не важны подробности конфигурации для работы с VM соответственно и код создания можно унифицировать, а строку с конфигурацией использовать как черный ящик.
ICC также можно рассматривать как шаблон проектирования, объединяющий и унифицирующий другие порождающие шаблоны — ФФ, синглетон, и прочее.
Java и C# имеет различные реализации ICC (java spring, dagger) которые используются очень широко. Для питона же они практически не применяются. Сначала я покажу как написать pythonic ICC, а потом рассмотрю почему он не нужен. Написание своего связанно с тем, что по уже готовые пишутся людьми только что пришедшими с Java/C# и не отличаются питонистичностью.
Итак что можно хотеть от идеального ICC? Во-первых оставить пользовательский код почти без изменений. Во-вторых поддерживать возможность возвращать при инстанцировании целевого класса экземпляры другого класса, или определенный объект или результат вызова некоторой функции.
Итак был такой код:
class Bee(object):
def __init__(self, x):
pass
class Cee(object):
def __init__(self, x):
pass
assert isinstance(Bee(1), Bee)
class Bee(object):
def __init__(self, x):
pass
class Cee(object):
def __init__(self, x):
pass
assert isinstance(Bee(1), Bee)
Мы хотим иметь возможность не меняя код инстанцирования Bee выбирать что именно будет получаться — экземпляр Bee или Cee. С позиции duck typing классы Bee и Cee реализуют один и тот-же интерфейс и взаимозаменяемы, хоть мы это и не декларируем явным наследованием.
В принципе инстанцирование можно и не менять, но тогда его поведение будет не совсем очевидным. С первого взгляда кажется, что мы инстанцируем обычный класс Bee, а в итоге получаем экземпляр другого класса, который к классу Bee никакого отношения не имеет. Т.е. isinstance(Bee(), Bee) == False. Поэтому немного изменим пример. Bee и Cee будут наследовать общий интерфейс IBee и именно этот интерфейс мы и будем инстанцировать.
class IBee(IOCInterface):
def __init__(self, x):
pass
class Bee(IBee):
def __init__(self, x):
print "Bee.__init__ called"
class Cee(IBee):
def __init__(self, x):
print "Cee.__init__ called"
IBee.register(Bee)
assert isinstance(IBee(1), Bee)
IBee.register(Cee)
assert isinstance(IBee(1), Cee)
class IBee(IOCInterface):
def __init__(self, x):
pass
class Bee(IBee):
def __init__(self, x):
print "Bee.__init__ called"
class Cee(IBee):
def __init__(self, x):
print "Cee.__init__ called"
IBee.register(Bee)
assert isinstance(IBee(1), Bee)
IBee.register(Cee)
assert isinstance(IBee(1), Cee)
Что бы это работало нужно перехватить конструирование объекта типа IBee и вернуть что-мы-там-хотим. Для этого вспоминаем, что конструирование объекта в python выражается следующим псевдокодом:
# obj = Cls(x, y) ==>
obj = Cls.__new__(Cls, x, y)
if isinstance(obj, Cls):
Cls.__init__(obj, x, y)
# obj = Cls(x, y) ==>
obj = Cls.__new__(Cls, x, y)
if isinstance(obj, Cls):
Cls.__init__(obj, x, y)
Т.е. Cls.__new__ возвращает пустой экземпляр типа Cls, Cls.__init__ наполняет его реальными данными. Очень похоже на operator new + конструктор в С++. Итак нам нужно перегрузить IBee.__new__ и возвращать из него наш объект.
ioc = {}
class IOCInterface(object):
def __new__(cls, *args, **kwargs):
return ioc[cls](cls, *args, **kwargs)
@classmethod
def register(cls, impl):
factory = lambda ccls, *args, **kwargs:
super(IOCInterface, ccls).__new__(impl, *args, **kwargs)
cls.register_factory(factory)
@classmethod
def register_instance(cls, obj):
cls.register_factory(lambda *args, **kwargs: obj)
@classmethod
def register_factory(cls, func):
ioc[cls] = func
ioc = {}
class IOCInterface(object):
def __new__(cls, *args, **kwargs):
return ioc[cls](cls, *args, **kwargs)
@classmethod
def register(cls, impl):
factory = lambda ccls, *args, **kwargs:
super(IOCInterface, ccls).__new__(impl, *args, **kwargs)
cls.register_factory(factory)
@classmethod
def register_instance(cls, obj):
cls.register_factory(lambda *args, **kwargs: obj)
@classmethod
def register_factory(cls, func):
ioc[cls] = funcНемного пояснений. Класс IOCInterface будет базовым для всех интерфейсов. Переменная ioc будет хранить текущую конфигурацию — отображение интерфейса на фабричную функцию для этого интерфейса. Для простоты примера мы будем хранить конфигурацию в глобальной переменной. Перегруженный метод __new__ получает инстанцируемый класс первым параметром, а дальше идут параметры конструктора. Он берет зарегистрированную для этого класса фабричную функцию и создает новый объект с ее помощью. IOCInterface.register позволяет зарегистрировать класс для данного интерфейса. IOCInterface.register_instance — зарегистрировать синглетон. Для унификации они создают специальные фабричные функции.
Замечания:
- Нельзя использовать cls.__new__ как фабричную функцию в IOCInterface.register, так как мы получим вечный цикл. Нужно «проскочить» IOCInterface в иерархии сcls;
- Для классов с перегруженным __new__ нужно смотреть по ситуации;
- Есть соблазн просто сохранять класс/синглетон в словарь и потом в __new__
делать что-то вида;
def __new__(cls, *args, **kwargs):
obj = ioc[cls]
if isinstance(obj, type):
return obj(cls, *args, **kwargs)
elif type(obj, (types.FunctionType, types.LambdaType)):
return obj(cls, *args, **kwargs)
else:
return obj
def __new__(cls, *args, **kwargs):
obj = ioc[cls]
if isinstance(obj, type):
return obj(cls, *args, **kwargs)
elif type(obj, (types.FunctionType, types.LambdaType)):
return obj(cls, *args, **kwargs)
else:
return obj
Делать этого не стоит, хотя бы потому что так мы не сможем зареги