Fabrika yöntemleri Python'da enjekte edilen çerçeveye karşı - temizleyici nedir?


9

Uygulamalarımda genellikle yaptığım şey, tüm hizmetleri / dao / repo / müşterilerimi fabrika yöntemlerini kullanarak oluşturmamdır

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

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

Ve uygulama oluşturduğumda

service = Service.from_env()

tüm bağımlılıkları yaratan şey

ve testlerde gerçek db kullanmak istemediğimde sadece DI

service = Service(db=InMemoryDatabse())

Hizmet bir veritabanı oluşturmayı bildiğini ve hangi veritabanı türünü oluşturduğunu bildiğinden (ayrıca InMemoryDatabse veya MongoDatabase de olabilir) temiz / hex mimariden oldukça uzak olduğunu varsayalım.

Temiz / altıgen mimaride sahip olacağımı tahmin ediyorum

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

Ve bunu yapmak için enjektör çerçevesi kurarım

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

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

Sorularım:

  • Yolum gerçekten kötü mü? Artık temiz bir mimari değil mi?
  • Enjeksiyon kullanmanın yararları nelerdir?
  • Enjeksiyon çerçevesini rahatsız etmeye ve kullanmaya değer mi?
  • Alan adını dışarıdan ayırmanın daha iyi başka yolları var mı?

Yanıtlar:


1

Bağımlılık Enjeksiyonu tekniğinde aşağıdakiler dahil (ancak bunlarla sınırlı olmamak üzere) birkaç ana hedef vardır:

  • Sisteminizin parçaları arasındaki bağlantıyı indirme. Bu şekilde her parçayı daha az çabayla değiştirebilirsiniz. Bkz. "Yüksek uyum, düşük bağlantı"
  • Sorumluluklarla ilgili daha katı kurallar uygulamak. Bir varlık soyutlama düzeyinde sadece bir şey yapmalıdır. Diğer varlıklar buna bağımlı olanlar olarak tanımlanmalıdır. Bkz. "IoC"
  • Daha iyi test deneyimi. Açık bağımlılıklar, sisteminizin farklı bölümlerini, üretim kodunuzla aynı ortak API'ya sahip bazı ilkel test davranışıyla saplamanıza olanak tanır. Bkz. "Alay değil 'saplamalar"

Akılda tutulması gereken bir diğer şey, uygulamalara değil, genellikle soyutlamalara güvenmemiz gerektiğidir. Sadece belirli uygulamaları enjekte etmek için DI kullanan birçok insan görüyorum. Büyük bir fark var.

Çünkü bir uygulamaya enjekte edip bir güvendiğinizde, nesne oluşturmak için hangi yöntemi kullandığımız konusunda hiçbir fark yoktur. Sadece önemli değil. Örneğin, requestsuygun soyutlamalar olmadan enjeksiyon yaparsanız, yine de aynı yöntemlere, imzalara ve dönüş türlerine benzer bir şeye ihtiyacınız olacaktır. Bu uygulamayı hiç değiştiremezsiniz. Ancak, enjekte fetch_order(order: OrderID) -> Orderettiğinizde içeride bir şey olabileceği anlamına gelir. requests, veritabanı, her neyse.

Özetlemek gerekirse:

Enjeksiyon kullanmanın yararları nelerdir?

Ana avantajı, bağımlılıklarınızı manuel olarak bir araya getirmek zorunda kalmamanızdır. Bununla birlikte, bu büyük bir maliyetle geliyor: sorunları çözmek için karmaşık, hatta büyülü araçlar kullanıyorsunuz. Bir gün ya da başka bir karmaşıklık size karşı savaşacak.

Enjeksiyon çerçevesini rahatsız etmeye ve kullanmaya değer mi?

injectÖzellikle çerçeve hakkında bir şey daha . Bir şey enjekte ettiğim nesneler hakkında bilgi sahibi olmayı sevmiyorum. Bu bir uygulama detayıdır!

PostcardÖrneğin, bir dünya etki alanı modelinde bu şeyi nasıl biliyor?

punqBasit ve dependencieskarmaşık olanlar için kullanılmasını tavsiye ederim .

inject"bağımlılıkların" ve nesne özelliklerinin temiz bir şekilde ayrılmasını zorunlu kılmaz. Söylendiği gibi, DI'nin ana hedeflerinden biri daha katı sorumluluklar uygulamaktır.

Bunun aksine, nasıl punqçalıştığını göstereyim :

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)

Görmek? Bir kurucumuz bile yok. Bağımlılıklarımızı beyan edici olarak tanımlarız ve punqotomatik olarak enjekte ederiz . Ve biz belirli bir uygulama tanımlamıyoruz. Yalnızca izlenecek protokoller. Bu stile "işlevsel nesneler" veya SRP- stilli sınıflar denir .

Sonra punqkabın kendisini tanımlarız :

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

Ve kullanın:

from project.implemented import container

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

Görmek? Artık sınıflarımızın onları kim ve nasıl yarattığı hakkında hiçbir fikri yok. Dekoratörler yok, özel değerler yok.

SRP tarzı sınıflar hakkında daha fazla bilgiyi buradan edinebilirsiniz:

Alan adını dışarıdan ayırmanın daha iyi başka yolları var mı?

Zorunlu olanlar yerine fonksiyonel programlama kavramlarını kullanabilirsiniz. İşlev bağımlılığı enjeksiyonunun ana fikri, sahip olmadığınız bağlama dayanan şeyleri çağırmamanızdır. Bu çağrıları, bağlam mevcut olduğunda daha sonra kullanmak üzere zamanlayabilirsiniz. Bağımlılık enjeksiyonunu basit işlevlerle şu şekilde gösterebilirsiniz:

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

Bu örüntü ile ilgili tek sorun, _award_points_for_lettersoluşturulması zor olacaktır.

Bu yüzden kompozisyona yardımcı olmak için özel bir ambalaj yaptık (bunun bir parçası 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

Örneğin, kendisini saf bir işlevle oluşturmak için RequiresContextözel bir .mapyöntemi vardır . Ve bu kadar. Sonuç olarak, basit API ile sadece basit fonksiyonlar ve kompozisyon yardımcıları var. Büyü yok, ekstra karmaşıklık yok. Ve bir bonus olarak her şey düzgün bir şekilde yazılır ve uyumludur mypy.

Bu yaklaşım hakkında daha fazla bilgiyi burada bulabilirsiniz:


0

İlk örnek, "uygun" bir temiz / altıgene oldukça yakındır. Eksik olan bir Kompozisyon Kökü fikridir ve herhangi bir enjektör çerçevesi olmadan temiz / altıgen yapabilirsiniz. Onsuz, şöyle bir şey yaparsınız:

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

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

kiminle konuştuğuna bağlı olarak Pure / Vanilla / Poor Man's DI ile devam ediyor. Soyut bir arayüz kesinlikle gerekli değildir, çünkü ördek veya yapısal yazmaya güvenebilirsiniz.

Bir DI çerçevesi kullanmak isteyip istemediğiniz bir fikir ve lezzet meselesidir, ancak bu yolda ilerlemeyi seçerseniz, punq gibi enjekte etmek için başka daha basit alternatifler vardır.

https://www.cosmicpython.com/ bu sorunlara derinlemesine bakan iyi bir kaynaktır.


0

farklı bir veritabanı kullanmak isteyebilirsiniz ve bunu basit bir şekilde yapma esnekliğine sahip olmak istersiniz, bu nedenle bağımlılık enjeksiyonunu hizmetinizi yapılandırmanın daha iyi bir yolu olarak görüyorum

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.