Czy powinienem używać wtrysku zależności lub statycznych?


81

Projektując system, często napotykam problem polegający na tym, że wiele modułów (logowanie, dostęp do bazy danych itp.) Jest używanych przez inne moduły. Pytanie brzmi: jak przejść do dostarczania tych komponentów innym komponentom. Dwie odpowiedzi wydają się możliwe wstrzyknięcie zależności lub użycie wzorca fabrycznego. Jednak oba wydają się błędne:

  • Fabryki sprawiają, że testowanie jest uciążliwe i nie pozwala na łatwą wymianę implementacji. Nie ujawniają również zależności (np. Badasz metodę, nieświadomy faktu, że wywołuje metodę, która wywołuje metodę, która wywołuje metodę korzystającą z bazy danych).
  • Wstrzyknięcie Dependecy ogromnie pęcznieje na listach argumentów konstruktora i rozmywa niektóre aspekty w całym kodzie. Typowa sytuacja ma miejsce, gdy wyglądają tak konstruktory o ponad połowie klas(....., LoggingProvider l, DbSessionProvider db, ExceptionFactory d, UserSession sess, Descriptions d)

Oto typowa sytuacja, z którą mam problem: Mam klasy wyjątków, które używają opisów błędów ładowanych z bazy danych, używając zapytania, które ma parametr ustawienia języka użytkownika, który jest w obiekcie sesji użytkownika. Aby utworzyć nowy wyjątek, potrzebuję opisu, który wymaga sesji bazy danych i sesji użytkownika. Więc jestem skazany na przeciąganie tych wszystkich obiektów wszystkimi moimi metodami, na wypadek, gdybym musiał rzucić wyjątek.

Jak poradzić sobie z takim problemem?


2
Jeśli fabryka może rozwiązać wszystkie twoje problemy, być może możesz po prostu wstrzyknąć fabrykę do swoich obiektów i pobrać z niej LoggingProvider, DbSessionProvider, ExceptionFactory, UserSession.
Giorgio

1
Zbyt wiele „danych wejściowych” do metody, niezależnie od tego, czy zostały one przekazane, czy wstrzyknięte, jest bardziej problemem samego projektu metod. Niezależnie od tego, co wybierzesz, możesz nieco zmniejszyć rozmiar swoich metod (co jest łatwiejsze po uzyskaniu zastrzyku na miejscu)
Bill K

Rozwiązaniem tutaj nie powinno być ograniczenie argumentów. Zamiast tego buduj abstrakcje, które budują obiekt wyższego poziomu, który wykonuje całą pracę w obiekcie i daje korzyści.
Alex

Odpowiedzi:


74

Użyj wstrzykiwania zależności, ale ilekroć listy argumentów konstruktora stają się zbyt duże, refaktoryzuj je za pomocą usługi elewacji . Chodzi o to, aby zgrupować niektóre argumenty konstruktora, wprowadzając nową abstrakcję.

Na przykład, możesz wprowadzić nowy typ SessionEnvironmentenkapsulujący a DBSessionProvider, the UserSessioni załadowany Descriptions. Jednak, aby wiedzieć, które abstrakcje mają największy sens, należy znać szczegóły swojego programu.

Podobne pytanie zostało już zadane tutaj na SO .


9
+1: Myślę, że grupowanie argumentów konstruktora w klasy jest bardzo dobrym pomysłem. Zmusza cię również do uporządkowania tych argumentów w struktury o większym znaczeniu.
Giorgio

5
Ale jeśli wynik nie jest znaczącą strukturą, ukrywasz złożoność, która narusza SRP. W takim przypadku należy dokonać refaktoryzacji klasy.
danidacar

1
@Giorgio Nie zgadzam się z ogólnym stwierdzeniem, że „grupowanie argumentów konstruktora w klasy jest bardzo dobrym pomysłem”. Jeśli kwalifikujesz to jako „w tym scenariuszu”, to jest inaczej.
tymtam

19

