Które klasy powinny być automatycznie generowane przez Spring (kiedy używać zastrzyku zależności)?

32

Używam Dependency Injection na wiosnę już od jakiegoś czasu i rozumiem, jak to działa oraz jakie są zalety i wady korzystania z niego. Kiedy jednak tworzę nową klasę, często zastanawiam się - czy tą klasą powinien zarządzać Spring IOC Container?

I nie chcę mówić o różnicach między adnotacją @Autowired, konfiguracją XML, iniekcją seterów, iniekcją konstruktora itp. Moje pytanie jest ogólne.

Załóżmy, że mamy usługę z konwerterem:

@Service
public class Service {

    @Autowired
    private Repository repository;

    @Autowired
    private Converter converter;

    public List<CarDto> getAllCars() {
        List<Car> cars = repository.findAll();
        return converter.mapToDto(cars);
    }
}

@Component
public class Converter {

    public CarDto mapToDto(List<Car> cars) {
        return new ArrayList<CarDto>(); // do the mapping here
    }
}

Najwyraźniej konwerter nie ma żadnych zależności, więc nie jest konieczne, aby został on automatycznie napisany. Ale dla mnie wygląda to lepiej jako autowired. Kod jest bardziej przejrzysty i łatwy do przetestowania. Jeśli napiszę ten kod bez DI, usługa będzie wyglądać następująco:

@Service
public class Service {

    @Autowired
    private Repository repository;

    public List<CarDto> getAllCars() {
        List<Car> cars = repository.findAll();
        Converter converter = new Converter();
        return converter.mapToDto(cars);
    }
}

Teraz znacznie trudniej to przetestować. Co więcej, nowy konwerter zostanie utworzony dla każdej operacji konwersji, nawet jeśli zawsze jest w tym samym stanie, co wydaje się narzutem.

Istnieją pewne dobrze znane wzorce w Spring MVC: Kontrolery korzystające z usług i Usługi korzystające z repozytoriów. Następnie, jeśli repozytorium jest autowiredowane (którym zwykle jest), to usługa również musi być autowiredowana. I to jest całkiem jasne. Ale kiedy używamy adnotacji @Component? Jeśli masz jakieś statyczne klasy użytkowania (takie jak konwertery, mapery) - czy je autoryzujesz?

Czy starasz się, aby wszystkie klasy były automatycznie programowane? Wtedy wszystkie zależności klasowe są łatwe do wstrzyknięcia (ponownie, łatwe do zrozumienia i przetestowania). A może próbujesz uruchomić się automatycznie tylko wtedy, gdy jest to absolutnie konieczne?

Spędziłem trochę czasu na szukaniu ogólnych zasad dotyczących korzystania z automatycznego okablowania, ale nie mogłem znaleźć żadnych konkretnych wskazówek. Zwykle ludzie mówią o „czy używasz DI? (Tak / nie)” lub „jaki typ zastrzyku zależności preferujesz”, co nie odpowiada na moje pytanie.

Byłbym wdzięczny za wszelkie wskazówki dotyczące tego tematu!

Kacper86
źródło
3
+1 za zasadniczo „Kiedy idzie za daleko?”
Andy Hunt
Sprawdź to pytanie i ten artykuł
Laiv

Odpowiedzi:

8

Zgadzam się z komentarzem @ ericW i chcę tylko dodać, pamiętaj, że możesz użyć inicjatorów, aby zachować kompaktowy kod:

@Autowired
private Converter converter;

lub

private Converter converter = new Converter();

lub, jeśli klasa naprawdę nie ma żadnego stanu

private static final Converter CONVERTER = new Converter();

Jednym z kluczowych kryteriów decydujących o tym, czy Spring powinna utworzyć instancję i wstrzyknąć fasolę, jest to, że fasola jest tak skomplikowana, że ​​chcesz się z niej kpić podczas testowania? Jeśli tak, to wstrzyknij. Na przykład, jeśli konwerter wykonuje podróż w obie strony do dowolnego systemu zewnętrznego, należy zamiast tego uczynić go komponentem. Lub jeśli zwracana wartość ma duże drzewo decyzyjne z dziesiątkami możliwych odmian w oparciu o dane wejściowe, to wyśmiewaj je. I tak dalej.

Udało ci się już dobrze rozwinąć tę funkcjonalność i ją zamknąć, więc teraz jest tylko kwestia, czy jest wystarczająco złożona, aby uznać ją za oddzielną „jednostkę” do testowania.

Obrabować
źródło
5

Nie sądzę, że musisz @Autowiredować wszystkie swoje klasy, powinno to zależeć od rzeczywistego użycia, w twoich scenariuszach lepiej jest używać metody statycznej zamiast @Autowired. Nie widziałem żadnej korzyści użycia @Autowired dla tych prostych klas utils, a to absolutnie zwiększy koszt kontenera Spring, jeśli nie użyje go poprawnie.

ericW
źródło
2

