Jak wstrzykiwać różne implementacje interfejsu do jednego serwisu za pomocą Spring?

1

Muszę napisać funkcjonalność postowania ogłoszeń na różnych stronach z ogłoszeniami poprzez ich API w Spring 4. Mianowicie: Ogłoszenie utworzone w moim lokalnym systemie, może być rozsyłane automatycznie na różne zewnętrzne strony. Wiele kodu będzie się powtarzać więc chcę go ujednolicić i spinać wszystkie dane zewnętrznych ofert powiązanych z lokalną do jednej bazy. Zrobię to za pomoca interfejsów, tj. wspólny kod będzie jeden w danych serwisach, a logika mocno zależna od poszczególnych API zewnętrznych będzie wstrzykiwana w formie różnych implementacji serwisów. Jak najlepiej rozróżniać kiedy jaka implementacja ma być wstrzykiwana? Gdzie ta logika powinna byc umieszczona. Myślałem o umieszczeniu tego w konfiguracji beanów i wstrzykiwania zależności przez konstruktory czy to będzie dostatecznie czytalne i czyste rozwiazanie? Chyba jedyne jakie mi wpadło do głowy.

1

Dependency Injection to właśnie nic innego, jak tworzenie obiektów i wrzucanie zależności przez konstruktory. Kontenery DI mogą co najwyżej zautomatyzować pewne czynności. Zatem tak, jest to czytelne i czyste rozwiązanie. Dużo zależy oczywiście od tego, na podstawie czego będziesz podejmował decyzję o tym, której implementacji użyć - Twój post nie mówi za wiele na ten temat, dlatego podam Ci dwa przykłady, które - mam nadzieję - pomogą Ci podjąć właściwą decyzję.

Będę posługiwał się terminem graf obiektów oraz konstruowanie grafu obiektów - mowa tu oczywiście o obiektach klas zarządzanych przez Twój kontener DI i tym, jak są one pospinane. Konstruowanie takiego grafu to moment, kiedy kontener DI rozkminia zależności i decyduje o tym, w jakiej kolejności tworzyć obiekty oraz który gdzie wrzucić.

Przykład #1: masz dwie implementacje interfejsu: A i B. Masz klasę C, która musi dostać jedną z tych dwóch implementacji. Wybór implementacji nie zależy od stanu aplikacji - kontener DI ma wszystkie niezbędne informacje, np. zna jakieś startowe ustawienia konfiguracyjne.

W takim przypadku możesz sobie to w klasie konfiguracyjnej beanów zaimplementować, czyli wybór będzie się dokonywał podczas konstruowania grafu obiektów. Do metody wrzucasz:

  • referencje do obu implementacji,
  • wartość flagi konfiguracyjnej, na podstawie której wybierzesz jedną z nich.

W metodzie sprawdzasz wartość flagi, tworzysz docelowy obiekt i wrzucasz jedną z tych dwóch implementacji.

Przykład #2: scenariusz dynamiczny, tj. wyboru implementacji możesz dokonać wyłącznie na podstawie bieżącego stanu aplikacji. Np. przetwarzasz jakiś request od użytkownika i musisz wybrać implementację na podstawie danych w nagłówkach opisujących to, co klient akceptuje i rozumie. Wtedy oczywiście implementację możesz wybrać dopiero po odczytaniu i przetworzeniu requestu, czyli robienie tego w klasach konfiguracyjnych lekko odpada.

Tutaj stosuję inne podejście - akurat na co dzień korzystam z innych kontenerów DI (Guice, Dagger) i tam jest coś takiego, jak multibindings, czyli można sobie wstrzyknąć zbiór/mapę wszystkich implementacji:

@Inject
public MojSerwis(Map<String, Something> allSomethings) {
   ...
}

Zatem tutaj sobie wstrzykuję po prostu taką mapę - wszystkie implementacje mam nazwane i normalnie w ramach serwisu implementuję logikę, która wybierze mi jedną z nich na podstawie danych requestu. Docelowa klasa C raczej dostanie tę implementację nie w konstruktorze, ale podczas wywołania na niej jakiejś metody, która ma coś przeliczyć:

C service = ...
Something chosenImpl = allSomethings.get(getNegotiatedSomething(request));
service.performAction(chosenImpl);

Nie ma tu wielkiej filozofii. Jeśli implementację mogę poznać dopiero tuż przed wywołaniem mojej performAction(), to nie angażuję do tego kontenera DI.

1

Oprócz ładnego posta od @zyxist, to od siebie dodam adnotację @Qualifier 8)

0

@zyxist: Super, dziękuję za obszerne wytłumaczenie! moja opcja to dynamiczna opcja 2. User wybiera do jakiego Api chce wysyłać ogłoszenie.
Nie rozumiem tylko jednej rzeczy czemu w przypadku uzycia konfiguracji beanów mówisz "angażuję kontener DI"? W przypadku kiedy nie konfigurujemy beanów, kontener nie jest używany? Przecież jakaś domyślna konfiguracja istnieje, jakoś te dependencje muszą być utworzone i rozwiązane, co w takim razie to robi jesli nie kontener DI?

1

Chodzi o to, że ta część:

C service = ...
Something chosenImpl = allSomethings.get(getNegotiatedSomething(request));
service.performAction(chosenImpl);

To jest już część logiki Twojej aplikacji, a nie logiki kontenera DI. Kontener DI tworzy oczywiście wcześniej potrzebne obiekty, ale wybór implementacji i wrzucenie jej przy wywołaniu metody jako argument realizowane są już gdzie indziej.

2

W springu też można wstrzyknąć zbiór wszystkich implementacji:

@Autowired Processor[] allProcessors;

Potem już w trybie runtime można je przeglądać i wybierać. Można np. do interfejsu dorzucić coś w rodzaju isSupported() albo getSubjectClass(). Wtedy kod wybierający implementację przejrzy sobie, która implementacja co potrafi i wybierze odpowiednią. Na przykład procesory, które potrafią przetwarzać wiadomości danego typu mogą ten typ zwracać. Niedawno coś takiego zastosowałem. Mam nadzieję, że nie jest to jakiś antywzorzec :)

1 użytkowników online, w tym zalogowanych: 0, gości: 1