วิธีการของโรงงานเทียบกับกรอบการฉีดใน Python - ทำความสะอาดคืออะไร


9

สิ่งที่ฉันมักจะทำในแอปพลิเคชันของฉันคือฉันสร้างบริการ / dao / repo / ลูกค้าทั้งหมดโดยใช้วิธีการจากโรงงาน

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

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

และเมื่อฉันสร้างแอปที่ฉันทำ

service = Service.from_env()

สิ่งที่สร้างการพึ่งพาทั้งหมด

และในการทดสอบเมื่อฉันไม่ต้องการใช้ฐานข้อมูลจริงฉันเพียงแค่ทำ DI

service = Service(db=InMemoryDatabse())

ฉันคิดว่ามันค่อนข้างไกลจากสถาปัตยกรรม clean / hex เนื่องจาก Service รู้วิธีสร้างฐานข้อมูลและรู้ว่าสร้างฐานข้อมูลประเภทใด (อาจเป็น InMemoryDatabse หรือ MongoDatabase)

ฉันเดาว่าในสถาปัตยกรรม clean / hex ฉันจะมี

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

และฉันจะตั้งกรอบการทำงานของหัวฉีดให้ทำ

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

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

และคำถามของฉันคือ:

  • ทางของฉันแย่จริงๆเหรอ? มันไม่ได้เป็นสถาปัตยกรรมที่สะอาดอีกต่อไป?
  • ประโยชน์ของการใช้หัวฉีดคืออะไร?
  • มันคุ้มค่าที่จะรำคาญและใช้กรอบการฉีดหรือไม่?
  • มีวิธีอื่นที่ดีกว่าในการแยกโดเมนจากภายนอกหรือไม่

คำตอบ:


1

มีหลายเป้าหมายหลักในเทคนิคการฉีดพึ่งพารวมถึง (แต่ไม่ จำกัด เพียง):

  • ลดการเชื่อมต่อระหว่างส่วนต่าง ๆ ของระบบของคุณ วิธีนี้คุณสามารถเปลี่ยนแต่ละส่วนได้โดยใช้ความพยายามน้อยลง ดูที่"การติดต่อกันสูงการมีเพศสัมพันธ์ต่ำ"
  • เพื่อบังคับใช้กฎที่เข้มงวดเกี่ยวกับความรับผิดชอบ นิติบุคคลหนึ่งจะต้องทำเพียงสิ่งเดียวในระดับที่เป็นนามธรรม ต้องกำหนดเอนทิตีอื่นเป็นการอ้างอิงถึงสิ่งนี้ ดู"IoC"
  • ประสบการณ์การทดสอบที่ดีขึ้น การพึ่งพาอย่างชัดเจนช่วยให้คุณสามารถทำให้ส่วนต่าง ๆ ของระบบของคุณมีพฤติกรรมการทดสอบดั้งเดิมที่มี API สาธารณะเดียวกันกับรหัสการผลิตของคุณ ดู"ต้นขั้ว Mocks arent '

สิ่งอื่นที่ต้องจำไว้คือเรามักจะพึ่งพา abstractions ไม่ใช่การใช้งาน ฉันเห็นผู้คนจำนวนมากที่ใช้ DI เพื่อแทรกการใช้งานเฉพาะอย่างยิ่ง มีความแตกต่างใหญ่

เพราะเมื่อคุณฉีดและพึ่งพาการนำไปใช้งานมันไม่มีความแตกต่างในวิธีการที่เราใช้ในการสร้างวัตถุ มันไม่สำคัญ ตัวอย่างเช่นถ้าคุณฉีดrequestsโดยไม่มี abstractions ที่เหมาะสมคุณจะยังคงต้องการอะไรที่คล้ายกันกับวิธีการที่เหมือนกันลายเซ็นและประเภทส่งคืน คุณจะไม่สามารถแทนที่การใช้งานนี้ได้เลย แต่เมื่อคุณฉีดfetch_order(order: OrderID) -> Orderหมายความว่าอะไรก็ตามที่อยู่ภายใน requestsฐานข้อมูลอะไรก็ตาม

เพื่อสรุปสิ่งต่างๆ:

ประโยชน์ของการใช้หัวฉีดคืออะไร?

ประโยชน์หลักคือคุณไม่ต้องรวบรวมการอ้างอิงของคุณด้วยตนเอง อย่างไรก็ตามสิ่งนี้มาพร้อมกับค่าใช้จ่ายมหาศาล: คุณใช้เครื่องมือที่ซับซ้อนหรือน่าอัศจรรย์แม้กระทั่งในการแก้ปัญหา วันหนึ่งหรือความซับซ้อนอื่นจะต่อสู้คุณกลับ

มันคุ้มค่าที่จะรำคาญและใช้กรอบการฉีดหรือไม่?

อีกสิ่งหนึ่งที่เกี่ยวกับinjectกรอบโดยเฉพาะอย่างยิ่ง ฉันไม่ชอบเมื่อวัตถุที่ฉันฉีดบางสิ่งบางอย่างรู้เกี่ยวกับมัน มันเป็นรายละเอียดการใช้งาน!

