Bağımlılık Enjeksiyonu tekniğinde aşağıdakiler dahil (ancak bunlarla sınırlı olmamak üzere) birkaç ana hedef vardır:
- Sisteminizin parçaları arasındaki bağlantıyı indirme. Bu şekilde her parçayı daha az çabayla değiştirebilirsiniz. Bkz. "Yüksek uyum, düşük bağlantı"
- Sorumluluklarla ilgili daha katı kurallar uygulamak. Bir varlık soyutlama düzeyinde sadece bir şey yapmalıdır. Diğer varlıklar buna bağımlı olanlar olarak tanımlanmalıdır. Bkz. "IoC"
- Daha iyi test deneyimi. Açık bağımlılıklar, sisteminizin farklı bölümlerini, üretim kodunuzla aynı ortak API'ya sahip bazı ilkel test davranışıyla saplamanıza olanak tanır. Bkz. "Alay değil 'saplamalar"
Akılda tutulması gereken bir diğer şey, uygulamalara değil, genellikle soyutlamalara güvenmemiz gerektiğidir. Sadece belirli uygulamaları enjekte etmek için DI kullanan birçok insan görüyorum. Büyük bir fark var.
Çünkü bir uygulamaya enjekte edip bir güvendiğinizde, nesne oluşturmak için hangi yöntemi kullandığımız konusunda hiçbir fark yoktur. Sadece önemli değil. Örneğin, requests
uygun soyutlamalar olmadan enjeksiyon yaparsanız, yine de aynı yöntemlere, imzalara ve dönüş türlerine benzer bir şeye ihtiyacınız olacaktır. Bu uygulamayı hiç değiştiremezsiniz. Ancak, enjekte fetch_order(order: OrderID) -> Order
ettiğinizde içeride bir şey olabileceği anlamına gelir. requests
, veritabanı, her neyse.
Özetlemek gerekirse:
Enjeksiyon kullanmanın yararları nelerdir?
Ana avantajı, bağımlılıklarınızı manuel olarak bir araya getirmek zorunda kalmamanızdır. Bununla birlikte, bu büyük bir maliyetle geliyor: sorunları çözmek için karmaşık, hatta büyülü araçlar kullanıyorsunuz. Bir gün ya da başka bir karmaşıklık size karşı savaşacak.
Enjeksiyon çerçevesini rahatsız etmeye ve kullanmaya değer mi?
inject
Özellikle çerçeve hakkında bir şey daha . Bir şey enjekte ettiğim nesneler hakkında bilgi sahibi olmayı sevmiyorum. Bu bir uygulama detayıdır!
Postcard
Örneğin, bir dünya etki alanı modelinde bu şeyi nasıl biliyor?
punq
Basit ve dependencies
karmaşık olanlar için kullanılmasını tavsiye ederim .
inject
"bağımlılıkların" ve nesne özelliklerinin temiz bir şekilde ayrılmasını zorunlu kılmaz. Söylendiği gibi, DI'nin ana hedeflerinden biri daha katı sorumluluklar uygulamaktır.
Bunun aksine, nasıl punq
çalıştığını göstereyim :
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)
Görmek? Bir kurucumuz bile yok. Bağımlılıklarımızı beyan edici olarak tanımlarız ve punq
otomatik olarak enjekte ederiz . Ve biz belirli bir uygulama tanımlamıyoruz. Yalnızca izlenecek protokoller. Bu stile "işlevsel nesneler" veya SRP- stilli sınıflar denir .
Sonra punq
kabın kendisini tanımlarız :
# 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)
Ve kullanın:
from project.implemented import container
send_postcards = container.resolve(SendTodaysPostcardsUsecase)
send_postcards(datetime.now())
Görmek? Artık sınıflarımızın onları kim ve nasıl yarattığı hakkında hiçbir fikri yok. Dekoratörler yok, özel değerler yok.
SRP tarzı sınıflar hakkında daha fazla bilgiyi buradan edinebilirsiniz:
Alan adını dışarıdan ayırmanın daha iyi başka yolları var mı?
Zorunlu olanlar yerine fonksiyonel programlama kavramlarını kullanabilirsiniz. İşlev bağımlılığı enjeksiyonunun ana fikri, sahip olmadığınız bağlama dayanan şeyleri çağırmamanızdır. Bu çağrıları, bağlam mevcut olduğunda daha sonra kullanmak üzere zamanlayabilirsiniz. Bağımlılık enjeksiyonunu basit işlevlerle şu şekilde gösterebilirsiniz:
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
Bu örüntü ile ilgili tek sorun, _award_points_for_letters
oluşturulması zor olacaktır.
Bu yüzden kompozisyona yardımcı olmak için özel bir ambalaj yaptık (bunun bir parçası 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
Örneğin, kendisini saf bir işlevle oluşturmak için RequiresContext
özel bir .map
yöntemi vardır . Ve bu kadar. Sonuç olarak, basit API ile sadece basit fonksiyonlar ve kompozisyon yardımcıları var. Büyü yok, ekstra karmaşıklık yok. Ve bir bonus olarak her şey düzgün bir şekilde yazılır ve uyumludur mypy
.
Bu yaklaşım hakkında daha fazla bilgiyi burada bulabilirsiniz: