Có một số mục tiêu chính trong kỹ thuật Tiêm phụ thuộc, bao gồm (nhưng không giới hạn):
- Giảm khớp nối giữa các bộ phận của hệ thống của bạn. Bằng cách này bạn có thể thay đổi từng phần với ít nỗ lực hơn. Xem "Độ kết dính cao, khớp nối thấp"
- Để thực thi các quy tắc chặt chẽ hơn về trách nhiệm. Một thực thể chỉ phải làm một điều trên mức độ trừu tượng của nó. Các thực thể khác phải được định nghĩa là phụ thuộc vào cái này. Xem "IoC"
- Kinh nghiệm kiểm tra tốt hơn. Các phụ thuộc rõ ràng cho phép bạn khai thác các phần khác nhau trong hệ thống của mình với một số hành vi kiểm tra nguyên thủy có cùng API công khai so với mã sản xuất của bạn. Xem "Mocks arent 'stub"
Một điều khác cần ghi nhớ là chúng ta thường sẽ dựa vào sự trừu tượng, không phải việc triển khai. Tôi thấy rất nhiều người sử dụng DI để chỉ thực hiện cụ thể. Có một sự khác biệt lớn.
Bởi vì khi bạn tiêm và dựa vào một triển khai, sẽ không có sự khác biệt trong phương pháp chúng ta sử dụng để tạo đối tượng. Nó không quan trọng. Ví dụ: nếu bạn tiêm requests
mà không có sự trừu tượng phù hợp, bạn vẫn sẽ yêu cầu bất cứ điều gì tương tự với cùng phương thức, chữ ký và các kiểu trả về. Bạn sẽ không thể thay thế việc thực hiện này cả. Nhưng, khi bạn tiêm fetch_order(order: OrderID) -> Order
nó có nghĩa là bất cứ điều gì có thể được bên trong. requests
, cơ sở dữ liệu, bất cứ điều gì.
Để tổng hợp mọi thứ:
Lợi ích của việc sử dụng thuốc tiêm là gì?
Lợi ích chính là bạn không phải lắp ráp các phụ thuộc của mình một cách thủ công. Tuy nhiên, điều này đi kèm với một chi phí rất lớn: bạn đang sử dụng các công cụ phức tạp, thậm chí kỳ diệu để giải quyết vấn đề. Một ngày hoặc phức tạp khác sẽ chống lại bạn.
Có đáng để bận tâm và sử dụng khung tiêm?
Một điều nữa về inject
khung nói riêng. Tôi không thích khi các đối tượng mà tôi tiêm một cái gì đó biết về nó. Đây là một chi tiết thực hiện!
Làm thế nào trong một Postcard
mô hình miền thế giới , ví dụ, biết điều này?
Tôi muốn giới thiệu để sử dụng punq
cho các trường hợp đơn giản và dependencies
cho những trường hợp phức tạp.
inject
cũng không thực thi một sự tách biệt rõ ràng giữa "các phụ thuộc" và các thuộc tính đối tượng. Như đã nói, một trong những mục tiêu chính của DI là thực thi các trách nhiệm chặt chẽ hơn.
Ngược lại, hãy để tôi chỉ ra cách punq
hoạt động:
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)
Xem? Chúng tôi thậm chí không có một nhà xây dựng. Chúng tôi khai báo xác định các phụ thuộc của chúng tôi và punq
sẽ tự động tiêm chúng. Và chúng tôi không định nghĩa bất kỳ triển khai cụ thể. Chỉ có các giao thức để làm theo. Phong cách này được gọi là "các đối tượng chức năng" hoặc các lớp được SRP -styled.
Sau đó, chúng tôi xác định punq
chính container:
# 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)
Và sử dụng nó:
from project.implemented import container
send_postcards = container.resolve(SendTodaysPostcardsUsecase)
send_postcards(datetime.now())
Xem? Bây giờ các lớp học của chúng tôi không có ý tưởng ai và làm thế nào tạo ra chúng. Không trang trí, không có giá trị đặc biệt.
Đọc thêm về các lớp học theo kiểu SRP tại đây:
Có cách nào khác tốt hơn để tách miền khỏi bên ngoài không?
Bạn có thể sử dụng các khái niệm lập trình chức năng thay vì các khái niệm bắt buộc. Ý tưởng chính của tiêm phụ thuộc chức năng là bạn không gọi những thứ dựa trên bối cảnh bạn không có. Bạn lên lịch các cuộc gọi này sau, khi bối cảnh có mặt. Đây là cách bạn có thể minh họa tiêm phụ thuộc chỉ với các chức năng đơn giản:
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
Vấn đề duy nhất với mẫu này là _award_points_for_letters
sẽ khó soạn.
Đó là lý do tại sao chúng tôi đã tạo ra một trình bao bọc đặc biệt để giúp sáng tác (nó là một phần của 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
Ví dụ, RequiresContext
có .map
phương thức đặc biệt để tự soạn thảo với một hàm thuần túy. Và đó là nó. Kết quả là bạn chỉ có các chức năng đơn giản và trình trợ giúp sáng tác với API đơn giản. Không có phép thuật, không có sự phức tạp thêm. Và như một phần thưởng, mọi thứ đều được gõ đúng và tương thích mypy
.
Tìm hiểu thêm về phương pháp này ở đây: