Importy nieprzerywające pracy w IntelliJ

Do niedawna nienawidziłem przeklejania kodu do IDE. Nie dlatego, żebym uważał, że prawdziwemu programiście nie wypada robić copy-paste ze Stack Overflow (to bzdura). Cierpiałem na samą myśl, że będę musiał dodać importy do wszystkich użytych w tym fragmencie klas, ponieważ normalnie w IntelliJ jest to niewygodne. Dobra wiadomość: istnieje wbudowana opcja, która pozwala ułatwić sobie życie — ale domyślnie jest wyłączona. W dalszej części pokażę, jak ustawić swoje IDE do wydajnej pracy z importami. Na samym końcu będzie krótka dygresja o modelach mentalnych dla programistów.

Problem

Kod przeklejamy często — ze Stacka, z tutoriala biblioteki, której próbujemy zacząć używać w projekcie, z innej wersji kodu (np. cofając tylko fragment pliku do starszej wersji). Rzeczy, które robimy często, powinny być dobrze zoptymalizowane. Tymczasem nie znając IDE postępujemy tak: wklejamy, widzimy podświetlone na czerwono nazwy, które aktualnie nie są znane, ale nie mamy sposobu załatwiania sprawy jedna klasa po drugiej. Niewygodne jest, że trzeba ustawić kursor dokładnie wewnątrz czerwonej nazwy (nie wystarczy być w tej samej linijce), wcisnąć Alt+Enter i czekać, co dalej:

  1. jeśli jest jedna klasa do wyboru, IDE jej użyje bez pokazywania dodatkowych okienek
  2. jeśli nie, to zobaczymy listę opcji, z której musimy wybrać „Import class”, pokaże się kolejna lista, na której musimy wybrać poprawną klasę.

Trudno przeskakiwać szybko od przypadku do przypadku, bo jest ryzyko, że w pośpiechu klikniemy inną opcję niż trzeba.

Importy przeszkadzają nie tylko przy przeklejaniu. Jeśli czujemy flow i przelewamy szybko koncepcję z głowy na ekran, niezaimportowana klasa uniemożliwi nam skorzystanie z code completion — wpisujemy kropkę, przygotowani na wpisanie nazwy metody, a tu żadna lista metod się nie pokazuje. Wtedy albo pamiętamy dobrze, jak nazywa się potrzebna metoda, jakiego typu jest każdy z jej parametrów i piszemy dalej z pamięci (powodzenia przy złożonych collectorach z java.util.stream!), albo wracamy o kilka znaków, wprowadzamy import, przesuwamy kursor na koniec linii i piszemy dalej. W taki sposób możemy wdepnąć na kilka importowych min zanim zdołamy przelać jedną myśl na ekran. Ile czasu w stosunku do pracy kreatywnej zajmie nam porządkowanie importów?

Problem z importami wprowadza takie zamieszanie i wymaga na tyle uwagi, że staje się context switchem — wybija nas z rytmu. Badania ludzkiego mózgu mówią wyraźnie: nie jesteśmy stworzeni do multitaskingu, zmiana kontekstu jest bardzo kosztowna i długo trwa powrót do pracy na najwyższym poziomie. Jeden głupi import może sprawić, że zapomnimy, co właściwie chcieliśmy napisać; zapomnimy przez nieistotne pierdoły, bo importy nie są najistotniejszą częścią naszego kodu, co zauważyli dawno twórcy IDE, zwijając domyślnie listę importów, tak że zazwyczaj jej nie oglądamy.

Inną sytuacją, gdy warto mieć tę sprawę dobrze ogarniętą, jest live coding. Uzupełnianie importów nie tylko zabierze nam cenny czas prezentacji, może też zniecierpliwić widownię, a ryzyko zgubienia przez nas wątku jest jeszcze wyższe, niż gdy kodujemy we własnym tempie przy biurku nieobserwowani przez nikogo.

Ok, co w takim razie chcielibyśmy osiągnąć? Ludziom nie idzie za dobrze skupianie się na kilku rzeczach na raz. W związku z tym, gdy przykładowo tworzymy kod, powinniśmy najpierw starać się zapisać ogólną ideę, a nie koncentrować się na nazwach. Jeśli mamy problem z nazwą, lepiej wpisać cokolwiek (metoda”foo”), a za moment, gdy ogólny kształt kodu będzie widoczny, wrócić i skupić się tylko na nazwach (tu dobry model postępowania). To nie jest żaden wynalazek, tworzenie kodu ma wiele wspólnego z tworzeniem tekstów dla ludzi, a tam polecane są podobne zasady, np. żeby pisząc nie zatrzymywać się na poprawę błędów pisowni. Wracając do importów, najlepiej byłoby, gdyby nie wchodziły nam w drogę w trakcie kodowania, a poprawki w nich dało się zrobić hurtowo potem, szybko i sprawnie.

