Metodi di fabbrica vs inject framework in Python: cos'è più pulito?


9

Quello che faccio di solito nelle mie applicazioni è che creo tutti i miei servizi / dao / repo / client usando i metodi di fabbrica

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

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

E quando creo un'app, lo faccio

service = Service.from_env()

ciò che crea tutte le dipendenze

e nei test quando non voglio usare il vero db faccio solo DI

service = Service(db=InMemoryDatabse())

Suppongo che sia abbastanza lontano dall'architettura clean / hex poiché il servizio sa come creare un database e sa quale tipo di database crea (potrebbe anche essere InMemoryDatabse o MongoDatabase)

Immagino che in architettura pulita / esadecimale avrei

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

E vorrei creare un framework per iniettori da fare

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

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

E le mie domande sono:

  • La mia strada è davvero cattiva? Non è più un'architettura pulita?
  • Quali sono i vantaggi dell'utilizzo dell'iniezione?
  • Vale la pena disturbare e utilizzare l'inject framework?
  • Esistono altri modi migliori per separare il dominio dall'esterno?

Risposte:


1

Esistono diversi obiettivi principali nella tecnica di Iniezione delle dipendenze, tra cui (ma non limitato a):

  • Abbassamento dell'accoppiamento tra le parti del sistema. In questo modo puoi cambiare ogni parte con meno sforzo. Vedi "Alta coesione, basso accoppiamento"
  • Applicare regole più severe sulle responsabilità. Un'entità deve fare solo una cosa sul suo livello di astrazione. Altre entità devono essere definite come dipendenze da questa. Vedi "IoC"
  • Migliore esperienza di test. Le dipendenze esplicite consentono di stub diverse parti del sistema con un comportamento di test primitivo che ha la stessa API pubblica del codice di produzione. Vedi "Mock arent 'stub"

L'altra cosa da tenere a mente è che di solito faremo affidamento su astrazioni, non su implementazioni. Vedo molte persone che usano DI per iniettare solo un'implementazione particolare. C'è una grande differenza.

Perché quando si inietta e si fa affidamento su un'implementazione, non vi è alcuna differenza nel metodo utilizzato per creare oggetti. Non importa. Ad esempio, se si inietta requestssenza astrazioni adeguate, si richiederebbe comunque qualcosa di simile con gli stessi metodi, firme e tipi di ritorno. Non saresti in grado di sostituire affatto questa implementazione. Ma quando si inietta fetch_order(order: OrderID) -> Ordersignifica che tutto può essere dentro. requests, database, qualunque cosa.

Per riassumere:

Quali sono i vantaggi dell'utilizzo dell'iniezione?

Il vantaggio principale è che non è necessario assemblare manualmente le dipendenze. Tuttavia, questo ha un costo enorme: stai usando strumenti complessi, persino magici, per risolvere i problemi. Un giorno o un'altra complessità ti combatteranno.

Vale la pena disturbare e utilizzare l'inject framework?

Un'altra cosa sul injectframework in particolare. Non mi piace quando gli oggetti in cui inietto qualcosa lo sanno. È un dettaglio di implementazione!

Come in un Postcardmodello di dominio mondiale , ad esempio, conosce questa cosa?

Consiglierei di usare punqper casi semplici e dependenciescomplessi.

injectinoltre non impone una netta separazione tra "dipendenze" e proprietà degli oggetti. Come è stato detto, uno degli obiettivi principali di DI è quello di imporre responsabilità più rigorose.

Al contrario, lascia che ti mostri come punqfunziona:

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)

Vedere? Non abbiamo nemmeno un costruttore. Definiamo in modo dichiarativo le nostre dipendenze e punqle inietteremo automaticamente. E non definiamo implementazioni specifiche. Seguiranno solo i protocolli. Questo stile è chiamato "oggetti funzionali" o classi con stile SRP .

Quindi definiamo il punqcontenitore stesso:

# 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)

E usalo:

from project.implemented import container

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

Vedere? Ora le nostre lezioni non hanno idea di chi e come le crei. Nessun decoratore, nessun valore speciale.

Maggiori informazioni sulle classi in stile SRP qui:

Esistono altri modi migliori per separare il dominio dall'esterno?

È possibile utilizzare concetti di programmazione funzionale anziché imperativi. L'idea principale dell'iniezione di dipendenza dalle funzioni è che non si chiamano cose che si basano su un contesto che non si ha. Si programmano queste chiamate per dopo, quando il contesto è presente. Ecco come è possibile illustrare l'iniezione di dipendenza con semplici funzioni:

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

L'unico problema con questo modello è che _award_points_for_letterssarà difficile da comporre.

Ecco perché abbiamo realizzato un involucro speciale per aiutare la composizione (fa parte di 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

Ad esempio, RequiresContextha un .mapmetodo speciale per comporsi con una funzione pura. E questo è tutto. Di conseguenza hai solo semplici funzioni e aiutanti di composizione con una semplice API. Nessuna magia, nessuna complessità aggiuntiva. E come bonus tutto è correttamente digitato e compatibile con mypy.

Maggiori informazioni su questo approccio qui:


0

L'esempio iniziale è abbastanza vicino a un clean / hex esatto. Ciò che manca è l'idea di una radice di composizione, e puoi fare clean / hex senza alcun framework di iniettore. Senza di essa, faresti qualcosa del tipo:

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

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

che va dalla DI / Pure / Vanilla / Poor Man, a seconda di chi parli. Un'interfaccia astratta non è assolutamente necessaria, poiché puoi fare affidamento sulla tipizzazione anatra o sulla digitazione strutturale.

Se vuoi usare un framework DI è una questione di opinioni e gusti, ma ci sono altre alternative più semplici da iniettare come punq che potresti prendere in considerazione, se scegli di seguire questa strada.

https://www.cosmicpython.com/ è una buona risorsa che esamina questi problemi in modo approfondito.


0

potresti voler utilizzare un database diverso e vuoi avere la flessibilità di farlo in modo semplice, per questo motivo considero l'iniezione di dipendenza un modo migliore per configurare il tuo servizio

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.