Preferuj wstrzykiwanie zależności
Elastyczność i łatwiejsze testowanie
Ten wpis jest częścią serii, w której tworzę wpisy na podstawie wybranego tematu z książki Effective Java (3rd edition 2018), której autorem jest Joshua Bloch. Jest to uaktualnione wydanie pod Jave 9 jednej z najlepszych książek o Javie. Nie ograniczam się jednak tylko do książki, więc czasem temat będzie rozbudowany i trafią się informacje z innych źródeł na ten sam temat.
Ten wpis nawiązuje do tematu z Item 5 z rozdziału 2:
Creating and Destroying Objects
- Item 1: Consider static factory methods instead of constructors
- Item 2: Consider a builder when faced with many constructor parameters
- Item 3: Enforce the singleton property with a private constructor or an enum type
- Item 4: Enforce noninstantiability with a private constructor
- Item 5: Prefer dependency injection to hardwiring resources
- Item 6: Avoid creating unnecessary objects
- Item 7: Eliminate obsolete object references
- Item 8: Avoid finalizers and cleaners
- Item 9: Prefer try-with-resources to try-finally
Statyczne klasy utility i singletony nie powinny być stosowane tam, gdzie zachowanie klasy jest sparametryzowane przez wewnętrzne zależności. W książce pokazane jest to na przykładzie spellcheckera:
// Inappropriate use of static utility - inflexible & untestable!
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker() {} // Noninstantiable
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}
// Inappropriate use of singleton - inflexible & untestable!
public class SpellChecker {
private final Lexicon dictionary = ...;
private SpellChecker(...) {}
public static INSTANCE = new SpellChecker(...);
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}
Oba rozwiązania są niepoprawne, bo zakładają, że istnieje tylko jeden słownik, który jest warty używania. W praktyce każdy język ma swój własny, a tu nie mamy możliwości reużycia tego kodu z innym. Również podczas testowania moglibyśmy chcieć użyć jakiegoś specjalnego słownika.
Takie klasy powinny wspierać różne typy tych zależności. To klient powinien je dostarczać. W takich sytuacjach powinniśmy preferować wstrzykiwanie zależności zamiast definiować je na sztywno.
Wstrzykiwanie zależności (Dependency Injection)
Jest kilka sposobów wstrzykiwania zależności. Dwa najbardziej popularne to:
Constructor injection
Najprostszy i zarazem najczęściej wykorzystywany sposób na wstrzykiwanie zależności polega na podaniu ich poprzez konstruktor:
Client(Service service) {
this.service = service;
}
Jest to preferowany sposób.
Zapewnia, że obiekt zawsze jest spójny, bo zależności są wymuszone
Zależności nie mogą zostać zmienione
Co wcale nie musi być minusem, bo jest to pierwszy krok, aby klasa była immutable i thread safe.
Innym wariantem tego wzorca jest podanie fabryki zależności (wzorzec Factory Method) do konstruktora.
Interfejs Supplier<T>
nadaję się do tego idealnie.
Setter injection
Tu zależności ustawiamy setterem:
public void setService(Service service) {
this.service = service;
}
Elastyczność
Dzięki temu możemy zmieniać zależności w dowolnym czasie, jednak może się to przerodzić w coś niepożądanego, szczególnie w wielowątkowym środowisku.
Brak przymusu podania tej zależności
Może to prowadzić do niespójnej klasy, bo nic nie wymusza żeby tą zależność podać. Aby to osiągnąć trzeba by utworzyć metodę, która sprawdzałaby przed każdym użyciem zależnej funkcji czy ta zależność została ustawiona.
Wstrzykiwanie zależności w dużych projektach z mnóstwem zależności może stać się problematyczne, dlatego w takich przypadkach warto korzystać z przeznaczonych do tego frameworków, takich jak Spring, Guice czy Dagger, które automatyzują i upraszczają ten proces.
Wstrzykiwanie zależności w Springu
Różne frameworki mogą dostarczać zautomatyzowany sposób wstrzykiwania zależności. Np. w Springu wykorzystując adnotację @Autowired
, możemy wstrzyknąć zależność będące beanami (np.:
@Component
public class FooFormatter {
public String format() {
return "foo";
}
}
) na 3 sposoby.
Wstrzykiwanie bezpośrednio w pole
public class FooService {
@Autowired
private FooFormatter fooFormatter;
public void doSomething() {
fooFormatter.format();
//...
}
}
Dzięki adnotacji @Autowired
obiekt fooFormatter
będzie automatycznie wstrzykiwany przez Springa i od razu dostępny do użycia. Nie ma potrzeby ręcznego podawania zależności.
Jest to najczęściej nadużywana opcja, szczególnie przez początkujących i nie jest zalecaną praktyką.
Minusy:
- Uniemożliwia ręczne wstrzyknięcie zależności bez automagii framworka (refleksja) np. podczas testowania.
- Używając tego rozwiązania nie jesteśmy też w stanie zadeklarować zmiennej jako
final
. - Ukrywany jest fakt, że klasa ma zależność. Nic nie stoi na przeszkodzie, żeby zrobić coś takiego:
FooService fooService = new FooService();
fooService.doSomething();
Co oczywiście skutkuje NullPointerException
.
Jest to najłatwiejszy sposób dodawania zależności, bo wystarczy dodać kolejne pole @Autowired
, jednak nie koniecznie jest to zaleta. Przez to jesteśmy kuszeni, aby dodawać bezboleśnie kolejne zależności, aż w końcu nasz klasa będzie God klasą z 10+ zależnościami. Używając wstrzykiwania przez konstruktor od razu widać, kiedy przesadzamy z liczbą zależności. Generalnie jeśli klasa ma więcej niż 4-5 zależności trzeba się zastanowić czy przypadkiem nie robi za dużo i nie narusza SRP.
Wstrzykiwanie poprzez konstruktor
public class FooService {
private FooFormatter fooFormatter;
@Autowired
public FooService(FooFormatter fooFormatter) {
this.fooFormatter = fooFormatter;
}
}
Od Spring 4.3, jeśli klasa posiada tylko jeden konstruktor, to adnotacja @Autowired
nie jest wymagana. Z kolei jeśli klasa definiuje kilka konstruktorów, to musimy oznaczyć jeden z nich, aby pokazać kontenerowi DI, którego ma użyć.
Jest to najbardziej preferowana opcja. W niektórych framworkach (patrz Angular) jest to tak faworyzowany sposób, że jest on jedynym sposobem wstrzykiwania zależności.
Plusy:
- Nie utrudnia testowania
- Nie jesteśmy uzależnieniu od adnotacji
@Autowired
- Można łatwo zobaczyć kiedy przesadzamy z zależnościami
- Zależności mogą być immutable - możemy zadeklarować zależności jako
final
. - Zalecany przez twórców springa - link
Wstrzykiwanie poprzez setter
public class FooService {
private FooFormatter fooFormatter;
@Autowired
public void setFooFormatter(FooFormatter fooFormatter) {
this.fooFormatter = fooFormatter;
}
}
Ten wariant ma podobne minusy co wstrzykiwanie bezpośrednio w pole, ale przynajmniej nie ukrywa, że klasa zależy od FooFormatter
i umożliwia podanie jej w łatwy sposób. To rozwiązanie może być stosowane do podawania opcjonalnych zależności (bo nie jest to wymuszone jak w przypadku konstruktora), jednak w większości wypadków preferowane jest wstrzykiwanie zależności przez konstruktor.
Dlaczego wstrzykiwać zależności
Wstrzykiwanie zależności niesie ze sobą wiele korzyści i sprawia, że klasy:
Są bardziej elastyczne (decoupling)
Klasa jest uniezależniona od konkretnej implementacji. Dzięki temu jest konfigurowalna przez klienta, co sprzyja reużywalności i łatwości w utrzymaniu.
Są łatwiejsze do przetestowania
Można w łatwy sposób mockować takie zależności i testować je w izolacji.
Podsumowując, świadome korzystanie z wstrzykiwania zależności ma niemal same zalety, a preferowanym sposobem wstrzykiwania zależności jest ten z wykorzystaniem konstruktora.