มีหลายเป้าหมายหลักในเทคนิคการฉีดพึ่งพารวมถึง (แต่ไม่ จำกัด เพียง):
- ลดการเชื่อมต่อระหว่างส่วนต่าง ๆ ของระบบของคุณ วิธีนี้คุณสามารถเปลี่ยนแต่ละส่วนได้โดยใช้ความพยายามน้อยลง ดูที่"การติดต่อกันสูงการมีเพศสัมพันธ์ต่ำ"
- เพื่อบังคับใช้กฎที่เข้มงวดเกี่ยวกับความรับผิดชอบ นิติบุคคลหนึ่งจะต้องทำเพียงสิ่งเดียวในระดับที่เป็นนามธรรม ต้องกำหนดเอนทิตีอื่นเป็นการอ้างอิงถึงสิ่งนี้ ดู"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
และเป็นโบนัสทุกอย่างจะถูกพิมพ์ถูกต้องและเข้ากันได้กับ
อ่านเพิ่มเติมเกี่ยวกับวิธีการนี้ได้ที่นี่: