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

3

14

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?
Selfesteem answered 25/1, 2020 at 10:6 Comment(0)
N
5

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:

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:

Neron answered 3/2, 2020 at 20:41 Comment(0)
S
0

The initial example is pretty close to a "proper" clean/hex. What's missing is the idea of a Composition Root, and you can do clean/hex without any injector framework. Without it, you'd do something like:

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

# In your app entry point:
service = Service(PostGresDb(config.host, config.port, config.dbname))

which goes by Pure/Vanilla/Poor Man's DI, depending on who you talk to. An abstract interface is not absolutely necessary, since you can rely on duck-typing or structural typing.

Whether or not you want to use a DI framework is a matter of opinion and taste, but there are other simpler alternatives to inject like punq that you could consider, if you choose to go down that path.

https://www.cosmicpython.com/ is a good resource that looks at these issues in depth.

Shrew answered 29/1, 2020 at 8:59 Comment(0)
O
0

you may want to use a different database and you want to have the flexibility to do it in a simple way, for this reason, I consider dependency injection a better way to configure your service

Ophthalmology answered 2/2, 2020 at 17:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.