Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Factory methods vs inject framework in Python - what is cleaner?

What I usually do in my applications is that I create all my services/dao/repo/clients using factory methods

class Service:
    def init(self, db):
        self._db = db

    @classmethod
    def from_env(cls):
        return cls(db=PostgresDatabase.from_env())

And when I create app I do

service = Service.from_env()

what creates all dependencies

and in tests when I dont want to use real db I just do DI

service = Service(db=InMemoryDatabse())

I suppose that is quite far from clean/hex architecture since Service knows how to create a Database and knows which database type it creates (could be also InMemoryDatabse or MongoDatabase)

I guess that in clean/hex architecture I would have

class DatabaseInterface(ABC):
    @abstractmethod
    def get_user(self, user_id: int) -> User:
        pass

import inject
class Service:
    @inject.autoparams()
    def __init__(self, db: DatabaseInterface):
        self._db = db

And I would set up injector framework to do

# in app
inject.clear_and_configure(lambda binder: binder
                           .bind(DatabaseInterface, PostgresDatabase()))

# in test
inject.clear_and_configure(lambda binder: binder
                           .bind(DatabaseInterface, InMemoryDatabse()))

And my questions are:

  • Is my way really bad? Is it not a clean architecture anymore?
  • What are the benefits of using inject?
  • Is it worth to bother and use inject framework?
  • Are there any other better ways of separating the domain from the outside?
like image 810
Ala Głowacka Avatar asked Jan 25 '20 10:01

Ala Głowacka


People also ask

Is dependency injection same as factory pattern?

So to answer the question the main difference between the Factory pattern and DI is how the object reference is obtained. With dependency injection as the name implies the reference is injected or given to your code. With Factory pattern your code must request the reference so your code fetches the object.

What is Python inject?

python-inject is a fast and simple to use python dependency injection framework. It uses decorators and descriptors to reference external dependencies. python-inject has been created to provide the pythonic way of dependency injection, utilizing specific Python functionality.

Is dependency injection useful in Python?

Dependency injection is a principle that helps to achieve an inversion of control. A dependency injection framework can significantly improve the flexibility of a language with static typing. Implementation of a dependency injection framework for a language with static typing is not something that one can do quickly.


1 Answers

There are several main goals in Dependency Injection technique, including (but not limited to):

  • Lowering coupling between parts of your system. This way you can change each part with less effort. See "High cohesion, low coupling"
  • To enforce stricter rules about responsibilities. One entity must do only one thing on its level of abstraction. Other entities must be defined as dependencies to this one. See "IoC"
  • Better testing experience. Explicit dependencies allow you to stub different parts of your system with some primitive test behaviour that has the same public API than your production code. See "Mocks arent' stubs"

The other thing to keep in mind is that we usually shall rely on abstractions, not implementations. I see a lot of people who use DI to inject only particular implementation. There's a big difference.

Because when you inject and rely on an implementation, there's no difference in what method we use to create objects. It just does not matter. For example, if you inject requests without proper abstractions you would still require anything similar with the same methods, signatures, and return types. You would not be able to replace this implementation at all. But, when you inject fetch_order(order: OrderID) -> Order it means that anything can be inside. requests, database, whatever.

To sum things up:

What are the benefits of using inject?

The main benefit is that you don't have to assemble your dependencies manually. However, this comes with a huge cost: you are using complex, even magical, tools to solve problems. One day or another complexity will fight you back.

Is it worth to bother and use inject framework?

One more thing about inject framework in particular. I don't like when objects where I inject something knows about it. It is an implementation detail!

How in a world Postcard domain model, for example, knows this thing?

I would recommend to use punq for simple cases and dependencies for complex ones.

inject also does not enforce a clean separation of "dependencies" and object properties. As it was said, one of the main goal of DI is to enforce stricter responsibilities.

In contrast, let me show how punq works:

from typing_extensions import final

from attr import dataclass

# Note, we import protocols, not implementations:
from project.postcards.repository.protocols import PostcardsForToday
from project.postcards.services.protocols import (
   SendPostcardsByEmail,
   CountPostcardsInAnalytics,
)