Poprawki hurtem

Klawisz F2 w IntelliJ uruchamia akcję Navigation » Next Highlighted Error, co umożliwia nam przeskakiwanie z kursorem od jednego brakującego importu do drugiego i naprawianie ich po kolei.

Rozwiązanie automatyczne

W ustawieniach Idei: Editor » General » Auto Import znajdziemy pozycję Add unambiguous imports on the fly, domyślnie wyłączoną. Każdy język ma niezależny checkbox o takiej nazwie: żeby znaleźć Kotlina trzeba przewinąć prawą część okna do końca. Na moim komputerze ta funkcja działa naprawdę szybko: nawet spory wklejony fragment kodu przestaje być „czerwony” w czasie poniżej sekundy. Nie pomoże jednak, gdy na classpathie mamy więcej niż jedną klasę o danej nazwie — wtedy niestety wracamy do tradycyjnego poprawiania z Alt+Enter klasa po klasie. Czy jest to w takim razie jakiekolwiek usprawnienie?

Okno ustawień Editor » General » Auto Import dla Javy

Tak, jeśli włożymy trochę pracy w uporządkowanie ustawień importu. W tym samym oknie ustawień znajduje się lista „Exclude from import and completion”. Widoczny opis mówi sam za siebie — dodajemy klasę albo początkowy fragment pakietu (jdk.nashorn wyklucza zarówno jdk.nashorn jak i jdk.nashorn.internal itp.) i Idea przestanie uznawać dopasowane klasy jako kandydatów do importu.

Osobiście nigdy nie dodaję elementów do tej listy z okna ustawień, uważam, że stratą czasu jest robienie tego na zapas. Uzupełniam listę w miarę potrzeb przy pracy z kodem — zawsze, gdy Alt+Enter nie dodaje mi natychmiastowo importu, przeglądam wyświetlonych kandydatów, zazwyczaj tylko jedna propozycja ma sens, więc resztę dodaję do czarnej listy.

'Exclude from auto-import' przy pracy z kodem

Nie da się wyeliminować wszystkich możliwych przypadków importu z więcej niż jednym kandydatem, ale nie jest to konieczne. Działa tu zasada Pareto i już krótka czarna lista może robić ogromną różnicę: w większości sytuacji „imports on the fly” zadziała bez potrzeby naszej interwencji. Trochę czasu zajęło mi odzwyczajenie się od stresu związanego z nieznalezionymi nazwami i zauważenie, że sprawa rozwiązuje się od teraz sama, a ja nie muszę myśleć na zapas o importach. Dajcie też sobie czas na zmianę przyzwyczajeń i dostrzeżenie pozytywnych zmian.

Ograniczeniem nie do przeskoczenia są statyczne importy. „Add unambiguous imports on the fly” nie doda ich automatycznie i nie ma obecnie sposobu, żeby zmusić do tego IntelliJ. Jeśli wklejamy kod to zostaje nam jedynie skorzystanie z klawisza F2 i dodania importów hurtem. Jeśli natomiast piszemy kod „z palca” to rozwiązaniem, które sprawdza się u mnie, jest zaczęcie od kodu bez statycznych importów. Czyli nie wpisuję:

when(mock.someMethod("some arg")).thenReturn("foo");

lecz:

Mockito.when(mock.someMethod("some arg")).thenReturn("foo");

Zazwyczaj jeszcze zanim dojdę do pierwszego nawiasu zamykającego, Idea w tle dodaje import do klasy Mockito i po wprowadzeniu kropki dostaję podpowiedź składni, przez co nie muszę pisać całego „thenReturn”. Gdy skończę przepisywać „myśl” na ekran, mogę wrócić kursorem do metody „when” i przez Alt+Enter wprowadzić statyczny import.

Jakie importy wyłączyć

