Cześć! Dzisiaj przyjrzymy się jednej z podstawowych adnotacji dostępnej w Springu, a mianowicie @Autowired
. Celem dzisiejszego materiału jest zrozumienie, dlaczego wstrzykiwanie zależności przez pole nie jest najlepszym rozwiązaniem, oraz jak za pomocą konstruktorów możemy znacząco poprawić jakość i testowalność naszego kodu.
Dlaczego @Autowired
na polu to nie najlepszy pomysł?
Zakładając, że masz już pewne doświadczenie ze Spring Bootem i @Autowired
, prawdopodobnie zauważyłeś, że wstrzykiwanie zależności bezpośrednio do pól klasy jest popularną praktyką (chociaż mocno liczę, że tak nie jest…). To podejście ma kilka istotnych wad. Najistotniejszą z nich jest fakt, jak się zaraz przekonamy, że trudniej oderwać się nam od frameworka w testach. Jesteśmy zdani na łaskę testów opartych o Spring’a, które są cięższe i trudniejsze w utrzymaniu niż testy jednostkowe. Ich czas wykonania jest o rząd większy.
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
@Component
public class ServiceA {
public String returnSomething() {
return "A";
}
}
@Component
public class ServiceB {
public String returnSomething() {
return "B";
}
}
@Component
public class ServiceC {
@Autowired
private ServiceA serviceA;
@Autowired
private ServiceB serviceB;
public String returnSomething() {
return serviceA.returnSomething() + serviceB.returnSomething();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootTest
class ServiceCIntTest {
@Autowired
ServiceC serviceC;
@Test
void test() {
assertEquals("AB", serviceC.returnSomething());
}
}
1
2
3
4
5
6
7
8
9
10
class ServiceCTest {
ServiceC serviceC = new ServiceC();
@Test
void test() {
assertEquals("AB", serviceC.returnSomething()); // NullPointerException...
}
}
Wstrzykiwanie przez konstruktor
Przejdźmy zatem do bardziej polecanej metody – wstrzykiwania przez konstruktor. W ServiceC
dokonaliśmy pewnych zmian, usunęliśmy adnotacje @Autowired
z pól i dodaliśmy konstruktor, który przyjmuje wymagane zależności. Ten ruch otwiera przed nami nowe możliwości, szczególnie jeśli chodzi o testowalność.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class ServiceC {
private ServiceA serviceA;
private ServiceB serviceB;
public ServiceC(ServiceA serviceA, ServiceB serviceB) {
this.serviceA = serviceA;
this.serviceB = serviceB;
}
public String returnSomething() {
return serviceA.returnSomething() + serviceB.returnSomething();
}
}
Dodatkowo warto podkreślić, że adnotacja @Autowired
nie jest konieczna w przypadku, gdy w klasie mamy tylko jeden konstruktor. Spring potrafi się domyślić, że to właśnie z tego konstruktora powinien skorzystać. Gorzej jest w przypadku, gdy kandydat na komponent Springa ma co najmniej dwa konstruktory. Wtedy to po naszej stronie leży odpowiedzialność, aby wskazać konstruktor, poprzez @Autowired
, z którego powinien skorzystać Spring.
Praktyczny przykład – od testów integracyjnych do jednostkowych
Dzięki zastosowaniu konstruktorów stworzyliśmy furtkę dla zmiany naszego podejścia od testów Springowych do jednostkowych. Tworząc instancję ServiceC
ręcznie, możemy mu przekazać niezbędne zależności, bez potrzeby angażowania Springowego kontenera. Zyskujemy przez to mocno na czasie uruchomienia naszych testów, które nie potrzebują całej magii Spring’a.
1
2
3
4
5
6
7
8
9
10
11
12
13
class ServiceCTest {
ServiceC serviceC = new ServiceC(
new ServiceA(),
new ServiceB()
);
@Test
void test() {
assertEquals("AB", serviceC.returnSomething());
}
}
Po przeprowadzeniu naszych testów widzimy, że wszystko działa poprawnie. Efektywnie pokazaliśmy, że podejście z wykorzystaniem konstruktora jest nie tylko bardziej eleganckie, znowu — subiektywna opinia, ale i praktyczniejsze na dłuższą metę.
Podsumowanie
Mam nadzieję, że ten wpis dał Ci lepszy wgląd w to, dlaczego wstrzykiwanie przez konstruktor jest lepszym podejściem w projektowaniu aplikacji w Springu. Dzięki temu możemy tworzyć bardziej elastyczne i testowalne rozwiązania, co jest kluczowe, dla nas, programistów.