Wstrzyknięcie Dependecy ogromnie pęcznieje na listach argumentów konstruktora i rozmywa niektóre aspekty w całym kodzie.

Z tego nie wydaje się, abyś rozumiał DI właściwie - chodzi o odwrócenie wzorca tworzenia obiektów w fabryce.

Twój konkretny problem wydaje się być bardziej ogólnym problemem związanym z OOP. Dlaczego obiekty nie mogą po prostu rzucać normalnych, nieczytelnych dla człowieka wyjątków podczas ich działania, a następnie mieć coś przed ostatnią próbą / chwytaniem, która przechwytuje ten wyjątek, i w tym momencie używa informacji o sesji, aby zgłosić nowy, ładniejszy wyjątek ?

Innym podejściem byłoby posiadanie fabryki wyjątków, która jest przekazywana do obiektów przez ich konstruktorów. Zamiast zgłosić nowy wyjątek, klasa może zgłosić metodę fabryczną (np throw PrettyExceptionFactory.createException(data).

Pamiętaj, że twoje obiekty, oprócz obiektów fabrycznych, nigdy nie powinny używać newoperatora. Wyjątki są na ogół jednym szczególnym przypadkiem, ale w twoim przypadku mogą być wyjątkiem!


1
Czytałem gdzieś, że kiedy lista parametrów staje się zbyt długa, to nie dlatego, że używasz wstrzykiwania zależności, ale dlatego, że potrzebujesz więcej wstrzykiwań zależności.
Giorgio

Może to być jeden z powodów - ogólnie, w zależności od języka, twój konstruktor powinien mieć nie więcej niż 6-8 argumentów, a nie więcej niż 3-4 z nich powinny być obiektami, chyba że określony wzorzec (jak Builderwzorzec) dyktuje to. Jeśli przekazujesz parametry do swojego konstruktora, ponieważ obiekt tworzy instancję innych obiektów, jest to oczywisty przypadek dla IoC.
Jonathan Rich

12

Już dość dobrze wymieniłeś wady statycznego wzorca fabrycznego, ale nie całkiem zgadzam się z wadami wzorca wstrzykiwania zależności:

Wstrzyknięcie zależności wymaga, aby napisać kod dla każdej zależności, która nie jest błędem, ale cechą: Zmusza cię do zastanowienia się, czy naprawdę potrzebujesz tych zależności, promując w ten sposób luźne łączenie. W twoim przykładzie:

Oto typowa sytuacja, z którą mam problem: Mam klasy wyjątków, które używają opisów błędów ładowanych z bazy danych, używając zapytania, które ma parametr ustawienia języka użytkownika, który jest w obiekcie sesji użytkownika. Aby utworzyć nowy wyjątek, potrzebuję opisu, który wymaga sesji bazy danych i sesji użytkownika. Więc jestem skazany na przeciąganie tych wszystkich obiektów wszystkimi moimi metodami, na wypadek, gdybym musiał rzucić wyjątek.

Nie, nie jesteś skazany. Dlaczego logika biznesowa odpowiada za lokalizację komunikatów o błędach dla konkretnej sesji użytkownika? Co jeśli w przyszłości chciałbyś skorzystać z tej usługi biznesowej z programu wsadowego (który nie ma sesji użytkownika ...)? A co jeśli komunikat o błędzie nie powinien być wyświetlany aktualnie zalogowanemu użytkownikowi, ale jego przełożonemu (który może preferować inny język)? A co, jeśli chcesz ponownie użyć logiki biznesowej na kliencie (który nie ma dostępu do bazy danych ...)?

Oczywiście lokalizowanie wiadomości zależy od tego, kto je przegląda, tj. Jest to odpowiedzialność warstwy prezentacji. Dlatego rzucałbym zwykłe wyjątki od usługi biznesowej, które niosą ze sobą identyfikator komunikatu, który można następnie wyszukać w module obsługi wyjątków warstwy prezentacji w dowolnym źródle wiadomości, którego używa.

W ten sposób możesz usunąć 3 niepotrzebne zależności (UserSession, ExceptionFactory i prawdopodobnie opisy), dzięki czemu Twój kod będzie zarówno prostszy, jak i bardziej uniwersalny.

Ogólnie rzecz biorąc, używałbym statycznych fabryk tylko do rzeczy, do których potrzebujesz wszechobecnego dostępu, i które są gwarantowane, że będą dostępne we wszystkich środowiskach, w których kiedykolwiek możemy chcieć uruchomić kod (np. Rejestrowanie). Do wszystkiego innego użyłbym zwykłego zastrzyku zależności.


To. Nie widzę potrzeby naciskania bazy danych, aby zgłosić wyjątek.
Caleth

1

Użyj iniekcji zależności. Korzystanie ze statycznych fabryk to wykorzystanie Service Locatorantypatternu. Zobacz przełomową pracę Martina Fowlera tutaj - http://martinfowler.com/articles/injection.html

Jeśli argumenty konstruktora stają się zbyt duże i nie używasz kontenera DI, to napisz własne fabryki do tworzenia instancji, pozwalając na jego konfigurację przez XML lub powiązanie implementacji z interfejsem.


5
Lokalizator usług nie jest antipatternem - sam Fowler odwołuje się do niego w opublikowanym adresie URL. Chociaż wzorzec Lokalizatora usług może być nadużywany (w taki sam sposób, w jaki nadużywa się Singletonów - w celu wyodrębnienia stanu globalnego), jest to bardzo użyteczny wzorzec.
Jonathan Rich

1
Ciekawe wiedzieć. Zawsze słyszałem, że to się nazywa anty-wzór.
Sam

4
Jest to tylko antypattern, jeśli lokalizator usług służy do przechowywania stanu globalnego. Lokalizator usług powinien być obiektem bezstanowym po utworzeniu instancji i najlepiej niezmienny.
Jonathan Rich

XML nie jest bezpieczny dla typu. Uznałbym to za plan z po każdym innym niepowodzeniu
Richard Tingle

1

Ja też bym poszedł z Dependency Injection. Pamiętaj, że DI odbywa się nie tylko za pośrednictwem konstruktorów, ale także za pomocą ustawień właściwości. Na przykład rejestrator może zostać wstrzyknięty jako właściwość.

Możesz także użyć kontenera IoC, który może znieść część obciążenia, na przykład poprzez utrzymanie parametrów konstruktora na rzeczach potrzebnych w czasie wykonywania przez logikę domeny (utrzymywanie konstruktora w sposób, który ujawnia zamiar zależności klasowe i rzeczywiste) i może wstrzyknąć inne klasy pomocnicze poprzez właściwości.

Kolejnym krokiem, który możesz chcieć pójść, jest Programowanie zorientowane na aspekty, które jest wdrażane w wielu dużych ramach. Może to pozwolić ci przechwycić (lub „doradzić” użycie terminologii AspectJ) konstruktora klasy i wstrzyknąć odpowiednie właściwości, być może mając specjalny atrybut.


4
Unikam DI poprzez settery, ponieważ wprowadza ono okno czasu, w którym obiekt nie jest w pełni zainicjowany (między wywołaniem konstruktora a settera). Innymi słowy, wprowadza kolejność wywołań metod (musi wywołać X przed Y), której unikam, jeśli to w ogóle możliwe.
RokL

1
DI za pomocą ustawień właściwości jest idealny dla opcjonalnych zależności. Rejestrowanie jest dobrym przykładem. Jeśli potrzebujesz rejestrować, ustaw właściwość Logger, jeśli nie, nie ustawiaj jej.
Preston

1

Fabryki sprawiają, że testowanie jest uciążliwe i nie pozwala na łatwą wymianę implementacji. Nie ujawniają również zależności (np. Badasz metodę, nieświadomy faktu, że wywołuje metodę, która wywołuje metodę, która wywołuje metodę korzystającą z bazy danych).

Nie do końca się zgadzam. Przynajmniej nie w ogóle.

Prosta fabryka:

public IFoo GetIFoo()
{
    return new Foo();
}

Prosty zastrzyk:

myDependencyInjector.Bind<IFoo>().To<Foo>();

Oba fragmenty służą temu samemu celowi, ustanawiają łącze między IFooi Foo. Cała reszta to tylko składnia.

Zmiana Foona ADifferentFoodokładnie tyle samo wysiłku w każdej próbce kodu.
Słyszałem, jak ludzie twierdzą, że wstrzykiwanie zależności pozwala na użycie różnych powiązań, ale ten sam argument można postawić na temat tworzenia różnych fabryk. Wybór odpowiedniego wiązania jest dokładnie tak samo złożony, jak wybór właściwej fabryki.

Metody fabryczne pozwalają np. Na użycie Foow niektórych miejscach i ADifferentFooinnych miejscach. Niektórzy mogą nazywać to dobrym (przydatne, jeśli jest to potrzebne), niektórzy mogą nazywać to złym (możesz zrobić półnagą robotę, zastępując wszystko).
Jednak nie jest tak trudno uniknąć tej dwuznaczności, jeśli trzymasz się jednej metody, która powraca, IFoodzięki czemu zawsze masz jedno źródło. Jeśli nie chcesz strzelać w stopę, nie trzymaj załadowanego pistoletu lub nie celuj w stopę.


Wstrzyknięcie Dependecy ogromnie pęcznieje na listach argumentów konstruktora i rozmywa niektóre aspekty w całym kodzie.

Oto dlaczego niektórzy wolą jawnie odzyskiwać zależności w konstruktorze, tak jak to:

public MyService()
{
    _myFoo = DependencyFramework.Get<IFoo>();
}

Słyszałem argumenty pro (bez wzdęcia konstruktora), słyszałem argumenty przeciw (użycie konstruktora umożliwia większą automatyzację DI).

Osobiście, choć poddałem się naszemu seniorowi, który chce użyć argumentów konstruktora, zauważyłem problem z listą rozwijaną w VS (w prawym górnym rogu, aby przejrzeć metody obecnej klasy), w której nazwy metod znikają z pola widzenia, gdy jeden sygnatury metody są dłuższe niż mój ekran (=> rozdęty konstruktor).

Z technicznego punktu widzenia nie obchodzi mnie to. Każda z tych opcji wymaga tyle samo wysiłku, by pisać. A ponieważ używasz DI, zwykle i tak nie wywołujesz ręcznie konstruktora. Ale błąd interfejsu Visual Studio sprawia, że ​​wolę nie nadąć argumentu konstruktora.


Na marginesie: zastrzyk zależności i fabryki nie wykluczają się wzajemnie . Miałem przypadki, w których zamiast wstawiania zależności wstawiłem fabrykę, która generuje zależności (NInject na szczęście pozwala na użycie, Func<IFoo>więc nie trzeba tworzyć rzeczywistej klasy fabryki).

Przypadki użycia tego są rzadkie, ale istnieją.


OP pyta o fabryki statyczne . stackoverflow.com/questions/929021/…
Basilevs

@Basilevs „Pytanie brzmi: w jaki sposób mogę dostarczyć te komponenty innym komponentom”.
Anthony Rutledge

1
@Basilevs Wszystko, co powiedziałem, oprócz notatki dodatkowej, dotyczy również fabryk statycznych. Nie jestem pewien, co konkretnie próbujesz wskazać. Jaki jest odnośnik?
Flater

Czy jednym z takich przypadków użycia wstrzyknięcia fabryki może być taki przypadek, w którym opracowano abstrakcyjną klasę żądań HTTP i zaplanowano pięć innych polimorficznych klas potomnych, jedną dla GET, POST, PUT, PATCH i DELETE? Nie możesz wiedzieć, która metoda HTTP będzie używana za każdym razem, gdy spróbujesz zintegrować wspomnianą hierarchię klas z routerem typu MVC (który może częściowo polegać na klasie żądania HTTP. PSR-7 Interfejs wiadomości HTTP jest brzydki.
Anthony Rutledge

@AnthonyRutledge: Fabryki DI mogą oznaczać dwie rzeczy (1) Klasa, która udostępnia wiele metod. Myślę, że o tym mówisz. Nie dotyczy to jednak szczególnie fabryk. Niezależnie od tego, czy jest to klasa logiki biznesowej z kilkoma metodami publicznymi, czy fabryka z kilkoma metodami publicznymi, jest to kwestia semantyki i nie ma znaczenia technicznego. (2) Przypadek użycia specyficzny dla DI dla fabryk jest taki, że zależność niefabryczna jest tworzona raz (podczas wstrzykiwania), podczas gdy wersja fabryczna może być używana do tworzenia rzeczywistej zależności na późniejszym etapie (i być może wiele razy).
Flater

0

W tym przykładzie próbnym klasa fabryczna jest używana w czasie wykonywania w celu określenia, jaki rodzaj przychodzącego obiektu żądania HTTP ma zostać utworzony, na podstawie metody żądania HTTP. Do samej fabryki wstrzykiwany jest przykładowy pojemnik wstrzykiwania zależności. Umożliwia to fabryce określenie czasu działania i pozwala kontenerowi wstrzykiwania zależności obsługiwać zależności. Każdy przychodzący obiekt żądania HTTP ma co najmniej cztery zależności (superglobale i inne obiekty).

<?php
namespace TFWD\Factories;

/**
 * A class responsible for instantiating
 * InboundHttpRequest objects (PHP 7.x)
 * 
 * @author Anthony E. Rutledge
 * @version 2.0
 */
class InboundHttpRequestFactory 
{
    private const GET = 'GET';
    private const POST = 'POST';
    private const PUT = 'PUT';
    private const PATCH = 'PATCH';
    private const DELETE = 'DELETE';

    private static $di;
    private static $method;

    // public function __construct(Injector $di, Validator $httpRequestValidator)
    // {
    //    $this->di = $di;
    //    $this->method = $httpRequestValidator->getMethod();
    // }

    public static function setInjector(Injector $di)
    {
        self::$di = $di;
    }    

    public static setMethod(string $method)
    {
        self::$method = $method;
    }

    public static function getRequest()
    {
        if (self::$method == self::GET) {
            return self::$di->get('InboundGetHttpRequest');
        } elseif ((self::$method == self::POST) && empty($_FILES)) {
            return self::$di->get('InboundPostHttpRequest');
        } elseif (self::$method == self::POST) {
            return self::$di->get('InboundFilePostHttpRequest');
        } elseif (self::$method == self::PUT) {
            return self::$di->get('InboundPutHttpRequest');
        } elseif (self::$method == self::PATCH) {
            return self::$di->get('InboundPatchHttpRequest');
        } elseif (self::$method == self::DELETE) {
            return self::$di->get('InboundDeleteHttpRequest');
        } else {
            throw new \RuntimeException("Unexpected HTTP request. Invalid request.");
        }
    }
}

Kod klienta dla konfiguracji typu MVC w ramach scentralizowanego index.phpmoże wyglądać następująco (pominięto sprawdzanie poprawności).

InboundHttpRequestFactory::setInjector($di);
InboundHttpRequestFactory::setMethod($httpRequestValidator->getMethod());
$di->set('InboundHttpRequest', InboundHttpRequestFactory::getRequest());
$router = $di->get('Router');  // The Router class depends on InboundHttpRequest objects.
$router->dispatch(); 

Alternatywnie możesz usunąć statyczną naturę (i słowo kluczowe) fabryki i pozwolić inżektorowi zależności zarządzać całą rzeczą (stąd skomentowany konstruktor). Będziesz jednak musiał zmienić niektóre (nie stałe) odwołania selfdo elementów klasy ( ) na elementy składowe instancji ( $this).


Komentarze negatywne bez komentarzy nie są przydatne.
Anthony Rutledge
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.