Cześć wszystkim! Dziś przyjrzymy się jednemu z bardziej zaawansowanych aspektów programowania w Springu – dynamicznej podmianie implementacji. To zagadnienie jest kluczowe, gdy nasza aplikacja rośnie wraz z rozwojem i zaczynamy potrzebować bardziej elastycznego podejścia do zarządzania kodem.
Cześć! Dzisiaj chciałbym się skupić na tym, żebyś zobaczył w jak łatwy sposób, można w Springu, wymieniać jedną implementację na drugą. Do tego oczywiście będziemy potrzebowali prostej aplikacji. W niej będą już na nas czekały klasy takie jak ServiceA
, ServiceB
oraz ServiceC
przygotowane w poprzednim przykładzie.
Sytuacja początkowa
Załóżmy, że na start ServiceC
ma jedną zależność do ServiceA.
Jego jedyna metoda print
będzie zwracała to, co zwróci ServiceA
. Plus tam dodanie prefiksu result =
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class ServiceC {
private final ServiceA serviceA;
public ServiceC(ServiceA serviceA) {
this.serviceA = serviceA;
}
public String print() {
return "result = " + serviceA.returnSomething();
}
}
Oczywiście, aby sprawdzić, czy wszystko działa, musimy sobie napisać odpowiedni test. Tutaj znowu oprzemy się o test integracyjny. Wstrzykniemy sobie ServiceC
i napiszemy test. Oczywiście te testy są pisane tylko po to, żeby je pisać, żebyśmy mieli weryfikację tego, czy nasz program działa. Natomiast jeśli chodzi o dobre praktyki, to być może skupimy się na nich w następnych wpisach. Dobrze, uruchommy nasz test i sprawdźmy, czy przejdzie. Pamiętamy, że ServiceA
zwraca literkę A
.
1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootTest
class ServiceCIntTest {
@Autowired
ServiceC serviceC;
@Test
void test() {
assertEquals("result = A", serviceC.print());
}
}
Zmiana wykorzystywanego serwisu
Teraz załóżmy, że ServiceC
powinien korzystać z ServiceB
. Okazuje się, że ma on podobną implementację co ServiceA
. Tylko aby dokonać podmiany, musimy wykonać dużą ilość pracy. Gdyby było więcej metod, z których byśmy korzystali, to ilość pracy byłaby nieporównywalna.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class ServiceC {
private final ServiceB serviceB;
public ServiceC(ServiceB serviceB) {
this.serviceB = serviceB;
}
public String print() {
return "result = " + serviceB.returnSomething();
}
}
Po tej zmianie test nie przechodzi. Tego się spodziewaliśmy. Jeśli zmienimy wartość w asercji na literkę B
, to test już przejdzie.
1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootTest
class ServiceCIntTest {
@Autowired
ServiceC serviceC;
@Test
void test() {
assertEquals("result = B", serviceC.print());
}
}
Poprawienie naszej sytuacji
Zastanówmy się teraz, w jaki sposób możemy robić takie podmiany bez ingerowania w implementację klasy ServiceC
. Jak widzimy ServiceA
oraz ServiceB
mają identyczną sygnaturę metody. Możemy w takim razie wyciągnąć ten wniosek przed nawias i utworzyć interfejs o nazwie ReturningSomething
.
1
2
3
public interface ReturningSomething {
String returnSomething();
}
Teraz wystarczy, że obydwa serwisy, ServiceA
oraz ServiceB
, go zaimplementują. Dodatkowo w serwisie zrobimy podmianę, aby implementacja zależała od tej nowej abstrakcji.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Component
public class ServiceA implements ReturningSomething {
@Override
public String returnSomething() {
return "A";
}
}
@Component
public class ServiceB implements ReturningSomething {
@Override
public String returnSomething() {
return "B";
}
}
@Component
public class ServiceC {
private final ReturningSomething returningSomething;
public ServiceC(ReturningSomething returningSomething) {
this.returningSomething = returningSomething;
}
public String print() {
return "result = " + returningSomething.returnSomething();
}
}
Problem z brakiem dopasowania
W tym momencie Spring nie wie, co dokładnie wstrzyknąć w miejsce pola ReturningSomething
znajdującego się w ServiceC
. Musimy mu w tym pomóc. Jednym ze sposobów, ale niekoniecznie najlepszym jest skorzystanie z adnotacji @Primary
. Są też inne sposoby, ale na ten moment to nie jest istotne. Nie chodziło mi w tym momencie o przekazanie wszystkich dobrych praktyk, a jedynie o rozwiązanie problemu stabilnego kodu.
1
2
3
4
5
6
7
8
9
10
@Primary
@Component
public class ServiceA implements ReturningSomething {
@Override
public String returnSomething() {
return "A";
}
}
W ten sposób Spring wie, który z beanów ma pierwszeństwo. W sytuacjach konfliktowych to ServiceA
będzie wykorzystany. Możemy to sprawdzić przez uruchomienie przygotowanego testu. Jeśli w asercji będzie assertEquals("result = A", serviceC.print());
to test przejdzie.
Podsumowanie
W tym momencie wystarczyłoby usunąć @Primary
z ServiceA
i przenieść go do ServiceB
, aby to ten drugi miał pierwszeństwo. Kod w ServiceC
nie musiał być w ogóle modyfikowany.
Jeśli mamy jakieś zachowanie, które może być implementowane przez dwie klasy, w różny sposób, to jest to dobry pomysł, aby przygotować taką dodatkową abstrakcję. Uzyskujemy wtedy możliwość łatwiej podmiany logiki w myśl wzorca strategii.
To by było na tyle dzisiaj, dzięki za Twój czas i do zobaczenia w następnym wpisie. Na razie i cześć!