Czemu Service service = new ServiceImpl() jest zazwyczaj głupie

Czy rzeczywiście interfejsy powinny być zawsze i wszędzie? Jakie są zalety i wady obu rozwiązań? Czy w przypadku obiektów zarządzanych przez kontener Springa jest jakaś różnica?

Jak zaczynałem swoją przygodę z Javą i zacząłem poznawać Springa to jedną z głównych zasad jakie wbiły mi różnego rodzaju poradniki było to, że zawsze gdy tworzę klasę, kontroler czy cokolwiek innego to najpierw powinienem utworzyć interfejs, a dopiero później implementację tego interfejsu.

Każdy pewnie wie dobrze o co chodzi, mowa o konstrukcji:

public interface Service {
    void save(Something something);
}

public class ServiceImpl implements Service {
    @Override
    public void save(Something something){
        //code
    }
}

Przekonywały mnie argumenty o łatwiejszym testowaniu, o czymś tam jeszcze i inne bzdety. No i jakoś ciekawość moja na tamte czasy została zaspokojona. Skrobałem sobie swoje projekciki mniejsze lub większe i rzeczywiście stosowałem się do tej konwencji. Wszyscy szczęśliwi.

Do czasu, aż nie trafiłem do większego projektu.

IDE zaczyna przeszkadzać?

I głownie to skłoniło mnie do zgłębienia sensowności tej konwencji. Przechodzenie po klasach stało się męczące.

W obie strony.

Ciężko się dostać do implementacji tej klasy w tradycyjny sposób – w IntelliJ zawsze mogłem to zrobić przytrzymując CTRL + klik myszą na klasę. Tutaj, co w pełni zrozumiałe, dostaję się do interfejsu. No i fakt – może rzeczywiście przesadzam, bo również mogę użyć innej magicznej kombinacji klawiszy i dostać się od razu do implementacji. Ale z drugiej strony – raz się dostaję przez CTRL + klik, a innym razem przez inna magiczna kombinacja. W momencie gdy chcę szybko poruszać się po kodzie to nie tak łatwo od razu odgadnąć czy dana rzecz jest tylko klasą czy interfejsem. Chociaż tutaj rozwiązaniem może być jakaś forma notacji węgierskiej (heh).

IDE sygnalizuje, że klasa nie jest używana nigdzie – nie prawda!

Niestety w drugą stronę jest gorzej. Chcę sprawdzić w którym miejscu jest wykorzystywany serwis – nie mogę. Najpierw muszę przejść do interfejsu, a dopiero później szukać po interfejsie. Słabo.

Ktoś może pomyśleć – wyszukaj w projekcie. No jak mam wyszukać jak nazwa ServiceImpl istnieje tylko w jednym miejscu – w klasie ServiceImpl i ewentualnie w jego konstruktorze.

Pytam wujka Google

No i zaczynam szukać sensownych powodów do używania tej konwencji (jak uczą poradniki) wszędzie i zawsze. No i znajduję jeszcze kilka pytań, które wymuszają na nas przemyślenie tego „wzorca”

That is not only bloat (and the opposite of „Convention Over Configuration” idea), but it causes real damage:
– Imagine you get another implementation (thats the whole point of an interface) – how would you name it?
– It doubles the amount of artifacts – and significantly increases the „complexity”
– It is really funny to read the javadocs on the interface and the impl – another redundancy

Źródło: SERVICE S = NEW SERVICEIMPL() – WHY YOU ARE DOING THAT?

Jeszcze kolejne ciekawe spostrzeżenia

Impl is noise
In fact, naming an implementing class with the Impl suffix is a tautology. Every concrete class is of course an implementation of something. It’s like naming the service interface IPersonService, the I prefix brings nothing more than noise to the name.

Impl is meaningless
Naming a class with the Impl suffix is like telling I don’t know how to name it. If you can find a simple name for your implementing class, it’s a sign of code smell. Things that can’t be named properly are either too complex or doing too many things, thus usually breaking the Single Responsibility principle.

