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) -> Order
itu 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 punq
untuk kasus-kasus sederhana dandependencies
untuk yang kompleks.
inject
juga 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 punq
kerjanya:
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 punq
akan 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 punq
wadah 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_letters
akan 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, RequiresContext
memiliki .map
metode 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: