Нашел следующее решение.
Интерфейс
Я создал базовый класс интерфейса
InterfaceBase
который наследует
ABC
, для реализации абстрактного класса интерфейса. Так же создал декоратор для класса, который решает следующие задачи:
- Наследует базовый класс интерфейса
InterfaceBase
- Добавляет статический метод
create(*args, **kwargs)
этому классу именно с помощью этого можно будет создавать имплементацию напрямую из интерфейса (параметры переданные в функцию будут переданы в конструктор класса имплементации при его создании).
Так что теперь декларация интерфейса выглядит следующим образом:
@myinterfacedecorator
class IMyInterface():
@abstractmethod
def methodname(self, parameter1, paramer2): pass
Имплементация
Теперь перейдем к вкуснятинке - имплементации. Дело в том, что меня интересует два фактора:
- Оторвать имплементацию полностью от интерфейса, что бы не аффектить
ABC
классом и его мета-классом на неё
- Дать возможность подключения разных интерфейсов к одной и той же имплементации
Для этих целей я для начала сделал следующую цепочку:
class InterfaceImplementationWrapper(object): pass
- этот класс является базовым для класса имплементации, в нем я создаю дубликаты абстрактных методов с базовой имплементацией по типу:
raise NotImplementedError("Method <name> is not implemented!")
- А так же класс, что наследует интерфейс и имеет аттрибут с имплементацией абстрактных методов интерфейса внутри себя. Имплементация же абстрактных методов есть суть wrapper-функций, что редеректят все вызовы в инстанс класса имплементации, что является полем в proxy (см. выше).
class InterfaceImplementationProxy(IMyInterface): pass
Стоит заметить, что это все я генерирую динамически и оборачиваю в функцию
implementInterface
.
Таким образом мы получаем следующий код для декларации класса-имплементации:
class CMyInterfaceImplementation(implementInterface(IMyInterface)): pass
Но данная архитектура порождает проблему отсутствия механизма контролирующего создание инстанса класса имплементации, если абстрактные методы не перегружены (инстанс создать можно, только пользоваться не перегруженными методами не получиться -> приведет к exeption). По этому в proxy-класс был добавлен механизм в конструкторе, что проверяет все ли абстрактные методы интерфейса были имплементированы в классе реализации.
Подитожим:
Мы имеем интерфейс отнаследованный от базового класса интерфейсов (объявленный через декоратор
@myinterfacedecorator
), что в свою очередь является наследником ABC, каждый метод интерфейса объявлен через декоратор
@abstractmethod
.
Так же у нас есть имплементация, что отнаследована от wrapper-класса, который имеет методы такие же как в классе интерфейсе (но уже не абстрактные), с базовой имплементацией, которая вызывает exception.
Фабрика
Теперь перейдем к механизму, что создает имплементацию - фабрике. Мне показалось удобным создавать методы через статический метод интерфейса
create
, как упомянул выше. Именно он обращается к фабрике и запрашивает создание нового инстанса имплементации. Вопрос в том, как запросить класс имплементации у фабрики и как класс имплементации попадет в фабрику.
Фабрика представляет из себя singleton что хранит словарь, где каждому интерфейсу соответствует тип имплементации. Что бы зарегестрировать там новый интерфейс, следует просто вызвать необходимый метод, куда передается полный пусть в проекте к фуйлу интерфейса, имплементации, а так же имена соответствующих классов. Эта функция реализована на функционале библеотеки импортов. Эту функцию проще всего разместить в файле декларации интерфейса, так что она теперь выглядит так:
@myinterfacedecorator
class IMyInterface():
@abstractmethod
def methodname(self, parameter1, paramer2): pass
registerInterfaceAndImplementation("project.component.api.interface", "IMyInterface", "project.component.src.implementation", "CMyInterfaceImplementation")
Каждый раз когда пользователь хочет создать интерфейс, ему нужно только заимпортить api-класс (класс интерфейс) и вызвать его статический метод для создания:
from project.component.api.interface import IMyInterface
component = IMyInterface.create(constructorParameter1, constructorParameter2)
component.method()
Функция
create
(она сгенерирована через декоратор
myinterfacedecorator
) обратиться к фабрике и передаст свой тип (интерфейса) в неё с запросом на создание имплементации. Фабрика проверит наличие соответствия этому интерфейсу класса имплементации, после чего создаст прокси с имплементацией внутри (если такового нет то выкенет exception)
P.s.
Знаю что уже существуют либы реализующие подобный функционал. Но есть рестрикшены, что не позволяют их использовать.