I na koniec idealne podsumowanie

I strongly agree with this: there is no point of creating an interface for a service which has only a single known implementation. Decoupling for the sake of decoupling has no sense.

Źródło: IMPL CLASSES ARE EVIL

Argumenty ZA ServiceImpl

Szukam dalej.. Wszystkie argumenty, które znajduję mogę sprowadzić do tych poniżej.

1. A co, jeżeli w przyszłości…

Basic idea behind having this kind of architecture is little different than just spring convention.
Lets say tomorrow you decide, you dont want to have single application for both projects, and go into one deployment for webapp and another for service Example UserService WebApp

Źródło: Code architecture of service interface and service impl classes spring

A co, jeżeli w przyszłości… i tutaj prawdę mówiąc nawet nie potrafię sobie wyobrazić realnej sytuacji kiedy to miałoby mnie uratować. Przecież jeżeli rzeczywiście w przyszłości będzie potrzeba, aby dopisać ten interfejs to co za problem?

To jest ogromna pułapka w którą często wpadamy jako programiści – robienie czegoś „na wszelki wypadek”. Stosując regułę KISS (Keep it Simple, Stupid) powinniśmy unikać takich rozwiązań.

The KISS principle states that most systems work best if they are kept simple rather than made complicated; therefore, simplicity should be a key goal in design, and unnecessary complexity should be avoided.

Źródło: Wikipedia – KISS

2. Testowanie.

Tutaj rzeczywiście może, ale nie musi zajść potrzeba takiej architektury.

Kiedy? Mało jest sytuacji kiedy serwis będziemy implementować jakoś inaczej w celach napisania testów. Wiadomo – są przypadki kiedy może stać się to nieuniknione, ale mowa tutaj o szczególnych przypadkach. Każde inne rozwiązanie możemy zastąpić mockowaniem takiego obiektu.

3. Impl jest wydajniejsze.

No to dotarliśmy chyba do części, która jest najkonkretniejsza.

Częstym argumentem stojącym za stosowaniem konwencji Impl jest to, że tworzony jest inny, lepszy rodzaj proxy przez kontener Springa. No i rzeczywiście tak jest.

JDK Reflection API

@Service
public class AccountServiceImpl  implements AccountService {
 @PersistenceContext
 private EntityManager entityManager;

 @Transactional
 public void create(Account account) {
 entityManager.persist(account);
 }
}

Przed uruchomieniem aplikacji wygląda to dokładnie tak

Natomiast w momencie uruchamiania aplikacji tworzona jest nowa klasa, zwana proxy.

Jest to dynamiczne proxy generowane przez Springa przy użyciu JDK Reflection API.

CGLIB

W przypadku gdy nie mamy żadnego interfejsu to proxy jest tworzone za pomocą CGLIB. W momencie startu aplikacji utworzona zostaje nowa klasa, która dziedziczy po naszej klasie nadpisując nasze metody.

Widać to dosyć ładnie gdy spróbujemy debbugerem sprawdzić nasze wywołanie.

wywołanie na obiekcie kontrolowanym przez Cglib

O czym warto pamiętać w przypadku tego typu proxy:

  • metody oznaczone final nie mogą być nadpisane,
  • konstruktor naszej klasy zostaje wywołany dwukrotnie – jeżeli macie tam jakieś czary to warto o tym pamiętać (to też znak, żeby te czary może przenieść w inne miejsce, bo konstruktor raczej nie nadaje się do takich rzeczy),
  • CGLIB to zewnętrzna zależność.

Dochodzi jeszcze kwestia wydajności. Odeślę tutaj do tego artykułu w którym opisane jest to bardzo dobrze.

tl;dr: CGLIB może być szybsze.

Podsumowując

Pominąłem całkowicie sytuacje gdy interfejsy są konieczne.

W innych przypadkach pozostaję dalej zwolennikiem zasady: jeżeli masz jedną implementację i nie widzisz sensownej potrzeby tworzenia interfejsu to go nie twórz.