@final
@dataclass(frozen=True, slots=True)
class SendTodaysPostcardsUsecase(object):
    _repository: PostcardsForToday
    _email: SendPostcardsByEmail
    _analytics: CountPostcardInAnalytics

    def __call__(self, today: datetime) -> None:
        postcards = self._repository(today)
        self._email(postcards)
        self._analytics(postcards)

See? We even don't have a constructor. We declaratively define our dependencies and punq will automatically inject them. And we do not define any specific implementations. Only protocols to follow. This style is called "functional objects" or SRP-styled classes.

Then we define the punq container itself:

# project/implemented.py

import punq

container = punq.Container()

# Low level dependencies:
container.register(Postgres)
container.register(SendGrid)
container.register(GoogleAnalytics)

# Intermediate dependencies:
container.register(PostcardsForToday)
container.register(SendPostcardsByEmail)
container.register(CountPostcardInAnalytics)

# End dependencies:
container.register(SendTodaysPostcardsUsecase)

And use it:

from project.implemented import container

send_postcards = container.resolve(SendTodaysPostcardsUsecase)
send_postcards(datetime.now())

See? Now our classes have no idea who and how creates them. No decorators, no special values.

Read more about SRP-styled classes here:

  • Enforcing Single Responsibility Principle in Python

Are there any other better ways of separating the domain from the outside?

You can use functional programming concepts instead of imperative ones. The main idea of function dependency injection is that you don't call things that relies on context you don't have. You schedule these calls for later, when the context is present. Here's how you can illustrate dependency injection with just simple functions:

from django.conf import settings
from django.http import HttpRequest, HttpResponse
from words_app.logic import calculate_points

def view(request: HttpRequest) -> HttpResponse:
    user_word: str = request.POST['word']  # just an example
    points = calculate_points(user_words)(settings)  # passing the dependencies and calling
    ...  # later you show the result to user somehow

# Somewhere in your `word_app/logic.py`:

from typing import Callable
from typing_extensions import Protocol

class _Deps(Protocol):  # we rely on abstractions, not direct values or types
    WORD_THRESHOLD: int

def calculate_points(word: str) -> Callable[[_Deps], int]:
    guessed_letters_count = len([letter for letter in word if letter != '.'])
    return _award_points_for_letters(guessed_letters_count)

def _award_points_for_letters(guessed: int) -> Callable[[_Deps], int]:
    def factory(deps: _Deps):
        return 0 if guessed < deps.WORD_THRESHOLD else guessed
    return factory

The only problem with this pattern is that _award_points_for_letters will be hard to compose.

That's why we made a special wrapper to help the composition (it is a part of the returns:

import random
from typing_extensions import Protocol
from returns.context import RequiresContext

class _Deps(Protocol):  # we rely on abstractions, not direct values or types
    WORD_THRESHOLD: int

def calculate_points(word: str) -> RequiresContext[_Deps, int]:
    guessed_letters_count = len([letter for letter in word if letter != '.'])
    awarded_points = _award_points_for_letters(guessed_letters_count)
    return awarded_points.map(_maybe_add_extra_holiday_point)  # it has special methods!

def _award_points_for_letters(guessed: int) -> RequiresContext[_Deps, int]:
    def factory(deps: _Deps):
        return 0 if guessed < deps.WORD_THRESHOLD else guessed
    return RequiresContext(factory)  # here, we added `RequiresContext` wrapper

def _maybe_add_extra_holiday_point(awarded_points: int) -> int:
    return awarded_points + 1 if random.choice([True, False]) else awarded_points

For example, RequiresContext has special .map method to compose itself with a pure function. And that's it. As a result you have just simple functions and composition helpers with simple API. No magic, no extra complexity. And as a bonus everything is properly typed and compatible with mypy.

Read more about this approach here:

  • Typed functional dependency injection
  • returns docs
like image 189
sobolevn Avatar answered Oct 14 '22 01:10

sobolevn