Wstrzykiwanie równoważenia zależności z publicznym projektem API

13

Zastanawiałem się, jak zrównoważyć projekt możliwy do przetestowania za pomocą wstrzykiwania zależności, zapewniając prosty stały publiczny interfejs API. Mój dylemat brzmi: ludzie chcieliby zrobić coś podobnego var server = new Server(){ ... }i nie musieliby się martwić tworzeniem wielu zależności i wykresu zależności, które Server(,,,,,,)mogą mieć. Podczas programowania nie martwię się zbytnio, ponieważ używam frameworka IoC / DI do obsługi tego wszystkiego (nie używam aspektów zarządzania cyklem życia żadnego kontenera, co jeszcze bardziej skomplikowałoby sprawę).

Teraz jest mało prawdopodobne, aby zależności zostały ponownie zaimplementowane. Składanie w tym przypadku jest prawie wyłącznie w celu przetestowania (i przyzwoitego projektu!), A nie tworzenia szwów dla rozszerzenia itp. Ludzie będą chcieli przez 99,999% czasu korzystać z domyślnej konfiguracji. Więc. Mógłbym zakodować zależności na stałe. Nie chcę tego robić, tracimy nasze testy! Mógłbym zapewnić domyślny konstruktor z zakodowanymi na stałe zależnościami i taki, który przyjmuje zależności. To ... niechlujny i prawdopodobnie wprowadzający w błąd, ale opłacalny. Mógłbym sprawić, by konstruktor odbierający zależność był wewnętrzny, a moja jednostka przetestowałaby zestaw znajomych (zakładając C #), który porządkuje publiczny interfejs API, ale pozostawia nieprzyjemną ukrytą pułapkę czającą się w celu konserwacji. Posiadanie dwóch konstruktorów, które są domyślnie połączone, a nie jawne, byłoby ogólnie złym projektem w mojej książce.

W tej chwili jest to najmniejsze zło, jakie mogę wymyślić. Opinie? Mądrość?

kolektiv
źródło

Odpowiedzi:

11

Wstrzykiwanie zależności jest silnym wzorcem, gdy jest dobrze stosowane, ale zbyt często jego praktykujący stają się zależni od pewnych zewnętrznych ram. Powstały interfejs API jest dość bolesny dla tych z nas, którzy nie chcą luźno wiązać naszej aplikacji z taśmą klejącą XML. Nie zapomnij o zwykłych starych obiektach (POO).

Najpierw zastanów się, jak możesz oczekiwać, że ktoś będzie korzystał z Twojego interfejsu API - bez zaangażowanych ram.

  • Jak utworzyć instancję serwera?
  • Jak rozszerzyć serwer?
  • Co chcesz udostępnić w zewnętrznym interfejsie API?
  • Co chcesz ukryć w zewnętrznym interfejsie API?

Podoba mi się pomysł zapewnienia domyślnych implementacji komponentów i fakt, że masz te komponenty. Następujące podejście jest całkowicie prawidłową i przyzwoitą praktyką OO:

public Server() 
    : this(new HttpListener(80), new HttpResponder())
{}

public Server(Listener listener, Responder responder)
{
    // ...
}

Dopóki dokumentujesz domyślne implementacje komponentów w dokumentach API i udostępniasz jednego konstruktora, który wykonuje całą konfigurację, wszystko powinno być w porządku. Konstruktory kaskadowe nie są kruche, o ile kod konfiguracji odbywa się w jednym głównym konstruktorze.

Zasadniczo wszyscy publiczni konstruktorzy mieliby różne kombinacje komponentów, które chcesz ujawnić. Wewnętrznie wypełniłyby wartości domyślne i odłożyły na prywatny konstruktor, który zakończy konfigurację. W gruncie rzeczy zapewnia to OSUSZANIE i jest dość łatwe do przetestowania.

Jeśli musisz zapewnić frienddostęp do testów, aby skonfigurować obiekt serwera w celu wyizolowania go do testowania, skorzystaj z niego. Nie będzie częścią publicznego interfejsu API, z którym chcesz być ostrożnym.

Po prostu bądź miły dla swoich klientów API i nie wymagaj frameworka IoC / DI do korzystania z biblioteki. Aby poczuć ból, który wyrządzasz, upewnij się, że twoje testy jednostkowe również nie opierają się na platformie IoC / DI. (To osobiste wkurzanie zwierzaka, ale jak tylko wprowadzisz ramę, nie jest już testowaniem jednostkowym - staje się testem integracyjnym).

Berin Loritsch
źródło
Zgadzam się i na pewno nie jestem fanem zupy konfiguracji IoC - konfiguracja XML nie jest czymś, czego używam w przypadku IoC. Z pewnością wymaganie korzystania z IoC jest dokładnie tym, czego unikałem - jest to założenie, którego publiczny projektant API nigdy nie może zrobić. Dzięki za komentarz, myślałem w myślach i uspokajające jest, że od czasu do czasu mam kontrolę zdrowia psychicznego!
kolektiv
-1 Ta odpowiedź zasadniczo mówi „nie używaj wstrzykiwania zależności” i zawiera niedokładne założenia. W twoim przykładzie, jeśli HttpListenerkonstruktor się zmieni, Serverklasa również musi się zmienić. Są sprzężone. Lepszym rozwiązaniem jest to, co mówi poniżej @Winston Ewert, który ma zapewnić domyślną klasę z predefiniowanymi zależnościami poza interfejsem API biblioteki. Następnie programista nie jest zmuszony do konfigurowania wszystkich zależności, ponieważ podejmujesz za niego decyzję, ale nadal ma elastyczność, aby je zmienić później. To wielka sprawa, gdy masz zależności od stron trzecich.
M. Dudley,
FWIW podany przykład nazywa się „wstrzyknięciem drania”. stackoverflow.com/q/2045904/111327 stackoverflow.com/q/6733667/111327
M. Dudley
1
@MichaelDudley, czy przeczytałeś pytanie OP. Wszystkie „założenia” oparte były na informacjach dostarczonych przez PO. Przez pewien czas „zastrzyk drania”, jak go nazywałeś, był preferowanym formatem wstrzykiwania zależności - szczególnie w systemach, których skład nie zmienia się podczas działania. Setery i gettery są także inną formą wstrzykiwania zależności. Po prostu użyłem przykładów opartych na tym, co zapewnił PO.
Berin Loritsch
4

W Javie w takich przypadkach zwykle konfiguracja „domyślna” jest budowana przez fabrykę, utrzymywana niezależnie od rzeczywistego „prawidłowo” zachowującego się serwera. Twój użytkownik pisze:

var server = DefaultServer.create();

podczas gdy Serverkonstruktor nadal akceptuje wszystkie swoje zależności i może być używany do głębokiego dostosowywania.

fdreger
źródło
+1, SRP. Obowiązkiem gabinetu dentystycznego nie jest budowa gabinetu dentystycznego. Odpowiedzialnością serwera nie jest zbudowanie serwera.
R. Schmitz
2

Co powiesz na zapewnienie podklasy, która zapewnia „publiczny interfejs API”

class StandardServer : Server
{
    public StandardServer():
        this( depends1, depends2, depends3)
    {
    }
}

Użytkownik może new StandardServer()i jest w drodze. Mogą również użyć podstawowej klasy Serwer, jeśli chcą mieć większą kontrolę nad tym, jak działa serwer. To mniej więcej takie podejście stosuję. (Nie używam frameworka, ponieważ nie widziałem jeszcze sensu.)

To wciąż ujawnia wewnętrzny interfejs API, ale myślę, że powinieneś. Podzieliłeś swoje obiekty na różne przydatne komponenty, które powinny działać niezależnie. Nie wiadomo, kiedy strona trzecia może chcieć użyć jednego z komponentów w izolacji.

Winston Ewert
źródło
1

Mógłbym sprawić, by konstruktor odbierający zależność był wewnętrzny, a moja jednostka przetestowałaby zestaw znajomych (zakładając C #), który porządkuje publiczny interfejs API, ale pozostawia nieprzyjemną ukrytą pułapkę czającą się w celu konserwacji.

Nie wydaje mi się to „paskudną ukrytą pułapką”. Konstruktor publiczny powinien po prostu wywoływać konstruktor wewnętrzny z zależnościami „domyślnymi”. Dopóki jest jasne, że konstruktor publiczny nie powinien być modyfikowany, wszystko powinno być w porządku.

Zaraz.
źródło
0

Całkowicie zgadzam się z twoją opinią. Nie chcemy zanieczyszczać granicy użycia komponentów tylko w celu testowania jednostkowego. To jest cały problem rozwiązania testowego opartego na DI.

Sugerowałbym użycie wzorca InjectableFactory / InjectableInstance . InjectableInstance to proste klasy narzędzi wielokrotnego użytku, które są zmiennymi posiadaczami wartości . Interfejs komponentu zawiera odwołanie do tego uchwytu wartości, który został zainicjowany przy użyciu domyślnej implementacji. Interfejs dostarczy metodę singletonową get (), która deleguje do posiadacza wartości. Klient komponentu wywoła metodę get () zamiast nowej, aby uzyskać instancję implementacji w celu uniknięcia bezpośredniej zależności. Podczas testowania możemy opcjonalnie zastąpić domyślną implementację próbną. Poniżej wykorzystam jeden przykład TimeProviderklasa, która jest abstrakcją Date (), więc pozwala testom jednostkowym wstrzykiwać i wyśmiewać, aby symulować inny moment. Przepraszam, użyję java tutaj, ponieważ jestem bardziej zaznajomiony z java. C # powinien być podobny.

public interface TimeProvider {
  // A mutable value holder with default implementation
  InjectableInstance<TimeProvider> instance = InjectableInstance.of(Impl.class);  
  static TimeProvider get() { return instance.get(); }  // Singleton method.

  class Impl implements TimeProvider {        // Default implementation                                    
    @Override public Date getDate() { return new Date(); }
    @Override public long getTimeMillis() { return System.currentTimeMillis(); }
  }

  class Mock implements TimeProvider {   // Mock implemention
    @Setter @Getter long timeMillis = System.currentTimeMillis();
    @Override public Date getDate() { return new Date(timeMillis); }
    public void add(long offset) { timeMillis += offset; }
  }

  Date getDate();
  long getTimeMillis();
}

// The client of TimeProvider
Order order = new Order().setCreationDate(TimeProvider.get().getDate()));

// In the unit testing
TimeProvider.Mock timeMock = new TimeProvider.Mock();
TimeProvider.instance.setInstance(timeMock);  // Inject mock implementation

InjectableInstance jest dość łatwy do wdrożenia, mam implementację referencyjną w Javie. Aby uzyskać szczegółowe informacje, zapoznaj się z moim postem na blogu Pośrednictwo zależności od fabryki wstrzykiwanej

Jianwu Chen
źródło
Więc ... Zastąpiłeś iniekcję zależności zmiennym singletonem? Brzmi okropnie, jak przeprowadziłbyś niezależne testy? Czy otwierasz nowy proces dla każdego testu, czy wymuszasz go w określonej kolejności? Java nie jest moją mocną stroną, czy coś źle zrozumiałem?
nvoigt
W projekcie Java maven wystąpienie współużytkowane nie stanowi problemu, ponieważ silnik junit będzie domyślnie uruchamiał każdy test w innym module ładującym klasy. Jednak ten problem występuje w niektórych środowiskach IDE, ponieważ może on ponownie użyć tego samego modułu ładującego klasy. Aby rozwiązać ten problem, klasa udostępnia metodę resetowania, która ma być wywoływana w celu przywrócenia stanu pierwotnego po każdym teście. W praktyce rzadko zdarza się, że inny test chce użyć innej makiety. Ten wzór stosujemy od lat w projektach na dużą skalę. Wszystko działa bezproblemowo, cieszyliśmy się czytelnym, czystym kodem bez poświęcania przenośności i testowalności.
Jianwu Chen