Грамотное внедрение зависимостей

У меня есть небольшая библиотека, в которой я получаю какие-то данные с сайта и затем сериализую их.

Я знаю, что правильным подходом к разработке является применение принципа внедрения зависимостей. В моём коде есть некий базовый класс, в котором есть все методы для работы.

class MyClass:
    def __init__(self):
        self.my_http_connection = HttpConnection()
    def my_method(self):
        return self.my_http_connection.get("address")

library = MyClass()

library.my_method()

В конструкторе я создаю экземпляр HttpConnection, на самом деле, у меня гораздо больше таких экземпляров, которые отвечают за совершенно разные ситуации. Например, класс для хэширования.

Правильно ли таким образом инициализировать классы в конструкторе? Насколько это хорошая практика? Я читал исходные коды redis-py и aiogram и там такие вещи реализованы совершенно другим образом. Например, любой метод в библиотеке aiogram реализуется через метод __call__. Условно

async def send_message(self):
    call = SendMessage()
    await self(call)

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

upd: Забыл упомянуть, что я ещё разделяю ответственность классов. Я не делаю так, чтобы в одном классе производились математические вычисления, http-запросы и работа с базой данных. Реальный пример выглядит так:

class Explorer:
    def __init__(self, connection: Http):
        self.conn = connection

    def get(url):
        return self.conn.get(url)

class MyClass:
    def __init__(self):
        self.my_http_connection = Http()
        self.serializer = Serializer()
        self.explorer = Explorer(self.my_http_connection)
    def my_method(self):
        return self.serializer.serialize(self.explorer.get("address"))

library = MyClass()

library.my_method()

И вот таким образом я реализую почти весь код. И в классе explorer может быть по аналогии ещё один класс, который отвечает за что-то и т.д.


Ответы (1 шт):

Автор решения: Ben Puls

На самом деле нет никакой проблемы в том, чтобы использовать так, как показано у вас на примерах. Кто-то создаёт экземпляры прямо в конструкторе, кто-то, как например, в библиотеке httpx, использует фабричный подход:

def _init_explorer(self, conn: Http) -> Explorer:
    return Explorer(conn)

После этого класс будет выглядеть аналогично:

class MyClass:
    def __init__(self):
        self.my_http_connection = Http()
        self.serializer = Serializer()
        self.explorer = self._init_explorer(self._my_connection)

    def _init_explorer(self, conn: Http) -> Explorer:
        return Explorer(conn)

    def my_method(self):
        return self.serializer.serialize(self.explorer.get("address"))

Никаких ограничений нет, в FastAPI, например, всё инициализируется в конструкторе также как и у вас.

def __init__(self):
    # Конечно, в FastAPI конструктор выглядит иначе.
    # Это учебный пример
    self.router: routing.APIRouter = routing.APIRouter(
        routes=routes,
        redirect_slashes=redirect_slashes,
        dependency_overrides_provider=self,
        on_startup=on_startup,
        on_shutdown=on_shutdown,
        lifespan=lifespan,
        default_response_class=default_response_class,
        dependencies=dependencies,
        callbacks=callbacks,
        deprecated=deprecated,
        include_in_schema=include_in_schema,
        responses=responses,
        generate_unique_id_function=generate_unique_id_function,
    )
    ...
→ Ссылка