Metode pabrik vs menyuntikkan kerangka kerja di Python - apa yang lebih bersih?

9

Apa yang biasanya saya lakukan dalam aplikasi saya adalah bahwa saya membuat semua layanan / dao / repo / klien saya menggunakan metode pabrik

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

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

Dan ketika saya membuat aplikasi saya lakukan

service = Service.from_env()

apa yang menciptakan semua dependensi

dan dalam tes ketika saya tidak ingin menggunakan db nyata saya hanya melakukan DI

service = Service(db=InMemoryDatabse())

Saya kira itu cukup jauh dari arsitektur clean / hex karena Layanan tahu bagaimana membuat Database dan tahu tipe database yang dibuatnya (bisa juga InMemoryDatabse atau MongoDatabase)

Saya kira dalam arsitektur hex / bersih saya akan memiliki

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

Dan saya akan mengatur kerangka kerja injector untuk dilakukan

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

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

Dan pertanyaan saya adalah:

  • Apakah cara saya benar-benar buruk? Apakah ini bukan arsitektur yang bersih lagi?
  • Apa manfaat menggunakan suntikan?
  • Apakah layak untuk repot dan menggunakan kerangka kerja suntikan?
  • Apakah ada cara lain yang lebih baik untuk memisahkan domain dari luar?
Ala Głowacka
sumber

Jawaban:

1

Ada beberapa tujuan utama dalam teknik Injeksi Ketergantungan, termasuk (tetapi tidak terbatas pada):

  • Menurunkan kopling antara bagian-bagian sistem Anda. Dengan cara ini Anda dapat mengubah setiap bagian dengan sedikit usaha. Lihat "Kohesi tinggi, kopling rendah"
  • Untuk menegakkan aturan yang lebih ketat tentang tanggung jawab. Satu entitas harus melakukan hanya satu hal pada tingkat abstraksi. Entitas lain harus didefinisikan sebagai dependensi untuk entitas ini. Lihat "IOC"
  • Pengalaman pengujian yang lebih baik. Ketergantungan eksplisit memungkinkan Anda untuk mematikan bagian yang berbeda dari sistem Anda dengan beberapa perilaku tes primitif yang memiliki API publik yang sama dari kode produksi Anda. Lihat "Rintisan bertopik arent '

Hal lain yang perlu diingat adalah bahwa kita biasanya akan mengandalkan abstraksi, bukan implementasi. Saya melihat banyak orang yang menggunakan DI untuk menyuntikkan hanya implementasi tertentu. Ada perbedaan besar.

Karena ketika Anda menyuntikkan dan mengandalkan implementasi, tidak ada perbedaan dalam metode apa yang kami gunakan untuk membuat objek. Itu tidak masalah. Misalnya, jika Anda menyuntikkanrequests tanpa abstraksi yang tepat, Anda masih memerlukan yang serupa dengan metode, tanda tangan, dan jenis yang sama. Anda tidak akan dapat mengganti implementasi ini sama sekali. Tetapi, ketika Anda menyuntikkan fetch_order(order: OrderID) -> Orderitu berarti bahwa apa pun bisa berada di dalam.requests, basis data, apa pun.

Singkatnya:

Apa manfaat menggunakan suntikan?

Manfaat utama adalah Anda tidak perlu memasang dependensi Anda secara manual. Namun, ini datang dengan biaya besar: Anda menggunakan alat yang kompleks, bahkan ajaib, untuk menyelesaikan masalah. Suatu hari atau kompleksitas lain akan melawan Anda kembali.

Apakah layak untuk repot dan menggunakan kerangka kerja suntikan?

Satu hal lagi tentang inject kerangka kerja khususnya. Saya tidak suka ketika benda-benda tempat saya menyuntikkan sesuatu tahu tentang itu. Ini adalah detail implementasi!

Bagaimana di dunia Postcard model domain , misalnya, mengetahui hal ini?

Saya akan merekomendasikan digunakan punquntuk kasus-kasus sederhana dandependencies untuk yang kompleks.

injectjuga tidak memberlakukan pemisahan "dependensi" dan properti objek secara bersih. Seperti yang dikatakan, salah satu tujuan utama DI adalah untuk menegakkan tanggung jawab yang lebih ketat.

Sebaliknya, izinkan saya menunjukkan cara punqkerjanya:

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)

Lihat? Kami bahkan tidak memiliki konstruktor. Kami secara deklaratif menentukan dependensi kami dan punqakan secara otomatis menyuntikkannya. Dan kami tidak mendefinisikan implementasi spesifik apa pun. Hanya protokol yang harus diikuti. Gaya ini disebut "objek fungsional" atau SRP kelas .

Kemudian kita mendefinisikan punqwadah itu sendiri:

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

Dan gunakan itu:

from project.implemented import container

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

Lihat? Sekarang kelas kami tidak tahu siapa dan bagaimana membuatnya. Tidak ada dekorator, tidak ada nilai khusus.

Baca lebih lanjut tentang kelas bergaya SRP di sini:

Apakah ada cara lain yang lebih baik untuk memisahkan domain dari luar?

Anda dapat menggunakan konsep pemrograman fungsional alih-alih yang penting. Gagasan utama injeksi ketergantungan fungsi adalah Anda tidak memanggil hal-hal yang bergantung pada konteks yang tidak Anda miliki. Anda menjadwalkan panggilan ini untuk nanti, ketika konteksnya ada. Inilah cara Anda mengilustrasikan injeksi ketergantungan hanya dengan fungsi sederhana:

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

Satu-satunya masalah dengan pola ini adalah _award_points_for_lettersakan sulit untuk menulis.

Itu sebabnya kami membuat pembungkus khusus untuk membantu komposisi (ini adalah bagian dari 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

Misalnya, RequiresContextmemiliki .mapmetode khusus untuk menyusun dirinya dengan fungsi murni. Dan itu saja. Akibatnya, Anda hanya memiliki fungsi sederhana dan pembantu komposisi dengan API sederhana. Tanpa sihir, tanpa kompleksitas tambahan. Dan sebagai bonus semuanya diketik dan kompatibel dengan benar mypy.

Baca lebih lanjut tentang pendekatan ini di sini:

sobolevn
sumber
0

Contoh awal cukup dekat dengan "benar" bersih / hex. Yang hilang adalah gagasan tentang Komposisi Root, dan Anda dapat melakukan clean / hex tanpa kerangka injektor. Tanpanya, Anda akan melakukan sesuatu seperti:

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

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

yang dilakukan oleh Pure / Vanilla / Poor Man's DI, tergantung pada siapa Anda berbicara. Antarmuka abstrak tidak mutlak diperlukan, karena Anda dapat mengandalkan mengetik-bebek atau mengetik struktural.

Apakah Anda ingin menggunakan kerangka kerja DI atau tidak adalah masalah pendapat dan selera, tetapi ada alternatif lain yang lebih sederhana untuk disuntikkan seperti punq yang dapat Anda pertimbangkan, jika Anda memilih untuk menempuh jalan itu.

https://www.cosmicpython.com/ adalah sumber yang bagus yang membahas masalah ini secara mendalam.

ejung
sumber
0

Anda mungkin ingin menggunakan database yang berbeda dan Anda ingin memiliki fleksibilitas untuk melakukannya dengan cara yang sederhana, karena alasan ini, saya menganggap injeksi ketergantungan adalah cara yang lebih baik untuk mengkonfigurasi layanan Anda

kederrac
sumber