Staram się być bezlitosny przy tworzeniu czarnej listy. Pierwszy odruch miałem taki, żeby w razie wątpliwości nie skreślać klasy, bo a nuż w kolejnym projekcie użyję bezpośrednio o tej tu biblioteki, kod będzie się świecił na czerwono, IDE nie będzie w stanie tego naprawić, ja nie będę pamiętał, gdzie w ustawieniach Idei jest ta cholerna czarna lista, albo kolejna wersja Idei umieści tę listę gdzieś w innym miejscu ustawień… Generalnie, że stracę więcej czasu próbując zaimportować jedną feralną klasę niż wcześniej zaoszczędziłem. Nic takiego nie nastąpiło.

Na mojej czarnej liście jest m.in. Joda Time, bo w obecnych czasach nikt nie powinien już używać tej biblioteki, a psuje pracę z prawie każdą klasą ze współczesnego API java.time.

Często wciągam na czarną listę całe pakiety zamiast pojedynczych klas. W zasadzie bez większego zastanowienia warto wykosić każdy pakiet z „internal” w nazwie, jaki zauważymy: jeśli w propozycjach do importu wyskakuje klasa my.lib.internal.collection.ArrayList, do czarnej listy dodajemy my.lib.internal. Oprócz tego mam tam na przykład:

  • io.netty.util.internal
  • jdk.jfr
  • jdk.nashorn.internal
  • junit.framework (przestarzałe klasy psujące próby importu z JUnita 4)
  • org.apache.catalina.Lifecycle (embedded Tomcat wciągany przez Spring Boot)
  • org.apache.el.stream.Optional (też embedded Tomcat)
  • org.graalvm
  • org.jboss.logging.MDC (jeśli jeszcze raz wyskoczy mi jakaś klasa z org.jboss.logging, to wyłączę cały pakiet)
  • sun

Ciekawa jest pozycja sun. Drugi screenshot pokazuje, że Idea pozwala na dodanie do czarnej listy co najmniej dwuczłonowego pakietu i nie da się „wyklikać” z edytora takiej jednoczłonowej pozycji na czarnej liście. Rozumiem, że to zabezpieczenie przed przypadkowym błędem, ale akurat w przypadku wewnętrznych klas Javy jestem 100% pewien, że nie będę ich importował — musiałem wejść do okna ustawień i zmienić sun.cośtam na sun. Jak widać po podwójnym wpisie dla jdk. nie jestem w tym wyjątkowo konsekwentny — 2 razy dołożyłem klasę z jdk. do czarnej listy i wystarczyło, więc nie wracałem więcej do sprawy.

Czemu tyle pracy nad czarną listą

Nie jestem w stanie podzielić się całą moją czarną listą — nie ma opcji eksportu/importu. IntelliJ na starcie nie dostarcza użytecznej listy, pewnie zakładając, że lepiej, żeby nowy użytkownik stracił odrobinę czasu na walkę z nadmiarem klas do importu niż ogromną ilość czasu na znajdowanie przyczyny, że potrzebna mu klasa nie jest widoczna. Szkoda, ale rozumiem.

Chciałbym zwrócić uwagę na inną kwestię: skąd tyle pakietów „internal” (jak Netty) i Joda Time na czarnej liście? Są to pakiety z bibliotek, które nic mnie nie obchodzą w moim projekcie: do czasu mam java.time, Netty nie używam bezpośrednio, bo abstrakcję na serwer zapewnia mi Spring. To nie są moje bezpośrednie zależności, ale mimo to Idea sugeruje mi je jako importy. Czy IntelliJ robi coś źle?

W pewnym uproszczeniu można powiedzieć, że winny jest Maven, a ściślej to, że dużo bibliotek korzysta z Mavena do budowania. Mój projekt używa klas z jakiejś biblioteki, a ta z kolei ma zależność na etapie kompilacji do Joda Time. Czyli mam transitive compile dependency do Joda Time i słusznie Idea sugeruje mi import z Joda Time, skoro znajduje się na moim classpathie używanym przy kompilacji źródeł.

Dużo lepiej taką sytuację da się zamodelować w Gradle’u, który odróżnia zależności typu api i implementation, generując lepsze POM-y niż sam Maven. Używana w moim projekcie biblioteka mogłaby deklarować zależność od Joda Time jako typu implementation, wtedy Joda Time nie byłaby obecna na moim compile classpath, ale dopiero w runtime classpath, a IntelliJ nie podpowiada klas widocznych jedynie w runtimie.