Postcardยกตัวอย่างเช่นในรูปแบบโดเมนโลกรู้สิ่งนี้?

ฉันขอแนะนำให้ใช้punqสำหรับกรณีง่าย ๆ และdependenciesสำหรับกรณีที่ซับซ้อน

injectยังไม่บังคับใช้การแยก "การพึ่งพา" และคุณสมบัติของวัตถุอย่างชัดเจน ตามที่กล่าวไว้หนึ่งในเป้าหมายหลักของ DI คือการบังคับใช้ความรับผิดชอบที่เข้มงวดยิ่งขึ้น

ในทางตรงกันข้ามให้ฉันแสดงวิธีการpunqทำงาน:

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)

ดู? เรายังไม่มีคอนสตรัคเตอร์ เรากำหนดการพึ่งพาของเราอย่างชัดเจนและpunqจะทำการฉีดให้โดยอัตโนมัติ และเราไม่ได้กำหนดการใช้งานเฉพาะใด ๆ โปรโตคอลเท่านั้นที่จะปฏิบัติตาม สไตล์นี้เรียกว่า "วัตถุที่ใช้งานได้" หรือคลาสที่จัดรูปแบบSRP

จากนั้นเราจะกำหนดpunqคอนเทนเนอร์เอง:

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

และใช้มัน:

from project.implemented import container

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

ดู? ตอนนี้ชั้นเรียนของเราไม่มีความคิดว่าใครและสร้างอย่างไร ไม่มีผู้ตกแต่งไม่มีค่าพิเศษ

อ่านเพิ่มเติมเกี่ยวกับคลาสสไตล์ SRP ที่นี่:

มีวิธีอื่นที่ดีกว่าในการแยกโดเมนจากภายนอกหรือไม่

คุณสามารถใช้แนวคิดการเขียนโปรแกรมใช้งานได้แทนแนวคิดที่จำเป็น แนวคิดหลักของการฉีดการพึ่งพาฟังก์ชั่นคือคุณไม่ได้เรียกสิ่งที่ต้องอาศัยบริบทที่คุณไม่มี คุณกำหนดเวลาการเรียกเหล่านี้ในภายหลังเมื่อมีบริบท นี่คือวิธีที่คุณสามารถแสดงให้เห็นถึงการฉีดพึ่งพากับฟังก์ชั่นที่เรียบง่ายเพียง:

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

ปัญหาเดียวของรูปแบบนี้คือ_award_points_for_lettersการเขียนยาก

นั่นเป็นเหตุผลที่เราทำเสื้อคลุมพิเศษเพื่อช่วยให้องค์ประกอบ (มันเป็นส่วนหนึ่งของ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

ตัวอย่างเช่นRequiresContextมี.mapวิธีพิเศษในการเขียนตัวเองด้วยฟังก์ชั่นบริสุทธิ์ และนั่นคือมัน เป็นผลให้คุณมีฟังก์ชั่นที่เรียบง่ายและผู้ช่วยการแต่งเพลงด้วย API ที่เรียบง่าย ไม่มีเวทย์มนตร์ไม่มีความซับซ้อนเป็นพิเศษ mypyและเป็นโบนัสทุกอย่างจะถูกพิมพ์ถูกต้องและเข้ากันได้กับ

อ่านเพิ่มเติมเกี่ยวกับวิธีการนี้ได้ที่นี่:


0

ตัวอย่างเริ่มต้นค่อนข้างใกล้เคียงกับ clean / hex ที่ "เหมาะสม" สิ่งที่ขาดหายไปคือแนวคิดของ Composition Root และคุณสามารถทำความสะอาด / hex ได้โดยไม่ต้องใช้เฟรมเวิร์กหัวฉีด หากไม่มีมันคุณจะทำสิ่งที่ชอบ:

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

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

ซึ่งไปโดย Pure / Vanilla / Poor Man's DI ขึ้นอยู่กับว่าคุณคุยกับใคร อินเทอร์เฟซแบบนามธรรมไม่จำเป็นอย่างยิ่งเนื่องจากคุณสามารถพึ่งพาการพิมพ์แบบเป็ดหรือการพิมพ์เชิงโครงสร้าง

ไม่ว่าคุณต้องการใช้เฟรมเวิร์ก DI หรือไม่นั้นเป็นเรื่องของความคิดเห็นและรสนิยม แต่มีทางเลือกอื่นที่ง่ายกว่าในการฉีดเช่น punq ที่คุณสามารถพิจารณาได้หากคุณเลือกที่จะลงเส้นทางนั้น

https://www.cosmicpython.com/เป็นทรัพยากรที่ดีซึ่งจะพิจารณาปัญหาเหล่านี้ในเชิงลึก


0

คุณอาจต้องการใช้ฐานข้อมูลที่แตกต่างกันและคุณต้องการที่จะมีความยืดหยุ่นในการทำมันด้วยวิธีง่าย ๆ ด้วยเหตุนี้ฉันคิดว่าการพึ่งพาการฉีดเป็นวิธีที่ดีกว่าในการกำหนดค่าบริการของคุณ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.