Các phương thức của nhà máy so với khung công tác trong Python - cái gì là sạch hơn?


9

Những gì tôi thường làm trong các ứng dụng của mình là tôi tạo tất cả các dịch vụ / dao / repo / client của mình bằng các phương thức xuất xưởng

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

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

Và khi tôi tạo ứng dụng, tôi làm

service = Service.from_env()

điều gì tạo ra tất cả sự phụ thuộc

và trong các thử nghiệm khi tôi không muốn sử dụng db thực, tôi chỉ làm DI

service = Service(db=InMemoryDatabse())

Tôi cho rằng nó khá xa so với kiến ​​trúc sạch / hex vì Service biết cách tạo Cơ sở dữ liệu và biết loại cơ sở dữ liệu nào nó tạo ra (cũng có thể là InMemoryDatabse hoặc MongoDatabase)

Tôi đoán rằng trong kiến ​​trúc sạch / hex tôi sẽ có

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

Và tôi sẽ thiết lập khung tiêm để làm

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

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

Và câu hỏi của tôi là:

  • Là cách của tôi thực sự xấu? Nó không phải là một kiến ​​trúc sạch nữa?
  • Lợi ích của việc sử dụng thuốc tiêm là gì?
  • Có đáng để bận tâm và sử dụng khung tiêm?
  • Có cách nào khác tốt hơn để tách miền khỏi bên ngoài không?

Câu trả lời:


1

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 requestsmà 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) -> Ordernó 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ề injectkhung 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 Postcardmô 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 punqcho các trường hợp đơn giản và dependenciescho những trường hợp phức tạp.

injectcũ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 punqhoạ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à punqsẽ 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 punqchí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_letterssẽ 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.mapphươ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:


0

Ví dụ ban đầu khá gần với "sạch" / hex thích hợp. Điều còn thiếu là ý tưởng về Root Root và bạn có thể thực hiện dọn dẹp / hex mà không cần bất kỳ khung công cụ tiêm nào. Không có nó, bạn sẽ làm một cái gì đó như:

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

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

mà đi bằng Pure / Vanilla / Poor Man's DI, tùy thuộc vào người bạn nói chuyện. Một giao diện trừu tượng là không hoàn toàn cần thiết, vì bạn có thể dựa vào gõ vịt hoặc gõ cấu trúc.

Việc bạn có muốn sử dụng khung DI hay không là vấn đề về quan điểm và sở thích, nhưng có những lựa chọn đơn giản khác để tiêm như chơi chữ mà bạn có thể cân nhắc, nếu bạn chọn đi theo con đường đó.

https://www.cosmicpython.com/ là một tài nguyên tốt xem xét các vấn đề này một cách sâu sắc.


0

bạn có thể muốn sử dụng một cơ sở dữ liệu khác và bạn muốn có sự linh hoạt để thực hiện nó một cách đơn giản, vì lý do này, tôi coi tiêm phụ thuộc là một cách tốt hơn để định cấu hình dịch vụ của bạn

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.