Póki co sytuacja jest jaka jest i trzeba się do niej dostosować. Warto jednak mieć w głowie model zależności proponowany przez twórców Gradle’a: pozwala wyjaśnić, czemu mogę bezpiecznie wciągnąć na czarną listę Joda Time, mimo że jest na moim compile classpath. Mogę tak zrobić, ponieważ nie jest to moja bezpośrednia zależność i tylko przez niedoskonałość narzędzi wyciekła poza runtime classpath.

Pozostaje mieć nadzieję, że więcej popularnych bibliotek pójdzie drogą JUnita i zamieni Mavena na poprawnie używanego Gradle’a. Im więcej, tym mniej niechcianych klas pojawi się w podpowiedziach naszych IDE.

Za dużo importów

Do tej pory zajmowaliśmy się sytuacją, kiedy kod nie działał, bo brakowało jakiegoś importu. Czasem możemy wpaść w kłopoty przez nadmiar importów — wtedy, gdy sięgamy do nieistniejącej klasy. Możemy np. używać w jednej z metod jakiejś biblioteki, następnie wkleić inną implementację tej metody, która już tej biblioteki nie potrzebuje i wyrzucić bibliotekę z zależności. Zostajemy z plikiem źródłowym, który nie kompiluje się ze względu na import (nieważne, że nieużywany). Do takiej sytuacji możemy też doprowadzić pechowym merge’em ze zdalnym branchem, gdzie ktoś inny skasował klasę, której my użyliśmy we własnym kodzie.

Wielu programistów Javy może nie widzieć tu problemu, ponieważ w takiej sytuacji użycie skrótu Ctrl+Alt+O czyli Optimize imports usuwa nasz nieużywany import nieznanej klasy i wszystko znowu się kompiluje. Nie musimy w zasadzie myśleć o „złym” imporcie i odrywać się od pracy. Tyle, że ta sama sytuacja urasta do poważniejszej przeszkody w przypadku, gdy pracujemy z kodem w Kotlinie — tam nie ma sposobu na automatyczne pozbycie się felernego importu. Opcja Optimize imports jest dostępna w Kotlinie, ale nie radzi sobie z nieistniejącymi klasami. Dopóki JetBrains nie naprawi KT-31331 (który wisi otwarty od maja 2019), musimy tracić czas na rozwijanie importów i ręczne kasowanie tych czerwonych.

Programista jako pisarz (offtopic)

Wróćmy na chwilę do pisania kodu bez zatrzymywania się, by poprawić importy oraz pisania tekstu bez zatrzymywania się, by poprawić literówki. Andy Hunt w książce Pragmatic Thinking and Learning zwraca uwagę na niedocenianie tworzenia dobrych metafor systemu informatycznego, jaki rozwijamy. Mam wrażenie, że brakuje nam też dobrego modelu mentalnego naszego zawodu. Informatyczna popkultura (np. starania firm szukających programistów) wbija do głowy obraz programisty jako superbohatera albo ninja. Jak przydatny to model mentalny? Mam zachowywać się jak superbohater, robiąc co mi się podoba i nie przejmując się innymi, bo ja wiem najlepiej, co uratuje świat?

Bardziej przydatnym modelem mentalnym może być programista-pisarz: ktoś, kogo praca nie podlega prymitywnemu mierzeniu jak praca budowlańca kładącego cegły, kto potrzebuje wpaść w nieprzerywany przez nic flow, żeby stworzyć materiał odpowiedniej jakości, kogo zadaniem jest pisanie rzeczy łatwych do odbioru przez innych. Korzyść z bardziej adekwatnego modelu mentalnego płynie z tego, że możemy rozwiązania z jednej rodziny problemów przenieść do innej rodziny problemów, jeśli tylko zauważymy, że w pewnym stopniu (to wciąż tylko model) są podobne. Mówiąc prościej, możemy podpatrywać rozwiązania dobrze sprawdzające się w pracy ze słowem pisanym, gdzie ludzie mieli wiele więcej lat niż w informatyce na przetestowanie w boju różnych best practices. Naśladowanie podejścia pisarzy do literówek jest przykładem, jakie korzyści możemy wynieść.

Kwestia programisty-ninja i programisty-pisarza to jeden z głównych tematów mojej prezentacji Problem sprytnego programisty. O tym, że głębokim nieporozumieniem jest estymowanie pracy umysłowej (na przykład programowania powyżej poziomu poprawek kosmetycznych) pisze choćby Cal Newport w książce Deep Work.