Moja ogólna zasada opiera się na czymś, co powiedziałeś: testowalność. Zadaj sobie pytanie „ Czy mogę to łatwo przetestować? ”. Jeśli odpowiedź brzmi „tak”, to bez jakiegokolwiek innego powodu byłbym z tym w porządku. Jeśli więc opracujesz test jednostkowy w tym samym czasie, w którym się rozwijasz, zaoszczędzisz wiele bólu.

Jedynym potencjalnym problemem jest to, że jeśli konwerter zawiedzie, test usługi również się nie powiedzie. Niektórzy twierdzą, że należy kpić z wyników konwertera w testach jednostkowych. W ten sposób można szybciej zidentyfikować błędy. Ale ma swoją cenę: musisz wyśmiewać wszystkie wyniki konwerterów, gdy prawdziwy konwerter mógł wykonać pracę.

Zakładam również, że nie ma żadnego powodu, aby używać różnych konwerterów dto.

Borjab
źródło
0

TL; DR: Hybrydowe podejście automatycznego okablowania dla DI i przekazywanie konstruktora dla DI może uprościć przedstawiony kod.

Patrzyłem na podobne pytania ze względu na pewne weblogiki z błędami / komplikacjami podczas uruchamiania frameworka wiosennego, obejmującymi zależności inicjalizacji @autowired bean. Zacząłem mieszać z innym podejściem DI: przekazywaniem konstruktorów. Wymaga warunków takich jak ty („Oczywiście, konwerter nie ma żadnych zależności, więc nie jest konieczne, aby był on automatycznie zapisany”). Niemniej jednak bardzo mi się podoba ta elastyczność i wciąż jest całkiem prosta.

@Service
public class Service {

    @Autowired
    private Repository repository;

    public List<CarDto> getAllCars(Converter converter) {
        List<Car> cars = repository.findAll();
        return converter.mapToDto(cars);
    }
    public List<CarDto> getAllCars() {
        Converter converter = new Converter();
        return getAllCars(converter);
    }
}

lub nawet jako rif z odpowiedzi Roba

@Service
public class Service {

    @Autowired
    private Repository repository;

    private final Converter converter = new Converter(); // static if safe for that

    public List<CarDto> getAllCars(Converter converter) {
        List<Car> cars = repository.findAll();
        return converter.mapToDto(cars);
    }
    public List<CarDto> getAllCars() {
        return getAllCars(converter);
    }
}

Może nie wymagać zmiany interfejsu publicznego, ale ja bym tego zrobił. Nadal

public List<CarDto> getAllCars(Converter converter) { ... }

może być chroniony lub prywatny w celu ograniczenia zakresu tylko do celów testowych / rozszerzenia.

Kluczową kwestią jest to, że DI jest opcjonalny. Zapewniona jest wartość domyślna, którą można zastąpić w prosty sposób. Ma swoją słabość wraz ze wzrostem liczby pól, ale dla 1, a może 2 pól, wolę to pod następującymi warunkami:

  • Bardzo mała liczba pól
  • Domyślnie prawie zawsze jest używana (DI to szew testowy) lub Domyślnie prawie nigdy nie jest używana (stop gap), ponieważ lubię spójność, więc jest to część mojego drzewa decyzyjnego, a nie prawdziwe ograniczenie projektowe

Ostatni punkt (trochę OT, ale związany z tym, jak decydujemy, co / gdzie @autowired): klasa konwertera, jak przedstawiono, jest metodą użytkową (bez pól, bez konstruktora, może być statyczna). Może rozwiązaniem byłoby zastosowanie metody mapToDto () w klasie Cars? To znaczy, przenieś zastrzyk konwersji do definicji samochodów, gdzie prawdopodobnie jest już ściśle związany:

@Service
public class Service {

   @Autowired
   private Repository repository;

   public List<CarDto> getAllCars() {
    return repository.findAll().stream.map(c -> c.mapToDto()).collect(Collectors.toList()));
   }
}
Kristian H.
źródło
-1

Myślę, że dobrym tego przykładem jest SecurityUtils lub UserUtils. Do każdej aplikacji Spring, która zarządza użytkownikami, potrzebujesz klasy Util z całą masą metod statycznych, takich jak:

getCurrentUser ()

isAuthenticated ()

isCurrentUserInRole (organ władzy)

itp.

i nigdy nie stworzyłem ich automatycznie. JHipster (którego używam jako niezłą ocenę najlepszych praktyk) nie robi tego.

Janome314
źródło
-2

Możemy oddzielić klasy na podstawie funkcji tej klasy, takich jak kontrolery, usługa, repozytorium, byt. Możemy wykorzystywać inne klasy do naszej logiki, nie tylko do celów kontrolerów, usług itp., Przy tej okazji możemy adnotować te klasy za pomocą @component. Spowoduje to automatyczne zarejestrowanie tej klasy w pojemniku sprężynowym. Jeśli jest zarejestrowany, będzie zarządzany przez kontener wiosenny. Pojęcie iniekcji zależności i inwersji kontroli może być dostarczone do tej klasy z adnotacjami.

SathishSakthi
źródło
3
ten post jest raczej trudny do odczytania (ściana tekstu). Czy mógłbyś edytować go w lepszym kształcie?
komar