Co każdy programista Javy wiedzieć powinien



Wszystko czego potrzebujesz to Java

Pracując nad pierwszą poważną wersją Visual Studio, zespół Microsoft przedstawił światu trzy postacie programistów: Morta, Elvisa i Einsteina. Mort był deweloperem oportunistycznym, który robił szybkie poprawki i wymyślał rzeczy na bieżąco. Elvis był pragmatycznym programistą, budującym rozwiązania na wieki, ucząc się w pracy. Einstein był programistą paranoicznym, mającym obsesję na punkcie zaprojektowania najbardziej wydajnego rozwiązania i wymyślenia wszystkiego przed napisaniem swojego kodu. Po stronie Javy religijnego podziału języków programowania śmialiśmy się z Mortsa i chcieliśmy być Einsteinami budującymi frameworki, aby upewnić się, że Elvisowie napisali swój kod we "właściwy sposób". To był świt ery frameworków i jeśli nie byłeś biegły w najnowszym, najlepszym mapowaniu relacyjnym obiektów i inwersji frameworka kontrolnego, nie byłeś porządnym programistą Java. Biblioteki się rozrosły do frameworków z preskryptowanymi architekturami. A kiedy te frameworki stały się ekosystemami technologicznymi, wielu z nas zapomniało o małym języku, który mógł - Javie. Java to świetny język, a jego biblioteka klas ma coś na każdą okazję. Chcesz pracować z plikami? java.nio cię chroni. Bazy danych? java.sql to miejsce, do którego należy się udać. Prawie każda dystrybucja Javy ma nawet pełnowymiarowy serwer HTTP, nawet jeśli musisz zejść z gałęzi o nazwie Java i na com.sun.net.httpserver. W miarę jak nasze aplikacje zmierzają w kierunku architektur bezserwerowych, w których jednostki wdrożeniowe mogą być pojedynczymi funkcjami, korzyści, jakie uzyskujemy z frameworków aplikacji, maleją. Dzieje się tak, ponieważ prawdopodobnie będziemy spędzać mniej czasu na rozwiązywaniu problemów technicznych i infrastrukturalnych, koncentrując nasze wysiłki programistyczne na możliwościach biznesowych realizowanych przez nasze programy. Jak ujął to Bruce Joyce:

Co jakiś czas musimy wymyślać koło na nowo, nie dlatego, że potrzebujemy wielu kół; ale ponieważ potrzebujemy wielu wynalazców.

Wiele z nich podjęło się budowy ogólnych ram logiki biznesowej, aby zmaksymalizować ponowne wykorzystanie. Większość zawiodła, ponieważ tak naprawdę nie ma żadnych ogólnych problemów biznesowych. Robienie czegoś wyjątkowego w określony sposób odróżnia jedną firmę od drugiej. Dlatego masz gwarancję, że napiszesz logikę biznesową w prawie każdym projekcie. W imię wymyślenia czegoś ogólnego i wielokrotnego użytku można pokusić się o wprowadzenie silnika reguł lub podobnego. W ostatecznym rozrachunku konfigurowanie silnika reguł to programowanie, często w języku gorszym od Javy. Dlaczego nie spróbować po prostu napisać Javy? Zdziwisz się, że wynik końcowy będzie łatwy do odczytania, co z kolei sprawia, że kod jest łatwy w utrzymaniu - nawet przez programistów nie korzystających z Javy. Dość często okazuje się, że biblioteka klas Java jest nieco ograniczona i możesz potrzebować czegoś, co sprawi, że praca z datami, sieciami lub czymś innym będzie nieco wygodniejsza. W porządku. Skorzystaj z biblioteki. Różnica polega na tym, że będziesz teraz korzystać z tej biblioteki, ponieważ pojawiła się konkretna potrzeba, a nie dlatego, że była częścią stosu, którego zawsze używałeś. Następnym razem, gdy przyjdzie ci do głowy pomysł na mały program, obudź swoją znajomość biblioteki klas Java z hibernacji zamiast sięgania po rusztowanie JHipster. Hipsteryzm jest passé; prowadzenie prostego życia jest tam, gdzie jest teraz. Założę się, że Mort kochał proste życie.

Testy zatwierdzające

Czy kiedykolwiek napisałeś asercję testową z obojętnym lub pustym oczekiwaniem? Coś takiego:

attachEquals("", functionCall())

Gdzie functionCall zwraca ciąg i nie wiesz dokładnie, jaki powinien być ten ciąg, ale wiesz, że jest prawidłowy, gdy go zobaczysz? Kiedy uruchamiasz test po raz pierwszy, oczywiście kończy się to niepowodzeniem, ponieważ functionCall zwraca ciąg, który nie jest pusty. (Możesz mieć kilka prób, dopóki zwracana wartość nie będzie wyglądała poprawnie.) Następnie wklej tę wartość zamiast pustego ciągu w asercieEquals. Teraz test powinien zdać. Wynik! To właśnie nazwałbym testami zatwierdzającymi. Kluczowym krokiem jest tutaj decyzja, że dane wyjściowe są prawidłowe i użyjesz ich jako oczekiwanej wartości. "Zatwierdzasz" wynik - wystarczy go zachować. Spodziewam się, że zrobiłeś tego rodzaju rzeczy, nie myśląc o tym naprawdę. Być może nazywasz to inną nazwą: jest to również nazywane testowaniem migawek lub testowaniem złotego wzorca. Z mojego doświadczenia wynika, że jeśli masz platformę testową zaprojektowaną specjalnie do jej obsługi, wiele rzeczy się układa i testowanie w ten sposób staje się łatwiejsze. W przypadku klasycznego frameworka do testowania jednostkowego, takiego jak JUnit, może to być trochę bolesne aby zaktualizować te oczekiwane ciągi, gdy się zmienią. W końcu wklejasz rzeczy w kodzie źródłowym. Za pomocą narzędzia do testowania zatwierdzania zatwierdzony ciąg znaków jest zamiast tego przechowywany w pliku. To natychmiast otwiera nowe możliwości. Możesz użyć odpowiedniego narzędzia do porównywania, aby przejść przez zmiany i scalić je jeden po drugim. Możesz uzyskać podświetlanie składni dla ciągów JSON i tym podobnych. Możesz wyszukiwać i zastępować aktualizacje w kilku testach w różnych klasach. Jakie są więc dobre sytuacje do testów zatwierdzających? Tu jest kilka:

Kod bez testów jednostkowych, który trzeba zmienić

Jeśli kod jest w produkcji, wszystko, co robi, jest domyślnie uważane za poprawne i może zostać zatwierdzone. Trudna część tworzenia testów przeradza się w problem znajdowania szwów i wycinania kawałków logiki, które zwracają coś interesującego, co można zatwierdzić.

REST API i funkcje, które zwracają JSON lub XML

Jeśli wynik jest dłuższym ciągiem, to przechowywanie go poza kodem źródłowym jest wielką wygraną. Zarówno JSON, jak i XML można sformatować ze spójną białą przestrzenią, dzięki czemu można je łatwo porównać z oczekiwaną wartością. Jeśli istnieją wartości w JSON lub XML, które bardzo się różnią - na przykład daty i godziny - może być konieczne sprawdzenie ich osobno przed zastąpieniem ich stałym ciągiem i zatwierdzeniem reszty.

Logika biznesowa budująca złożony obiekt zwrotu

Zacznij od napisania klasy Printer, która może przyjąć złożony obiekt zwracany i sformatować go jako ciąg. Pomyśl o paragonie, recepcie lub zamówieniu. Każdy z nich może być dobrze reprezentowany jako czytelny dla człowieka ciąg wielowierszowy. Twoja drukarka może wybrać drukowanie tylko podsumowania - przechodź przez wykres obiektu, aby wyciągnąć odpowiednie szczegóły. Twoje testy będą następnie ćwiczyć różne reguły biznesowe i używać drukarki do tworzenia ciągu do zatwierdzenia. Jeśli masz niekodującego właściciela produktu lub analityka biznesowego, może on nawet przeczytać wyniki testów i sprawdzić, czy są poprawne.

Jeśli masz już testy, które tworzą asercje dotyczące ciągów dłuższych niż jedna linia, polecam dowiedzieć się więcej o testowaniu zatwierdzającym i zacząć używać narzędzia, które je obsługuje.

Rozszerz Javadoc o AsciiDoc

Programiści Java znają już Javadoc. Ci, którzy są w pobliżu od dłuższego czasu, pamiętają, jakie to było transformacyjne, ponieważ Java stała się pierwszym głównym językiem integrującym generator dokumentacji bezpośrednio z kompilatorem i standardowym łańcuchem narzędzi. Wynikająca z tego eksplozja dokumentacji API (nawet jeśli nie zawsze świetna lub dopracowana) przyniosła nam wszystkim ogromne korzyści, a trend rozprzestrzenił się na wiele innych języków. Jak donosi James Gosling, Javadoc początkowo budziła kontrowersje, ponieważ "dobry pisarz technologii mógłby wykonać o wiele lepszą robotę" - ale istnieje znacznie więcej interfejsów API niż pisarzy technologii, którzy mogą je dokumentować, a wartość posiadania czegoś, co jest powszechnie dostępne, jest dobrze ugruntowana. Czasami potrzebujesz czegoś więcej niż dokumentacji API, choć znacznie więcej, niż możesz zmieścić na stronach przeglądu pakietów i projektów, które oferuje Javadoc. Przewodniki i przewodniki zorientowane na użytkownika końcowego, szczegółowe informacje o architekturze i teorii, objaśnienia łączenia ze sobą wielu komponentów… żadne z nich nie pasuje wygodnie do języka Javadoc. Więc czego możemy użyć, aby zaspokoić te inne potrzeby? Odpowiedzi zmieniały się z biegiem czasu. FrameMaker był przełomową, wieloplatformową potęgą dokumentacji technicznej GUI w latach 80-tych. Javadoc zawierał nawet Doclet MIF do generowania atrakcyjnej drukowanej dokumentacji API za pomocą FrameMaker - ale pozostała tylko szczątkowa wersja Windows. DocBook XML oferuje podobną moc strukturalną i łączącą, z otwartą specyfikacją i wieloplatformowym łańcuchem narzędzi, ale jego surowy format XML jest niepraktyczny w bezpośredniej pracy. Nadążanie za narzędziami do edycji stało się drogie i nużące, a nawet te dobre wydawały się niezgrabne i utrudniały pisanie. Jestem podekscytowany, że znalazłem lepszą odpowiedź: AsciiDoc oferuje całą moc DocBook w łatwym do pisania (i odczytywaniu) formacie tekstowym, w którym robienie prostych rzeczy jest trywialne, a robienie złożonych rzeczy jest możliwe. Większość konstrukcji AsciiDoc jest tak samo czytelna i dostępna, jak inne lekkie formaty znaczników, takie jak Markdown, które stają się znane za pośrednictwem internetowych forów dyskusyjnych. A kiedy chcesz mieć ochotę, możesz dołączyć złożone równania w formatach MathML lub LaTeX, sformatowane listy kodów źródłowych z ponumerowanymi i połączonymi objaśnieniami do akapitów tekstu, różnego rodzaju bloki upomnienia i nie tylko. AsciiDoc został wprowadzony wraz z implementacją Pythona w 2002 roku. Obecną oficjalną implementacją (i stewardem języka) jest Asciidoctor, wydany w 2013 roku. Jego kod Ruby może być również uruchamiany w JVM przez AsciidoctorJ (z wtyczkami Maven i Gradle) lub przeniesione do JavaScript, z których wszystkie działają dobrze w środowiskach ciągłej integracji. Gdy potrzebujesz zbudować całą witrynę powiązanej dokumentacji (nawet z wielu repozytoriów), narzędzia takie jak Antora sprawiają, że jest to szokująco łatwe. Społeczność jest przyjazna i wspierająca, a obserwowanie jej wzrostu i postępów w ciągu ostatniego roku jest inspirujące. A jeśli ma to dla Ciebie znaczenie, proces standaryzacji formalnej specyfikacji AsciiDoc jest w toku. Lubię tworzyć bogatą, atrakcyjną dokumentację dla projektów, którymi się dzielę. AsciiDoc znacznie to ułatwił i dał mi tak szybkie cykle realizacji, że polerowanie i udoskonalanie tej dokumentacji stało się zabawą. Mam nadzieję, że znajdziesz to samo. I zataczając pełne koło, jeśli zdecydujesz się wejść na całość z AsciiDoc, istnieje nawet Doclet o nazwie Asciidoclet, który pozwala pisać Javadoc przy użyciu AsciiDoc!

Bądź świadomy otoczenia swojego kontenera

Istnieje niebezpieczeństwo konteneryzacji starszych aplikacji Java, jak jest, z ich starszą wirtualną maszyną Java (JVM), ponieważ ergonomia tych starszych maszyn JVM zostanie oszukana podczas uruchamiania w kontenerach Dockera. Kontenery stały się de facto mechanizmem pakowania środowiska uruchomieniowego. Zapewniają wiele korzyści: pewien poziom izolacji, lepsze wykorzystanie zasobów, możliwość wdrażania aplikacji w różnych środowiskach i nie tylko. Kontenery pomagają również zmniejszyć sprzężenie między aplikacją a podstawową platformą, ponieważ tę aplikację można spakować do przenośnego kontenera. Ta technika jest czasami używana do modernizacji starszych aplikacji. W przypadku Javy kontener osadza starszą aplikację Javy wraz z jej zależnościami, w tym starszą wersję JVM używanej przez tę aplikację. Praktyka konteneryzacji starszych aplikacji Java z ich środowiskami z pewnością może pomóc w utrzymaniu starszych aplikacji działających w nowoczesnej, obsługiwanej infrastrukturze poprzez oddzielenie ich od starszej, nieobsługiwanej infrastruktury. Jednak potencjalne korzyści z takiej praktyki wiążą się z własnym zestawem zagrożeń ze względu na ergonomię JVM. Ergonomia maszyny JVM umożliwia samodostrojenie maszyny JVM na podstawie dwóch kluczowych wskaźników środowiskowych: liczby procesorów i dostępnej pamięci. Dzięki tym metrykom maszyna JVM określa ważne parametry, takie jak używany odśmiecacz pamięci, sposób jego konfiguracji, rozmiar sterty, rozmiar ForkJoinPool i tak dalej. Obsługa kontenerów Linux Docker, dodana w JDK 8, aktualizacja 191, pozwala maszynie JVM polegać na cgroups systemu Linux w celu uzyskania metryk zasobów przydzielonych do kontenera, w którym działa. Każda maszyna JVM starsza niż ta nie jest świadoma, że działa w kontenerze i uzyska dostęp do metryk z systemu operacyjnego hosta, a nie z samego kontenera. A biorąc pod uwagę, że w większości przypadków kontener jest skonfigurowany do korzystania tylko z podzbioru zasobów hosta, maszyna JVM będzie polegać na niepoprawnych metrykach, aby się dostroić. To szybko prowadzi do niestabilnej sytuacji, w której kontener prawdopodobnie zostanie zabity przez hosta, gdy będzie próbował zużyć więcej zasobów niż jest dostępnych. Poniższe polecenie pokazuje, które parametry JVM są konfigurowane przez ergonomię JVM:

java -XX:+PrintFlagsFinal -wersja | grep ergonomiczny

Obsługa kontenerów JVM jest domyślnie włączona, ale można ją wyłączyć za pomocą flagi -XX:-UseContainerSupport JVM. Użycie tej flagi JVM w kontenerze z ograniczonymi zasobami (procesor i pamięć) pozwala obserwować i badać wpływ ergonomii JVM z obsługą kontenera i bez niej. Uruchamianie starszych maszyn JVM w kontenerach platformy Docker nie jest oczywiście zalecane. Ale jeśli jest to jedyna opcja, starsza JVM powinna być przynajmniej skonfigurowana tak, aby nie przekraczała zasobów przydzielonych do kontenera, w którym działa. Idealnym, oczywistym rozwiązaniem jest użycie nowoczesnej, obsługiwanej JVM (na przykład JDK 11 lub nowszej ), które nie tylko będą domyślnie uwzględniać kontenery, ale także zapewnią aktualne i bezpieczne środowisko uruchomieniowe.

Zachowanie jest "łatwe"; Stan jest trudny

Kiedy po raz pierwszy zapoznałem się z programowaniem obiektowym, niektóre z pierwszych nauczanych pojęć dotyczyły potrójnego polimorfizmu, dziedziczenia i enkapsulacji. I szczerze mówiąc, spędziliśmy sporo czasu próbując je zrozumieć i kodować. Ale, przynajmniej dla mnie, zbyt duży nacisk położono na dwa pierwsze, a bardzo mało na trzecią i najważniejszą ze wszystkich: hermetyzację. Enkapsulacja pozwala nam okiełznać rosnący stan i złożoność, które są stałe w dziedzinie tworzenia oprogramowania. Idea, że możemy internalizować stan, ukrywać go przed innymi komponentami i oferować tylko starannie zaprojektowaną powierzchnię API dla dowolnej mutacji stanu, jest podstawą projektowania i kodowania złożonych systemów informatycznych. Ale przynajmniej w świecie Javy nie udało nam się rozpowszechnić niektórych najlepszych praktyk dotyczących budowy dobrze hermetyzowanych systemów. Właściwości JavaBean w anemicznych klasach, które po prostu ujawniają stan wewnętrzny za pomocą metod pobierających i ustawiających, są powszechne, a dzięki architekturze Java Enterprise wydaje się, że spopularyzowaliśmy koncepcję, że większość - jeśli nie wszystkie - logiki biznesowej powinna być zaimplementowana w klasach usług. W ich ramach używamy getterów do wyodrębniania informacji, przetwarzania ich w celu uzyskania wyniku, a następnie umieszczania wyniku z powrotem w naszych obiektach za pomocą seterów. A kiedy budzą się błędy, przeglądamy pliki dziennika, używamy debuggerów i próbujemy dowiedzieć się, co dzieje się z naszym kodem w środowisku produkcyjnym. Dość "łatwo" jest wykryć błędy spowodowane problemami z zachowaniem: fragmenty kodu robiące coś, czego nie powinny robić. Z drugiej strony, gdy wydaje się, że nasz kod postępuje właściwie, a wciąż mamy błędy, staje się on znacznie bardziej skomplikowany. Z mojego doświadczenia najtrudniejsze do rozwiązania są błędy spowodowane niespójnym stanem. Osiągnąłeś w systemie stan, który nie powinien mieć miejsca, ale tak jest - NullPointerException dla właściwości, która nigdy nie miała mieć wartości null, wartość ujemna, która powinna być tylko dodatnia i tak dalej. Szanse na znalezienie kroków, które doprowadziły do takiego niespójnego stanu, są niskie. Nasze klasy mają powierzchnie, które są zbyt zmienne i zbyt łatwo dostępne: każdy fragment kodu, gdziekolwiek w systemie, może mutować nasz stan bez jakichkolwiek kontroli lub równowagi. Oczyszczamy dane wejściowe dostarczone przez użytkownika za pomocą frameworków walidacji, ale ten "niewinny" ustawiacz wciąż istnieje, pozwalając dowolnemu fragmentowi kodu na wywołanie go. I nawet nie będę omawiał prawdopodobieństwa, że ktoś użyje instrukcji UPDATE bezpośrednio w bazie danych, aby zmienić niektóre kolumny w encjach mapowanych w bazie danych. Jak możemy rozwiązać ten problem? Jedną z możliwych odpowiedzi jest niezmienność. Jeśli możemy zagwarantować, że nasze obiekty są niezmienne, a spójność stanów jest sprawdzana podczas tworzenia obiektu, nigdy nie będziemy mieli niespójnego stanu w naszym systemie. Ale musimy wziąć pod uwagę, że większość frameworków Java nie radzi sobie zbyt dobrze z niezmiennością, więc powinniśmy przynajmniej starać się zminimalizować zmienność. Posiadanie odpowiednio zakodowanych metod fabrycznych i konstruktorów może również pomóc nam osiągnąć ten minimalnie zmienny stan. Dlatego nie generuj swoich seterów automatycznie. Poświęć trochę czasu, aby o nich pomyśleć. Czy naprawdę potrzebujesz tego setera w swoim kodzie? A jeśli zdecydujesz, że tak, być może z powodu pewnych wymagań ramowych, rozważ użycie warstwy antykorupcyjnej do ochrony i walidacji swojego stanu wewnętrznego po tych interakcjach ustawiających.

Analiza porównawcza jest trudna - pomaga JMH

Analiza porównawcza na JVM, zwłaszcza mikrobenchmarking, jest trudna. Nie wystarczy wykonać pomiar w nanosekundach wokół połączenia lub pętli i gotowe. Musisz wziąć pod uwagę rozgrzewkę, kompilację HotSpot, optymalizacje kodu, takie jak eliminacja wbudowanego i martwego kodu, wielowątkowość, spójność pomiaru i inne. Na szczęście Aleksey Shipilëv, autor wielu wspaniałych narzędzi JVM, wniósł do OpenJDK JMH, Java Microbenchmarking Harness. Składa się z małej biblioteki i wtyczki do systemu budowania. Biblioteka udostępnia adnotacje i narzędzia do deklarowania testów porównawczych jako adnotowanych klas i metod Java, w tym klasy BlackHole, która wykorzystuje wygenerowane wartości w celu uniknięcia eliminacji kodu. Biblioteka oferuje również poprawną obsługę stanów w obecności wielowątkowości. Wtyczka systemu kompilacji generuje plik JAR z odpowiednim kodem infrastruktury do prawidłowego uruchamiania i mierzenia testów. Obejmuje to dedykowane fazy rozgrzewania, prawidłowe wielowątkowość, uruchamianie wielu widełek i uśrednianie w nich i wiele więcej. Narzędzie dostarcza również ważnych porad, jak korzystać z zebranych danych i ich ograniczeń. Oto przykład pomiaru wpływu wstępnego rozmiaru kolekcji:

public class MyBenchmark {
static final int COUNT = 10000;
@Benchmark
public List testFillEmptyList() {
List list = new ArrayList<>();
for (int i=0;i list.add(Boolean.TRUE);
}
return list;
}
@Benchmark
public List testFillAllocatedList() {
List list = new ArrayList<>(COUNT);
for (int i=0;i list.add(Boolean.TRUE);
}
return list;
}
}

Aby wygenerować projekt i go uruchomić, możesz użyć archetypu JMH Maven:

mvn archetype:generate \
-DarchetypeGroupId=org.openjdk.jmh \
-DarchetypeArtifactId=jmh-java-benchmark-archetype \
-DinteractiveMode=false -DgroupId=com.example \
-DartifactId=coll-test -Dversion=1.0
cd coll-test
# add com/example/MyBenchmark.java
mvn clean install
java -jar target/benchmarks.jar -w 1 -r 1

# JMH version: 1.21

# Warmup: 5 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.example.MyBenchmark.testFillEmptyList

Result "com.example.MyBenchmark.testFillEmptyList":
30966.686 ą(99.9%) 2636.125 ops/s [Average]
(min, avg, max) = (18885.422, 30966.686, 35612.643), stdev = 3519.152
CI (99.9%): [28330.561, 33602.811] (assumes normal distribution)
# Run complete. Total time: 00:01:45
REMEMBER: The numbers below are just data. To gain reusable insights, you
need to follow up on why the numbers are the way they are. Use profilers (see -prof,-lprof),
design factorial experiments, perform baseline and negative tests that provide experimental
control, make sure the benchmarking environment is safe on JVM/OS/HW level, ask for
reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.
Benchmark Mode Cnt Score Error Units
MyBenchmark.testFillAllocatedList thrpt 25 56786.708 ą 1609.633 ops/s
MyBenchmark.testFillEmptyList thrpt 25 30966.686 ą 2636.125 ops/

Widzimy więc, że nasza wstępnie przydzielona kolekcja jest prawie dwa razy szybsza niż domyślna instancja, ponieważ nie trzeba jej zmieniać podczas dodawania elementów. JMH to potężne narzędzie w twoim zestawie narzędzi do pisania poprawnych mikrobenchmarków. Jeśli uruchomisz je w tym samym środowisku, są nawet porównywalne, co powinno być głównym sposobem interpretacji ich wyników. Mogą być również wykorzystywane do celów profilowania, ponieważ zapewniają stabilne, powtarzalne wyniki. Aleksey ma o wiele więcej do powiedzenia na ten temat, jeśli jesteś zainteresowany.

Korzyści z kodyfikacji i zapewnienia jakości architektonicznej

Potok kompilacji ciągłej dostawy powinien być główną lokalizacją, w której uzgodnione właściwości architektoniczne dla aplikacji są skodyfikowane i wymuszane. Jednak te zautomatyzowane zapewnienia jakości nie powinny zastępować ciągłych dyskusji zespołu na temat standardów i poziomów jakości i zdecydowanie nie powinny być wykorzystywane do unikania komunikacji wewnątrz lub między zespołami. To powiedziawszy, sprawdzanie i publikowanie metryk jakości w ramach procesu kompilacji może zapobiec stopniowemu spadkowi jakości architektury, który w innym przypadku byłby trudny do zauważenia. Jeśli zastanawiasz się, dlaczego powinieneś przetestować swoją architekturę, znajdziesz na stronie motywacyjnej ArchUnit. Zaczyna się od znanej historii: pewnego razu architekt narysował serię ładnych diagramów architektonicznych, które ilustrowały elementy systemu i ich interakcje. Potem projekt się rozrósł i przypadki użycia stały się bardziej złożone, nowi programiści zrezygnowali, a starzy programiści odpadli. To ostatecznie doprowadziło do dodania nowych funkcji w dowolny sposób. Wkrótce wszystko zależało od wszystkiego, a każda zmiana mogła mieć nieprzewidywalny wpływ na każdy inny element. Jestem pewien , że wielu czytelników rozpozna ten scenariusz. ArchUnit to otwarta, rozszerzalna biblioteka do sprawdzania architektury kodu Java przy użyciu platformy testów jednostkowych Java, takiej jak JUnit lub TestNG. ArchUnit może sprawdzać cykliczne zależności i sprawdzać zależności między pakietami i klasami, warstwami i plastrami i nie tylko. Robi to wszystko, analizując kod bajtowy Javy i importując wszystkie klasy do analizy. Aby używać ArchUnit w połączeniu z JUnit 4, uwzględnij następującą zależność od Maven Central:

< dependency >
< groupId >com.tngtech.archunit< /groupId >
< artifactId >archunit-junit< /artifactId >
< version > 0.5.0< /version >
< scope >test< /scope >

< /dependency >W swej istocie ArchUnit zapewnia infrastrukturę do importowania kodu bajtowego Java do struktur kodu Java. Możesz to zrobić za pomocą ClassFileImporter. Możesz tworzyć reguły architektoniczne, takie jak "usługi powinny być dostępne tylko dla kontrolerów" za pomocą interfejsu API Fluent podobnego do DSL, który z kolei może być oceniany w odniesieniu do importowanych klas:

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
// …
@Test
public void Services_should_only_be_accessed_by_Controllers() {
JavaClasses classes =
new ClassFileImporter().importPackages("com.mycompany.myapp");
ArchRule myRule = ArchRuleDefinition.classes()
.that().resideInAPackage("..service..")
.should().onlyBeAccessed()
.byAnyPackage("..controller..", "..service..");
myRule.check(classes);
}
Extending the preceding example, you can also enforce more layer-based
access rules using this test:
@ArchTest
public static final ArchRule layer_dependencies_are_respected =
layeredArchitecture()
.layer("Controllers").definedBy("com.tngtech.archunit.eg.controller..")
.layer("Services").definedBy("com.tngtech.archunit.eg.service..")
.layer("Persistence").definedBy("com.tngtech.archunit.eg.persistence..")
.whereLayer("Controllers").mayNotBeAccessedByAnyLayer()
.whereLayer("Services").mayOnlyBeAccessedByLayers("Controllers")
.whereLayer("Persistence").mayOnlyBeAccessedByLayers("Services");

Możesz również upewnić się, że przestrzegane są konwencje nazewnictwa, takie jak przedrostki nazw klas, lub określić, że klasa nazwana w określony sposób musi znajdować się w odpowiednim pakiecie. GitHub zawiera wiele przykładów ArchUnit, które pomogą Ci zacząć i podsunąć pomysły. Możesz spróbować wykryć i naprawić wszystkie wymienione problemy architektoniczne tutaj, zlecając doświadczonemu programiście lub architektowi przeglądanie kodu raz w tygodniu, identyfikowanie naruszeń i ich korygowanie. Jednak ludzie są znani z tego, że nie działają konsekwentnie, a kiedy na projekt nakłada się nieuniknioną presję czasu, często pierwszą rzeczą, którą należy poświęcić, jest ręczna weryfikacja. Bardziej praktyczną metodą jest skodyfikowanie uzgodnionych wytycznych architektonicznych i reguł za pomocą testów automatycznych, przy użyciu ArchUnit lub innego narzędzia i uwzględnienie ich jako części kompilacji ciągłej integracji. Wszelkie problemy mogą być szybko wykryte i naprawione przez inżyniera, który je spowodował.

Podziel problemy i zadania na małe kawałki

Uczysz się programować. Otrzymujesz małe zadanie. Piszesz pod tysiącem linijek kodu. Wpisujesz to i testujesz. Następnie dodajesz instrukcje drukowania lub używasz debugera. Może dostaniesz kawę. Potem zastanawiasz się, o czym myślałeś. Brzmi znajomo? A to tylko problem z zabawkami. Zadania i systemy robocze są znacznie większe. Rozwiązywanie dużych problemów wymaga czasu. A co gorsza, w pamięci RAM twojego mózgu jest zbyt wiele do przechowywania. Dobrym sposobem radzenia sobie z tym jest rozbicie problemu na małe kawałki. Im mniejszy, tym lepiej. Jeśli uda ci się sprawić, by ten jeden mały kawałek zadziałał, nie musisz już o tym myśleć i możesz przejść do następnego kawałka. Robiąc to dobrze, chcesz napisać automatyczne testy dla każdego małego problemu. Powinieneś także często dokonywać zobowiązań. To daje punkt wycofania, gdy rzeczy nie działają zgodnie z oczekiwaniami. Pamiętam, jak pomagałem koledze z drużyny, który utknął. Zapytałem, kiedy ostatnio wprowadził zmiany, ponieważ najłatwiejszą poprawką byłoby wycofanie i ponowne zastosowanie zmiany. Odpowiedź brzmiała "tydzień temu". Potem miał dwa problemy: pierwotny i to, że nie pomogę mu debugować tygodniowej pracy. Po tym doświadczeniu przeprowadziłem sesję szkoleniową dla mojego zespołu, jak dzielić zadania na mniejsze części. Starsi programiści powiedzieli mi, że ich zadania są "specjalne" i "nie można ich rozdzielić". Kiedy usłyszysz słowo "specjalny" w odniesieniu do zadania, powinieneś natychmiast nabrać podejrzeń. Postanowiłem umówić się na drugie spotkanie. Każdy był odpowiedzialny za przyniesienie przykładu "specjalnego" zadania, a ja pomogłem im je rozbić. Pierwszym przykładem był ekran, którego opracowanie zaplanowano na dwa tygodnie. Podzielę to tak:

•  Utwórz ekran hello world pod odpowiednim adresem URL - bez pól, po prostu wyświetla hello world.
•  Dodaj funkcję wyświetlania listy z bazy danych.
•  Dodaj obszar tekstowy.
•  Dodaj wybrane rozwijane.
•  < Długa lista mniejszych zadań >
I zgadnij co? Po każdym z tych drobnych zadań może nastąpić zatwierdzenie. Oznacza to, że zatwierdzenia mogą się zdarzać wiele razy dziennie. Potem powiedziano mi, że można to zrobić w przypadku ekranów, ale przetwarzanie plików było "specjalne". Co powiedziałem o słowie "specjalny"? To również podzieliłem:

•  Przeczytaj linię z pliku.
•  Sprawdź poprawność pierwszego pola, łącznie z wywołaniem bazy danych.
•  Sprawdź poprawność drugiego pola i przekształć je za pomocą logiki biznesowej.
•  < Kilka pól później >
•  Zastosuj pierwszą regułę logiki biznesowej do wszystkich pól.
•  < Kilka zasad później >
•  Dodaj wiadomość do kolejki.

Ponownie zadanie nie było wyjątkowe. Jeśli uważasz, że zadanie jest wyjątkowe, zatrzymaj się i zastanów, dlaczego. Często okaże się, że ta technika nadal ma zastosowanie. W końcu programista powiedział mi, że nie może zatwierdzić swojego kodu w mniej niż tydzień. Zadanie zostało przeniesione do mnie. Zrobiłem dodatkowe zobowiązanie, aby zwrócić uwagę. Licząc, popełniłem 22 razy w ciągu 2 dni, jakie zajęło mi wykonanie zadania. Może gdyby popełnił częściej, zrobiłoby to szybciej!

Buduj zróżnicowane zespołybr>
Przed laty dobry lekarz wiedział wszystko, robił wszystko: nastawił złamanie, wykonał operację, pobrał krew. Dobry lekarz był niezależny i samowystarczalny, a autonomia była wysoko ceniona. Przewiń do dzisiaj. Wiedza eksplodowała, przekraczając jednostkę i prowadząc do specjalizacji. Aby zapewnić odpowiednie rozwiązanie od początku do końca, zaangażowanych będzie wielu specjalistów, a różne zespoły będą musiały współdziałać. Dotyczy to również tworzenia oprogramowania. Współpraca jest obecnie jedną z najwyżej cenionych cech "dobrych" profesjonalistów. W przeszłości samodzielność i samowystarczalność wystarczały, by być "dobrym". Teraz wszyscy musimy zachowywać się jak ekipy z pitu: członkowie zespołu. Wyzwaniem jest budowanie zespołów, które odnoszą sukcesy i są różnorodne. Cztery rodzaje różnorodności - pochodzenie branżowe, kraj pochodzenia, ścieżka kariery i płeć - pozytywnie korelują z innowacjami. W jednorodnym zespole, niezależnie od wykształcenia, perspektywy mogą być zbędne. Na przykład kobiety wnoszą przełomowe innowacje. Jak duży jest wpływ? W zespołach zarządzających o dużym zróżnicowaniu płci zaobserwowano 8% wzrost przychodów z innowacji. Różnice między członkami grupy mogą być również źródłem wglądu - członkowie o różnym pochodzeniu, doświadczeniach i pomysłach zwiększają pulę informacji, umiejętności i sieci. Przy większej liczbie perspektyw osiągnięcie konsensusu wymaga konstruktywnej debaty. Jeśli środowisko, w którym wymieniane są pomysły, jest pozytywne, kreatywne rozwiązania pojawią się naturalnie. Jednak zwiększenie różnorodności grupowej nie jest łatwym zadaniem. Konflikt może powstać, gdy heterogeniczne grupy nie komunikują się skutecznie lub dzielą się na frakcje. Ludzie wolą współpracować z podobnymi do nich. Zwarta grupa rozwinie swój własny język i kulturę, a outsiderzy będą nieufni. Odległość wraz z pułapkami wpadek w komunikacji cyfrowej sprawiają, że zespoły programistyczne są szczególnie podatne na problemy typu "my kontra oni" i niekompletne informacje. Jak więc czerpać korzyści z różnorodności i unikać wad? Kluczem do współpracy jest rozwijanie bezpieczeństwa psychicznego i zaufania w zespole. Kiedy otaczają nas ludzie, którym możemy ufać, nawet jeśli różnią się od nas, z większą pewnością podejmujemy ryzyko i eksperymentujemy. Kiedy ufamy sobie nawzajem, możemy oczekiwać od innych informacji lub perspektywy, która pomoże rozwiązać trudny problem, stwarzając tym samym możliwości współpracy. Możemy przezwyciężyć trudne sytuacje, gdy poprosimy o informację zwrotną. W zespołach z bezpieczeństwem psychologicznym ludziom łatwiej jest uwierzyć, że korzyści z mówienia przewyższają koszty. Uczestnictwo prowadzi do mniejszego oporu przed zmianą, a im częściej ludzie uczestniczą, tym większe prawdopodobieństwo, że zaproponują nowe pomysły. Osobowość ma również znaczenie w tworzeniu oprogramowania; równie ważne jest budowanie środowiska zaufania dla różnych osobowości. Wszyscy mamy kolegę, który jest gotów przetestować każdą nową bibliotekę, framework lub narzędzie, kogoś, kto myśli, jak wykorzystać lub odkryć nową lśniącą czerwoną zabawkę, czasami z zaskakującymi rezultatami. Niektórzy są skłonni do ustanawiania nowych procesów, stylów formatu kodu lub szablonów dla komunikatów zatwierdzenia i będą nam przypominać, gdy nie postępujemy zgodnie z właściwą procedurą. Możesz mieć kolegów z zespołu, którzy będą obiecywać niedostatecznie i za dużo, a także tych, którzy myślą o wszystkim, co może pójść nie tak: aktualizowaniu zależności, instalowaniu poprawek, zagrożeniach bezpieczeństwa itp. Weź pod uwagę różnice między wszystkimi i nie naciskaj zbyt mocno. Możemy zwiększyć różnorodność w naszych zespołach w dwóch wymiarach: pochodzenia i osobowości. Jeśli mamy dobrą dynamikę zespołu i nadal budujemy wzajemne zaufanie, odniesiemy większe sukcesy jako programiści.

Kompilacje nie muszą być powolne i zawodne

Jakiś czas temu pracowałem na wczesnym etapie start-upu, gdzie baza kodu i zespół programistów rosły każdego dnia. Ponieważ dodawaliśmy coraz więcej testów, kompilacje trwały coraz dłużej. Około ósmej minuty zacząłem to zauważać, dlatego pamiętam tę konkretną liczbę. Od ośmiu minut czas budowy prawie się podwoił. Na początku było całkiem przyjemnie. Rozpocząłem budowę, poszedłem na kawę i porozmawiałem ze współpracownikami z innych zespołów. Ale po kilku miesiącach stało się to irytujące. Wypiłem dość kawy i wiedziałem, nad czym wszyscy pracują, więc sprawdzałem Twittera lub pomagałem innym programistom z mojego zespołu, czekając na zakończenie moich kompilacji. Po powrocie do pracy musiałbym wtedy zmienić kontekst. Konstrukcja również była zawodna. Jak zwykle w przypadku każdego projektu oprogramowania, przeprowadziliśmy szereg niestabilnych testów. Pierwszym, choć naiwnym rozwiązaniem było wyłączenie testów (tj. @Ignore), które kończyły się niepowodzeniem. W końcu doszło do tego, że łatwiej było wprowadzić zmiany i polegać na serwerze ciągłej integracji (CI) niż uruchamiać testy lokalnie. Problem z tą taktyką polegał na tym, że przesunęła problem dalej. Jeśli test zakończył się niepowodzeniem na etapie CI, debugowanie trwało znacznie dłużej. A jeśli początkowo niestabilny test przeszedł pomyślnie i pojawił się dopiero po połączeniu, blokował cały zespół, dopóki nie ustaliliśmy, czy był to uzasadniony problem. Sfrustrowany próbowałem naprawić niektóre problematyczne testy. Szczególnie jeden test rzuca mi się w oczy. Pojawił się dopiero po uruchomieniu całego zestawu testów, więc za każdym razem, gdy wprowadzałem zmianę, musiałem czekać ponad 15 minut na informację zwrotną. Te niewiarygodnie długie cykle informacji zwrotnych i ogólny brak odpowiednich danych oznaczały, że zmarnowałem dni na tropienie tego błędu. Nie chodzi jednak tylko o jedną firmę. Jedną z zalet bycia hopperem pracy jest to, że widziałem, jak pracuje wiele różnych zespołów. Myślałem, że te problemy są normalne, dopóki nie zacząłem pracować w firmie, w której pracujemy dokładnie nad tymi problemami. Zespoły, które podążają za inżynierią produktywności programistów, praktyką i filozofią ulepszania doświadczeń programistów za pomocą danych, są w stanie ulepszyć swoje powolne i zawodne kompilacje. Te zespoły są szczęśliwsze i mają wyższą przepustowość, co sprawia, że firma jest szczęśliwsza. Bez względu na to, jakiego narzędzia kompilacji używają, osoby odpowiedzialne za produktywność programistów mogą skutecznie mierzyć wydajność kompilacji oraz śledzić wartości odstające i regresje zarówno w przypadku kompilacji lokalnych, jak i CI. Spędzają czas na analizowaniu wyników i znajdowaniu wąskich gardeł w procesie kompilacji. Gdy coś pójdzie nie tak, udostępniają raporty (np. skany kompilacji Gradle) członkom zespołu i porównują kompilacje, które nie powiodły się i przechodziły, aby dokładnie określić problem - nawet jeśli nie są w stanie odtworzyć problemów na własnych komputerach. Mając te wszystkie dane, mogą faktycznie zrobić coś, aby zoptymalizować proces i zmniejszyć frustrację, z którą borykają się programiści. Ta praca nigdy się nie kończy, więc kontynuują iteracje, aby utrzymać produktywność programistów. Nie jest to łatwe zadanie, ale zespoły, które nad nim pracują, są w stanie w pierwszej kolejności zapobiec występowaniu opisanych przeze mnie problemów.

"Ale to działa na mojej maszynie!"

Czy kiedykolwiek dołączyłeś do nowego zespołu lub projektu i musiałeś spróbować znaleźć drogę do infrastruktury potrzebnej do zbudowania kodu źródłowego na komputerze dewelopera? Nie jesteś sam i mogłeś mieć pytania:

•  Jaka wersja i dystrybucja JDK są wymagane do skompilowania kodu?
•  A jeśli używam Linuksa, ale wszyscy inni korzystają z Windows?
•  Jakiego IDE używasz i jakiej wersji potrzebuję?
•  Którą wersję Mavena lub innego narzędzia do budowania muszę zainstalować, aby prawidłowo przebiegają przepływy pracy dla programistów?

Mam nadzieję, że odpowiedź na te pytania nie brzmiała: "Pozwól mi rzucić okiem na narzędzia zainstalowane na mojej maszynie" - każdy projekt powinien mieć jasno zdefiniowany zestaw narzędzi, które są zgodne z wymaganiami technicznymi do kompilacji, testowania, wykonania i spakuj kod. Jeśli masz szczęście, wymagania te są udokumentowane w playbooku lub wiki, chociaż jak wszyscy wiemy, dokumentacja łatwo staje się nieaktualna, a synchronizacja instrukcji z najnowszymi zmianami wymaga wspólnego wysiłku. Jest lepszy sposób na rozwiązanie problemu. W duchu infrastruktury jako kodu dostawcy narzędzi wymyślili opakowanie, rozwiązanie, które pomaga w udostępnianiu standardowej wersji środowiska wykonawczego narzędzia do kompilacji bez ręcznej interwencji. Zawiera instrukcje wymagane do pobrania i zainstalowania środowiska wykonawczego. W przestrzeni Java znajdziesz Gradle Wrapper i Maven Wrapper. Nawet inne narzędzia, takie jak Bazel, narzędzie Google do budowania open source, zapewniają mechanizm uruchamiania. Zobaczmy, jak Maven Wrapper działa w praktyce. Musisz mieć Maven runtime zainstalowany na twoim komputerze do generowania tak zwanych plików Wrapper. Pliki otoki reprezentują skrypty, konfigurację i instrukcje, których każdy programista projektu używa do kompilowania projektu ze wstępnie zdefiniowaną wersją środowiska uruchomieniowego Maven. W związku z tym te pliki powinny zostać zaewidencjonowane w SCM wraz z kodem źródłowym projektu w celu dalszej dystrybucji. Poniższe uruchamia cel Wrapper dostarczony przez wtyczkę Takari Maven:

mvn -N io.takari:maven:0.7.6:wrapper

Poniższa struktura katalogów przedstawia typowy projekt Maven wzbogacony o pliki Wrapper, oznaczony pogrubioną czcionką:



Po zainstalowaniu plików Wrapper budowanie projektu na dowolnej maszynie jest proste: uruchom żądany cel za pomocą skryptu mvnw. Skrypt automatycznie zapewnia, że środowisko wykonawcze Maven zostanie zainstalowane ze wstępnie zdefiniowaną wersją ustawioną w maven-wrapper.properties. Oczywiście proces instalacji jest wywoływany tylko wtedy, gdy środowisko wykonawcze nie jest jeszcze dostępne w systemie. Następujące polecenie używa skryptu do uruchomienia celów czyszczenia i instalacji w systemie Linux, Unix lub macOS:

./mvnw clean insatll

W systemie Windows użyj skryptu wsadowego z rozszerzeniem .cmd: mvnw.cmd clean install A co z uruchamianiem typowych zadań w środowisku IDE lub z potoku CI/CD? Zobaczysz, że inne środowiska wykonawcze również wywodzą tę samą konfigurację środowiska wykonawczego z definicji opakowania. Musisz tylko upewnić się, że skrypty Wrapper są wywoływane w celu wywołania kompilacji. Dawno minęły czasy "Ale to działa na mojej maszynie!" - ustandaryzuj raz, buduj wszędzie! Wprowadź koncepcję opakowania do dowolnego projektu JVM, aby poprawić odtwarzalność i łatwość konserwacji kompilacji.

Sprawa przeciwko grubym słoikom

We współczesnym tworzeniu stron internetowych w Javie myśl o pakowaniu i uruchamianiu aplikacji w czymkolwiek innym niż gruby JAR staje się niemal herezją. Jednak budowanie i wdrażanie tych artefaktów może mieć wyraźne wady. Jednym z oczywistych problemów jest zazwyczaj duży rozmiar grubych plików JAR, które mogą zużywać nadmiar przestrzeni dyskowej i przepustowości sieci. Ponadto proces kompilacji monolitycznej może zająć dużo czasu i spowodować, że programiści będą przełączać się w kontekście podczas oczekiwania. Brak współdzielonych zależności może również powodować niespójność w korzystaniu z narzędzi, takich jak rejestrowanie, oraz wyzwania związane z integracją komunikacji lub serializacją między usługami. Wykorzystanie grubych plików JAR do wdrażania aplikacji Java stało się popularne wraz z rozwojem stylu architektury mikrousług, DevOps i technologii natywnych dla chmury, takich jak chmura publiczna, kontenery i platformy orkiestracyjne. Ponieważ aplikacje były dekomponowane na zbiór mniejszych usług, które były uruchamiane i zarządzane niezależnie, z operacyjnego punktu widzenia sensowne było łączenie całego kodu aplikacji w jeden wykonywalny plik binarny. Pojedynczy artefakt jest łatwiejszy do śledzenia, a samodzielne wykonanie eliminuje potrzebę uruchamiania dodatkowych serwerów aplikacji. Jednak niektóre organizacje sprzeciwiają się temu trendowi i tworzą "chude słoiki". Zespół inżynierów HubSpot omówił, w jaki sposób wymienione powyżej wyzwania wpłynęły na ich cykl rozwoju w poście na blogu "Usterka w naszych plikach JAR: Dlaczego przestaliśmy budować grube pliki JAR". Ostatecznie stworzyli nową wtyczkę Maven: SlimFast. Ta wtyczka różni się od klasycznej wtyczki Maven Shade, którą zna większość programistów Java, ponieważ oddziela kod aplikacji od powiązanych zależności i odpowiednio buduje i przesyła dwa oddzielne artefakty. Tworzenie i przesyłanie zależności aplikacji osobno może wydawać się nieefektywne, ale ten krok występuje tylko wtedy, gdy zależności uległy zmianie. W wielu aplikacjach zależności zmieniają się rzadko, więc ten krok jest często niewykonalny; Plik JAR zależności pakietów jest przesyłany do pamięci zdalnej tylko minimalną liczbę razy. Wtyczka SlimFast używa wtyczki Maven JAR, aby dodać wpis manifestu Class-Path do wąskiego pliku JAR, który wskazuje na plik JAR zależności, i generuje plik JSON z informacjami o wszystkich artefaktach zależności w S3, aby te można pobrać później. W czasie wdrażania kompilacja pobiera wszystkie zależności aplikacji, ale następnie buforuje te artefakty na każdym z serwerów aplikacji, więc ten krok również jest zwykle niewykonany. Wynik netto jest taki, że w czasie kompilacji do zdalnego magazynu przesyłany jest tylko chudy plik JAR aplikacji, który zwykle ma tylko kilkaset kilobajtów. W czasie wdrażania tylko ten sam cienki plik JAR należy pobrać do docelowego środowiska wdrażania, co zajmuje ułamek sekundy. Jedną z głównych idei stojącą za pojawieniem się DevOps jest to, że zespół programistyczny i operacyjny (i wszystkie inne zespoły) powinien współpracować dla wspólnego celu. Wybór formatu artefaktu wdrażania jest ważną decyzją, której celem jest możliwość ciągłego wdrażania funkcji dla użytkowników końcowych. Wszyscy powinni współpracować, aby zrozumieć wymagania w odniesieniu do tego, jak wpływa to na doświadczenie programisty i zdolność do zarządzania zasobami zaangażowanymi we wdrażanie. Wtyczka SlimFast jest obecnie powiązana z AWS S3 do przechowywania artefaktów, ale kod jest dostępny na GitHub, a zasady można dostosować do dowolnego rodzaju pamięci zewnętrznej.

Odnowiciel Kodu

Zawsze pamiętaj, że osoba, dla której tak naprawdę pracujemy, to osoba, która za sto lat odrestauruje dzieło. On jest tym, której my chcemy zaimponować.

Ten cytat pochodzi z Hobie, postaci z powieści Donny Tartt The Goldfinch. Hobie jest konserwatorem antycznych mebli. Jestem szczególnie wdzięczny za ten cytat, ponieważ pięknie wyraża to, co zawsze myślałem o kodzie: najlepszy kod jest napisany z myślą o programistach, którzy przyjdą później. Myślę, że obecne praktyki programistyczne cierpią na chorobę spowodowaną zbyt dużym pośpiechem. Podobnie jak drzewa w zatłoczonej dżungli, celem jest przerośnięcie konkurencji. Drzewa konkurujące o światło często przerastają, stając się wysokie i cienkie i stając się podatne na drobne zakłócenia. Silne wiatry lub łagodna choroba mogą sprawić, że się załamią. Nie mówię, że nie musimy patrzeć na krótkoterminowe korzyści - w rzeczywistości do tego zachęcam - ale nie kosztem długoterminowej stabilności. Dzisiejsza branża oprogramowania jest jak te drzewa. Wiele "nowoczesnych" zespołów skupia się tylko na kolejnym tygodniu lub miesiącu. Firmy walczą o przeżycie kolejnego dnia, kolejnego sprintu, kolejnego cyklu. I wydaje się, że nikt się tym nie przejmuje. Deweloperzy zawsze mogą znaleźć inną pracę, podobnie jak menedżerowie. Przedsiębiorcy mogą próbować wypłacić pieniądze, zanim firma straci na wartości. Podobnie VC, który wsparł początkową inwestycję. Zbyt często kluczem do sukcesu jest wyczucie czasu odejścia, zanim ludzie zorientują się, że niesamowity wzrost był tylko guzem. Z drugiej strony może nie jest tak źle. Niektóre meble mają przetrwać setki lat, a niektóre prawdopodobnie rozpadną się w ciągu dekady. Możesz wydać tysiące w Sotheby′s na porcelanową szafkę - lub przejdź do IKEA i prawdopodobnie umeblujesz cały dom. Może po prostu musimy zrozumieć tę nową gospodarkę, którą stworzyliśmy, w której wszystko jest ulotne i ulotne. Oczekuje się, że aktywa nie będą trwać długo, tylko wystarczająco długo. Nie powinniśmy tworzyć rzeczy, które przetrwają próbę czasu, tylko próbę zysku. A jednak wierzę, że jest punkt środkowy, nowa rola zaczyna nabierać kształtu: przywracający kod. Robienie czegoś, co za pierwszym razem trwa wiecznie, jest tak drogie, że nie jest tego warte, ale skupienie się tylko na krótkoterminowym zysku stworzy kod, który upadnie pod własnym ciężarem. W tym miejscu pojawia się osoba przywracająca kod, osoba, której zadaniem nie jest "odtworzenie tego samego, ale lepszego" (powszechne życzenie, które prawie zawsze zawodzi), ale raczej przejęcie istniejącej bazy kodu i powolne jej przekształcenie, aby znów było możliwe do zarządzania . Dodaj kilka testów tutaj, rozbij tę brzydką klasę tam, usuń nieużywaną funkcjonalność i oddaj ją ulepszoną. Jako programiści musimy zdecydować, jakie oprogramowanie chcemy zbudować. Możemy przez chwilę skupić się na zysku, zbudować coś, co się utrzyma, ale w pewnym momencie musimy wybrać między trwałością, ostrożnym przekształceniem kodu, a zyskiem, porzuceniem go i rozpoczęciem od nowa. W końcu zyski są niezbędne, ale niektóre rzeczy są większe niż pieniądze.

Współbieżność na JVM

Pierwotnie surowe wątki były jedynym modelem współbieżności dostępnym w JVM i nadal są domyślnym wyborem do pisania programów równoległych i współbieżnych w Javie. Kiedy Java została zaprojektowana 25 lat temu, sprzęt był zupełnie inny. Zapotrzebowanie na uruchamianie aplikacji równoległych było mniejsze, a zalety współbieżności ograniczał brak procesorów wielordzeniowych - zadania można było oddzielić, ale nie wykonywać jednocześnie. W dzisiejszych czasach dostępność i oczekiwanie zrównoleglania wyraźnie uwidoczniło ograniczenia jawnej wielowątkowości. Wątki i blokady są zbyt niskopoziomowe: prawidłowe ich używanie jest trudne; jeszcze trudniej zrozumieć model pamięci Java. Wątki, które komunikują się przez współdzielony stan mutowalny, nie nadają się do masowego równoległości, co prowadzi do niedeterministycznych niespodzianek, gdy dostęp nie jest odpowiednio zsynchronizowany. Co więcej, nawet jeśli twoje blokady są poprawnie rozmieszczone, celem blokady jest ograniczenie wątków działających równolegle, zmniejszając w ten sposób stopień równoległości aplikacji. Ponieważ Java nie obsługuje pamięci rozproszonej, niemożliwe jest skalowanie programów wielowątkowych w poziomie na wielu komputerach. A jeśli pisanie programów wielowątkowych jest trudne, ich dokładne przetestowanie jest prawie niemożliwe - często stają się koszmarem konserwacji. Najprostszym sposobem na pokonanie ograniczeń pamięci współdzielonej jest koordynować wątki za pośrednictwem kolejek rozproszonych zamiast blokad. W tym przypadku przekazywanie wiadomości zastępuje pamięć współdzieloną, co również poprawia rozdzielenie. Kolejki są dobre do komunikacji jednokierunkowej, ale mogą wprowadzać opóźnienia. Akka udostępnia model aktora spopularyzowany przez Erlanga na JVM i jest bardziej znany programistom Scali. Każdy aktor jest obiektem odpowiedzialnym za manipulowanie tylko własnym stanem. Współbieżność jest implementowana z przepływem komunikatów między aktorami, dzięki czemu można je postrzegać jako bardziej ustrukturyzowany sposób korzystania z kolejek. Aktorzy mogą być zorganizowani w hierarchię, zapewniając wbudowaną odporność na awarie i odzyskiwanie poprzez nadzór. Aktorzy mają również pewne wady: niewpisane wiadomości nie działają dobrze z obecnym brakiem dopasowania wzorców w Javie, niezmienność wiadomości jest konieczna, ale obecnie nie można jej wymusić w Javie, kompozycja może być niezręczna, a zakleszczenie między aktorami jest nadal możliwe. Clojure stosuje inne podejście dzięki wbudowanej programowej pamięci transakcyjnej, zamieniając stertę JVM w transakcyjny zestaw danych. Podobnie jak zwykła baza danych, dane są modyfikowane za pomocą (optymistycznej) semantyki transakcyjnej. Transakcja jest automatycznie ponawiana, gdy napotka jakiś konflikt. Ma to tę zaletę, że nie blokuje, eliminując wiele problemów związanych z jawną synchronizacją. Dzięki temu łatwo je komponować. Dodatkowo wielu deweloperów zna transakcje. Niestety, to podejście jest nieefektywne w systemach masowo równoległych, w których współbieżne zapisy są bardziej prawdopodobne. W takich sytuacjach ponawianie prób jest coraz bardziej kosztowne, a wydajność może stać się nieprzewidywalna. Lambdy Java 8 promują wykorzystanie właściwości programowania funkcjonalnego w kodzie, takich jak niezmienność i przezroczystość referencyjna. Podczas gdy model aktora zmniejsza konsekwencje mutowalnego stanu, uniemożliwiając udostępnianie, programowanie funkcjonalne umożliwia udostępnianie stanu, ponieważ zabrania zmienności. Paralelizujący kod złożony z czystych, pozbawionych efektów ubocznych funkcji może być trywialny, ale program funkcjonalny może być mniej wydajny czasowo niż jego imperatywny odpowiednik i może nakładać większe obciążenie na garbage collector. Lambdy ułatwiają również wykorzystanie paradygmatu programowania reaktywnego w Javie, polegającego na asynchronicznym przetwarzaniu strumieni zdarzeń. Nie ma srebrnej kuli dla współbieżności, ale istnieje wiele różnych opcji z różnymi kompromisami. Twoim obowiązkiem jako programisty jest ich poznanie i wybranie tego, który najlepiej pasuje do danego problemu.

Ekspresja deklaratywna to droga do równoległości

Na początku Java była imperatywnym, obiektowym językiem programowania. Rzeczywiście, nadal jest. Z biegiem lat Java ewoluowała, na każdym etapie stając się coraz bardziej językiem deklaratywnej ekspresji. W trybie imperatywnym kod wyraźnie mówi komputerowi, co ma robić. Deklaratywny dotyczy kodu wyrażającego cel abstrahujący od sposobu, w jaki cel jest osiągany. Abstrakcja jest sercem programowania, więc przejście od kodu imperatywnego do kodu deklaratywnego jest naturalne. Sednem wyrażenia deklaratywnego jest użycie funkcji wyższego rzędu, funkcji, które przyjmują funkcje jako parametry i/lub funkcje zwracające. Pierwotnie nie była to integralna część Javy, ale wraz z Javą 8 przesunęła się na pierwszy plan: Java 8 była punktem zwrotnym w ewolucji Javy, umożliwiając zastąpienie wyrażeń imperatywnych wyrażeniami deklaratywnymi. Przykładem - trywialnym, niemniej jednak wskazującym na główną kwestię - jest napisanie funkcji, która zwraca do funkcji Listę zawierającą kwadraty argumentu List. Koniecznie możemy napisać:

List< Integer > squareImperative(final List< Integer > datum) {
var result = new ArrayList< Integer >();
for (var i = 0; i < datum.size(); i++) {
result.add(i, datum.get(i) * datum.get(i));
}
return result;
}

Funkcja tworzy abstrakcję na pewnym kodzie niskiego poziomu, ukrywając szczegóły przed kodem, który jej używa. W Javie 8 i nowszych możemy używać strumieni i wyrażać algorytm w bardziej deklaratywny sposób:

List< Integer > squareDeclarative(final List< Integer > datum) {
return datum.stream()
.map(i -> i * i)
.collect(Collectors.toList());
}

To określa wyższy poziom wyrażania tego, co ma być zrobione; szczegóły, w jaki sposób pozostawiono wdrożeniu biblioteki. Klasyczna abstrakcja. To prawda, że implementacja znajduje się w funkcji, która już abstrahuje i ukrywa, ale co wolałbyś zachować: implementacja imperatywna niskiego poziomu czy implementacja deklaratywna wysokiego poziomu? Dlaczego to taka wielka sprawa? Powyższe jest klasycznym przykładem żenująco równoległego obliczenia. Ocena każdego wyniku zależy tylko od jednego elementu danych wejściowych; nie ma sprzężenia. Możemy więc napisać:

List< Integer > squareDeclarative(final List< Integer > datum) {
return datum.parallelStream()
.map(i -> i * i)
.collect(Collectors.toList());
}

W ten sposób uzyskamy maksymalną równoległość, jaką biblioteka jest w stanie wydobyć z platformy. Ponieważ abstrahujemy od szczegółów tego, jak, skupiając się tylko na celu, możemy w trywialny sposób przekształcić sekwencyjne obliczenia równoległe na danych w równoległe. Czytelnikowi pozostanie jako ćwiczenie (próba) napisania równoległej wersji kodu imperatywnego, jeśli sobie tego życzy. Czemu? Ponieważ w przypadku problemów z równoległymi danymi, używanie strumieni jest właściwą abstrakcją. Robienie czegokolwiek innego to zaprzeczanie ewolucji Javy w Javie 8.

Dostarczaj lepsze oprogramowanie szybciej

Dla mnie "Dostarcz lepsze oprogramowanie, szybciej" jest zasadą przewodnią, którą gorąco polecam, ponieważ opisuje ona, co musi się stać, aby użytkownicy byli zadowoleni. Ponadto (a może co ważniejsze) podążanie za nim może zaowocować przyjemniejszą i ciekawszą karierą. Aby zobaczyć jak, przyjrzyjmy się trzem częściom tego ważnego pomysłu:

1. Dostarczanie oznacza branie odpowiedzialności za coś więcej niż tylko pisanie i debugowanie kodu. Wbrew pozorom nie płaci się za pisanie kodu. Płacisz za ułatwienie użytkownikom robienia czegoś, co uznają za wartościowe, i dopóki Twój kod nie zostanie uruchomiony w środowisku produkcyjnym, nie będą oni czerpać korzyści z Twojej ciężkiej pracy. Zmiana koncentracji z pisania kodu na dostarczanie oprogramowania wymaga zrozumienia całego procesu wprowadzania zmian do produkcji, a następnie wykonania dwóch kluczowych rzeczy:

•  Upewnienie się, że nie robisz rzeczy, które utrudniają proces, takich jak odgadywanie znaczenia niejasnego wymagania zamiast proszenia o wyjaśnienie przed jego wdrożeniem.
•  Upewnij się, że robisz rzeczy, które przyspieszają proces, takie jak pisanie i uruchamianie automatycznych testów, aby pokazać, że Twój kod spełnia kryteria akceptacji.

2. Better Software to skrót od dwóch idei, które powinieneś już znać: "budowanie właściwej rzeczy" i "budowanie właściwej rzeczy". Pierwszy oznacza upewnienie się, że to, co napisałeś, spełnia wszystkie wymagania i kryteria akceptacji. Drugi dotyczy pisania kodu, który jest łatwo zrozumiały dla innego programisty, aby mógł z powodzeniem naprawiać błędy lub dodawać nowe funkcje. Chociaż może to wydawać się łatwe, zwłaszcza jeśli stosujesz praktykę, taką jak programowanie sterowane testami (TDD), wiele zespołów ma tendencję do schylania się w jedną lub drugą stronę:

•  Osoby nie będące programistami mogą skłaniać programistów do pójścia na skróty, aby szybciej dostarczać nowe funkcje, z obietnicą, że wrócą i "zrobią to dobrze" później.
•  Czasami programiści, którzy właśnie się czegoś nauczyli, będą próbowali używać tego wszędzie, gdzie to możliwe, nawet jeśli wiedzą, że prostsze rozwiązanie działałoby równie dobrze.

W obu przypadkach saldo zostaje utracone, a wynikający z tego dług techniczny zwiększa czas potrzebny na dostarczenie wartości użytkownikom, aż do odzyskania salda.

3. Faster odnosi się zarówno do dostarczania, jak i lepszego oprogramowania i może być trudnym celem, ponieważ ludzie próbujący szybko zrobić skomplikowane rzeczy mają tendencję do popełniania błędów. Dla mnie oczywiste rozwiązanie to:

•  Korzystanie z procesu takiego jak TDD do tworzenia testów automatycznych, a następnie regularne uruchamianie zautomatyzowanych testów jednostkowych, integracyjnych i testów akceptacji użytkownika w celu weryfikacji zachowania systemu.
•  Tworzenie i uruchamianie zautomatyzowanego procesu, który uruchamia wszystkie testy w wielu środowiskach i, zakładając, że wszystkie zakończą się pomyślnie, wdraża kod w środowisku produkcyjnym.

Oba te procesy będą wykonywane wiele razy i wymagają dużej dbałości o szczegóły - takie właśnie zadanie wykonuje komputer szybciej i dokładniej niż człowiek. To dobrze, ponieważ masz jeszcze jedno zalecenie: częściej wdrażaj zmiany w środowisku produkcyjnym, aby każde wdrożenie zawierało mniej zmian i dlatego jest mniej prawdopodobne, że będą mieć problemy, a użytkownicy szybciej uzyskają korzyści z Twojej pracy. Przyjęcie zasady "Dostarcz lepsze oprogramowanie, szybciej" jest zarówno wyzwaniem, jak i zabawą. Pamiętaj, że znalezienie i naprawienie wszystkich miejsc, które wymagają pracy, zajmie trochę czasu, ale nagrody są tego warte.

Czy wiesz, która jest godzina?

O której w poniedziałek przylatuje samolot Skandynawskich Linii Lotniczych z Oslo do Aten? Dlaczego pytania, które wydają się tak proste w codziennym życiu, są tak trudne w programowaniu? Czas powinien być prosty, mijają tylko sekundy, coś, co komputer bardzo dobrze mierzy:

System.currentTimeMillis() = 1570964561568

Chociaż poprawne, 1570964561568 nie jest tym, czego chcemy, gdy pytamy, która jest godzina. Preferujemy 13:15, 13.10.2019. Okazuje się, że czas to dwie różne rzeczy. Z jednej strony mijają sekundy. Z drugiej strony mamy nieszczęśliwe mariaż astronomii i polityki. Odpowiadając na pytanie "Która godzina?" zależy od położenia słońca na niebie w stosunku do twojej pozycji oraz decyzji politycznych podjętych w tym regionie do tego momentu. Wiele problemów, jakie mamy z datą i godziną w kodzie, wynika z połączenia tych dwóch pojęć. Pomoże Ci korzystanie z najnowszej biblioteki java.time (lub Noda Time w .NET). Oto trzy główne pojęcia ułatwiające prawidłowe rozumowanie dotyczące czasu: LocalDateTime, ZonedDateTime i Instant. LocalDateTime odnosi się do koncepcji 13:15, 13 października 2019 r. Na osi czasu może być ich dowolna liczba. Natychmiastowa odnosi się do określonego punktu na osi czasu. Tak samo jest w Bostonie, jak w Pekinie. Aby przejść z LocalDateTime do Instant, potrzebujemy strefy czasowej, która zawiera przesunięcia czasu uniwersalnego koordynowanego (UTC) i reguły czasu letniego (DST) w tym czasie. ZonedDateTime to LocalDateTime ze strefą czasową. Których używasz? Jest tak wiele pułapek. Pokażę ci kilka. Załóżmy, że piszemy oprogramowanie do organizacji międzynarodowej konferencji. Czy to zadziała?

public class PresentationEvent {
final Instant start, end;
final String title;
}

Nie. Chociaż musimy reprezentować określony punkt w czasie, w przypadku przyszłych zdarzeń, nawet jeśli znamy czas i strefę czasową, nie możemy znać chwili z wyprzedzeniem, ponieważ reguły czasu letniego lub przesunięcia UTC mogą się zmienić od teraz do tego czasu. Potrzebujemy ZonedDateTime. Co powiesz na regularne wydarzenia, takie jak lot? Czy to zadziała?

public class Flight {
final String flightReference;
final ZonedDateTime departure, arrival;
}
Nie. To może zawieść dwa razy w roku. Wyobraź sobie lot wylatujący w sobotę o 22:00. i przybycie w niedzielę o 6:00 rano. Co się stanie, gdy cofniemy zegar o godzinę ze względu na czas letni? O ile samolot nie będzie bezużytecznie krążyć w ciągu dodatkowej godziny, wyląduje o 5:00, a nie o 6:00 rano. Kiedy przesuniemy się o godzinę do przodu, przyleci o 4:00. zarówno początek, jak i koniec. Oto, czego potrzebujemy:

public class Flight {
final String flightReference
final ZonedDateTime departure;
final Duration duration;
}

A co z wydarzeniami, które zaczynają się o 2:30? Który? Mogą być dwa lub w ogóle nie istnieć. W Javie następujące metody obsługują jesienne przejście na czas letni:

ZonedDateTime.withEarlierOffsetAtOverlap()
ZonedDateTime.withLaterOffsetAtOverlap()

W Noda Time określ obydwa przejścia czasu letniego za pomocą Resolverów. Tylko zarysowałem powierzchnię potencjalnych problemów, ale jak mówią, dobre narzędzia to połowa pracy. Użyj java.time (lub Noda Time), a zaoszczędzisz sobie wielu błędów.

Nie ukrywaj swoich narzędzi

Jakie jest podstawowe narzędzie, którego potrzebuje każdy programista Java? Eclipose? IntelliJ IDEA? NetBeans? Nie. To javac. Bez tego wszystko, co masz, to pliki dziwnie wyglądającego tekstu. Można wykonywać pracę bez zintegrowanych środowisk programistycznych (IDE) - zapytaj ludzi takich jak ja, którzy programowali w dawnych czasach. Programowanie bez niezbędnych narzędzi programistycznych nie jest możliwe. Biorąc pod uwagę, że są one kluczowe dla zadania, zaskakujące jest, jak rzadko ludzie używają bezpośrednio narzędzi takich jak javac. Chociaż wiedza o tym, jak efektywnie korzystać z IDE, jest ważna, zrozumienie, co robi i jak, ma kluczowe znaczenie. Kiedyś pracowałem nad projektem z dwoma podsystemami, jednym w C++ a drugi w Javie. Programiści C++ pracowali z wybranym przez siebie edytorem i wierszem poleceń. Programiści Java używali IDE. Pewnego dnia zmieniło się zaklęcie do interakcji z systemem kontroli wersji. Była to prosta zmiana w wierszu poleceń dla programistów C++, którzy bez opóźnień ruszyli w drogę. Zespół Javy spędził cały poranek zmagając się z konfiguracją Eclipse. W końcu po południu wrócili do produktywnej pracy. Ta niefortunna historia nie odzwierciedla zbyt dobrze opanowania przez zespół Java wybrane przez siebie narzędzia. Ale pokazuje również, jak bardzo byli zdystansowani w swojej codziennej pracy od podstawowych narzędzi swojej branży, pracując wyłącznie w IDE. Ukrywanie informacji to z pewnością świetna zasada umożliwiająca skupienie się na użytecznej abstrakcji, a nie na masie szczegółów. Ale oznacza to wybór zagłębiania się w szczegóły tylko wtedy, gdy jest to istotne, a nie ignorowanie szczegółów. Poleganie wyłącznie na IDE może podważyć mistrzostwo programisty w swoich narzędziach, ponieważ IDE celowo ukrywa nakrętki i śruby. Konfiguracja - często tylko przypadek zastosowania się do cudzych wskazówek - może zostać zapomniana jak tylko to się stanie. Umiejętność bezpośredniego korzystania z podstawowych narzędzi ma wiele zalet:

•  Scenariusze "działa na moim komputerze" są mniej prawdopodobne i łatwiejsze do rozwiązania, jeśli rozumiesz relacje między narzędziami, kodem źródłowym, innymi zasobami i wygenerowanymi plikami. Pomaga również wiedzieć, co spakować do instalacji.
•  Ustawienie różnych opcji jest niezwykle szybkie i łatwe. Początek za pomocą poleceń takich jak javac -help, dzięki czemu możesz zobaczyć, jakie są te opcje.
•  Znajomość podstawowych narzędzi jest cenna, gdy pomagamy ludziom, którzy korzystają z innego środowiska. Pomaga również, gdy coś pójdzie nie tak; trudno jest rozwiązywać problemy, gdy zintegrowane narzędzia nie działają. Widoczność jest lepsza w wierszu poleceń i możesz izolować części procesu, tak jak podczas debugowania kodu.
•  Masz dostęp do bogatszego zestawu narzędzi. Możesz zintegrować dowolną kombinację narzędzi, które mają interfejs wiersza poleceń (na przykład skrypty lub polecenia systemu Linux), a nie tylko te obsługiwane w środowisku IDE.
•  Użytkownicy końcowi nie będą uruchamiać Twojego kodu w IDE! W trosce o dobre wrażenia użytkownika przetestuj od początku, uruchamiając kod tak, jak będzie uruchamiane na komputerze użytkownika.

Nic z tego nie neguje korzyści płynących z IDE. Ale aby być naprawdę wykwalifikowanym w swoim rzemiośle, zrozum swoje podstawowe narzędzia i nie pozwól im zardzewieć.

Nie zmieniaj swoich zmiennych

Staram się, aby jak najwięcej zmiennych było ostatecznych, ponieważ łatwiej jest mi wnioskować o niezmiennym kodzie. To sprawia, że moje życie związane z kodowaniem jest prostsze, co jest dla mnie wysokim priorytetem - spędziłem zbyt dużo czasu, próbując dokładnie określić, jak zawartość zmiennej zmienia się w całym bloku kodu. Oczywiście wsparcie Javy dla niezmienności jest bardziej ograniczone niż w niektórych innych językach, ale wciąż możemy zrobić.

Przypisz raz

Oto mały przykład struktury, którą widzę wszędzie:

Thing thing;
if (nextToken == MakeIt) {
thing = makeTheThing();
} else {
thing = new SpecialThing(dependencies);
}
thing.doSomethingUseful();

Dla mnie nie oznacza to nieodwołalnie, że ustalimy wartość rzeczy przed jej użyciem i nie zmienimy jej ponownie. Zajmuje mi czas, aby przejść przez kod i dowiedzieć się, że nie będzie on pusty. To także wypadek, który może się wydarzyć, gdy musimy dodać więcej warunków i nie do końca zrozumieć logikę. Nowoczesne IDE ostrzegają o nieustawionych rzeczach - ale wtedy wielu programistów ignoruje ostrzeżenia. Pierwszą poprawką byłoby użycie wyrażenia warunkowego:

final var thing = nextToken == MakeIt
? makeTheThing()
: new SpecialThing(dependencies);
thing.doSomething();

Jedynym sposobem przejścia przez ten kod jest przypisanie wartości. Następnym krokiem jest zamknięcie tego zachowania w funkcji, której mogę nadać opisową nazwę:

final var thing = aThingFor(nextToken);
thing.doSomethingUseful();
private Thing aThingFor(Token aToken) {
return aToken == MakeIt
? makeTheThing()
: new SpecialThing(dependencies);
}

Teraz cykl życia rzeczy jest łatwy do zauważenia. Często ta refaktoryzacja pokazuje, że rzecz jest używana tylko raz, więc mogę usunąć zmienną:

aThingFor(aToken).doSomethingUseful();

Takie podejście przygotowuje nas do sytuacji, gdy nieuchronnie stan staje się bardziej skomplikowany; zauważ, że instrukcja switch jest prostsza bez konieczności powtarzania klauzul break:

private Thing aThingFor(Token aToken) {
switch (aToken) {
case MakeIt:
return makeTheThing();
case Special:
return new SpecialThing(dependencies);
case Green:
return mostRecentGreenThing();
default:
return Thing.DEFAULT;
}
}
Localize Scope
Here′s another variant:
var thing = Thing.DEFAULT;
// lots of code to figure out nextToken
if (nextToken == MakeIt) {
thing = makeTheThing();
}
thing.doSomethingUseful();

Jest gorzej, ponieważ przypisania do rzeczy nie są blisko siebie i mogą nawet się nie wydarzyć. Ponownie wyodrębniamy to do metody pomocniczej:

final var thing = theNextThingFrom(aStream);
private Thing theNextThingFrom(Stream aStream) {
// lots of code to figure out nextToken
if (nextToken == MakeIt) {
return makeTheThing();
}
return Thing.DEFAULT;
}

Alternatywnie, oddzielenie dotyczy dalej:

final var thing = aThingForToken(nextTokenFrom(aStream));

Lokalizowanie zakresu wszystkiego, co jest zmienne, w metodzie pomocniczej, sprawia, że kod najwyższego poziomu jest przewidywalny. Wreszcie, chociaż niektórzy programiści nie są do tego przyzwyczajeni, możemy wypróbować podejście strumieniowe:

final var thing = nextTokenFrom(aStream)
.filter(t -> t == MakeIt)
.findFirst()
.map(t -> makeTheThing())
.orElse(Thing.DEFAULT);

Regularnie przekonywałem się, że próba zablokowania wszystkiego, co się nie zmienia, sprawia, że zastanawiam się uważniej nad swoim projektem i usuwam potencjalne błędy. Zmusza mnie do jasnego określenia, gdzie rzeczy mogą się zmienić, i do umieszczania takich zachowań w lokalnych zakresach.

Opanuj myślenie SQL

Spójrz na to zapytanie:

SELECT c.id, c.name, c.address, o.items FROM customers c
JOIN orders o
ON o.customer_id = c.id
GROUP BY c.id

Pozyskujemy wszystkich klientów, którzy mają zamówienia, łącznie z ich nazwiskami i adresami wraz ze szczegółami ich zamówień. Cztery linijki kodu. Każdy, kto ma niewielkie doświadczenie w SQL, w tym osoby nie będące programistami, może zrozumieć to zapytanie. Teraz pomyśl o implementacji Javy. Możemy zadeklarować klasy dla Klienta i Zamówienie. Pamiętam, jak konsultanci z dobrymi intencjami mówili, że powinniśmy również tworzyć klasy, aby enkapsulować ich kolekcje, a nie używać "nagich" kolekcji Java. Nadal musimy wysyłać zapytania do bazy danych, więc pobieramy narzędzie do mapowania obiektowo-relacyjnego (ORM) i piszemy odpowiedni kod. Cztery linijki kodu szybko zamieniają się w dziesiątki, a nawet setki linijek. Kilka minut, które zajęło napisanie i udoskonalenie zapytania SQL, rozciągnęło się na godziny lub dni edycji, pisania testów jednostkowych, przeglądów kodu i tak dalej. Czy nie możemy po prostu zaimplementować całego rozwiązania za pomocą samego zapytania SQL? Czy jesteśmy pewni, że nie możemy? Nawet jeśli naprawdę nie możemy, czy możemy wyeliminować marnotrawstwo i pisać tylko to, co najważniejsze? Rozważ cechy zapytania SQL:
Nie potrzebujemy nowej tabeli dla wyjścia join, więc nie tworzymy jedną.
Największą wadą stosowanego programowania obiektowego było przekonanie, że należy wiernie odtworzyć model domeny w kodzie. W rzeczywistości kilka podstawowych definicji typów jest użytecznych do enkapsulacji i zrozumienia, ale przez resztę czasu potrzebujemy tylko krotek, zbiorów, tablic i tak dalej. Zbędne zajęcia stają się obciążeniem w miarę rozwoju kodu.

Zapytanie jest deklaratywne.
Nigdzie nie mówi bazie danych, jak wykonać zapytanie; po prostu określa ograniczenia relacyjne, które musi spełnić baza danych. Java jest językiem imperatywnym, więc zwykle piszemy kod, który mówi, co robić. Zamiast tego powinniśmy zadeklarować ograniczenia i pożądane wyniki, a następnie wyizolować sposób implementacji w jednym miejscu lub delegować do biblioteki, która może to za nas zaimplementować. Podobnie jak programowanie funkcjonalne, SQL jest deklaratywny. W programowaniu funkcjonalnym równoważne implementacje deklaratywne są osiągane za pomocą komponujących prymitywów, takich jak map, filter, reduction i tak dalej.

Język specyficzny dla domeny (DSL) jest dobrze dopasowany do problemu.
DSL mogą być nieco kontrowersyjne. Bardzo trudno jest zaprojektować dobry, a implementacje mogą być bałaganiarskie. SQL to DSL danych. Jest dziwaczny, ale jego długowieczność jest dowodem na to, jak dobrze wyraża typowe potrzeby w zakresie przetwarzania danych. Wszystkie aplikacje to tak naprawdę aplikacje danych. Koniec końców wszystko, co piszemy, to program do manipulacji danymi, niezależnie od tego, czy myślimy o tym w ten sposób, czy nie. Ogarnij ten fakt, a niepotrzebny szablon się ujawni, pozwalając napisać tylko to, co najważniejsze.

Zdarzenia między komponentami Java

Jedną z podstawowych koncepcji orientacji obiektowej w Javie jest to, że każdą klasę można uznać za komponent. Komponenty można rozbudowywać lub dołączać, tworząc większe komponenty. Ostateczna aplikacja jest również uważana za składnik. Komponenty są jak klocki Lego, które budują większą strukturę. Zdarzenie w Javie to akcja, która zmienia stan komponentu. Na przykład, jeśli twoim komponentem jest przycisk, kliknięcie tego przycisku jest zdarzeniem, które zmienia stan przycisku, który ma zostać kliknięty. Zdarzenia niekoniecznie zachodzą tylko na elementach wizualnych. Na przykład możesz mieć zdarzenie na składniku USB, że urządzenie jest podłączone. Lub zdarzenie na elemencie sieci, w którym przesyłane są dane. Zdarzenia pomagają rozdzielić zależności między komponentami. Załóżmy, że mamy składnik Piekarnik i składnik Osoba. Te dwa komponenty istnieją równolegle i działają niezależnie od siebie. Nie powinniśmy czynić Persona częścią Piekarnika ani na odwrót. Aby zbudować inteligentny dom, chcemy, aby Piekarnik przygotowywał jedzenie, gdy Osoba jest głodna. Oto dwie możliwe implementacje:

1. Piekarnik sprawdza osobę w stałych, krótkich odstępach czasu. To denerwuje Persona i jest również drogie dla Piekarnika, jeśli chcemy, aby sprawdzał wiele instancji Person.
2. Osoba przychodzi z wydarzeniem publicznym Głodny, na które zasubskrybowany jest Piekarnik. Gdy Hungry zostaje zwolniony, Piekarnik zostaje powiadomiony i zaczyna przygotowywać jedzenie.

Drugie rozwiązanie wykorzystuje architekturę zdarzeń do wydajnej obsługi nasłuchu i komunikacji między komponentami i bez bezpośredniego połączenia między osobą a piekarnikiem, ponieważ osoba uruchomi zdarzenie, a dowolny komponent, taki jak piekarnik, lodówka i stół, może to nasłuchiwać zdarzenie bez specjalnej obsługi ze strony składnika Person. Implementacja zdarzeń dla komponentu Java może przybierać różne formy, w zależności od oczekiwanego sposobu ich obsługi. Aby zaimplementować minimalny HungerListener w komponencie Person, najpierw utwórz interfejs nasłuchiwania:

@FunctionalInterface
public interface HungerListener {
void hungry();
}

Następnie w klasie Person zdefiniuj listę do przechowywania detektorów:

private List listeners = new ArrayList<>();

Zdefiniuj API, aby wstawić nowy odbiornik:

public void addHungerListener(HungerListener listener) {
listeners.add(listener);
}

Możesz utworzyć podobny interfejs API do usuwania odbiornika. Dodaj również metodę wyzwalania akcji głodu, aby powiadomić wszystkich słuchaczy o zdarzeniu:

public void becomesHungry() {
for (HungerListener listener : listeners)
listener.hungry();
}

Na koniec z klasy Oven dodaj kod, który nasłuchuje zdarzenia i implementuje akcję po wyzwoleniu zdarzenia:

Person person = new Person();
person.addHungerListener(() -> {
System.err.println("The person is hungry!");
// Oven takes action here
});

I wypróbuj to:

person.becomesHungry();

W przypadku kodu w pełni oddzielonego ostatnia sekcja powinna znajdować się w niezależnej klasie, która ma instancję Person i Oven i obsługuje logikę między nimi. Podobnie możemy dodać inne akcje dla lodówki, stołu i tak dalej. Wszyscy zostaną powiadomieni tylko wtedy, gdy osoba stanie się głodna.

Pętle sprzężenia zwrotnego

•  Ponieważ nasi menedżerowie produktu nie wiedzą, czego chcą, dowiadują się od klientów. Czasami się mylą.
•  Ponieważ nasi menedżerowie produktu nie wiedzą wszystkiego o systemach, zapraszają innych ekspertów do udziału w projekcie. Interesariusze mylą się.
•  Ponieważ nie wiem, co kodować, dowiaduję się od naszych menedżerów produktu. Czasami się mylimy.
•  Ponieważ popełniam błędy podczas pisania kodu, pracuję z IDE. Moje IDE poprawia mnie, gdy się mylę.
•  Ponieważ popełniam błędy w zrozumieniu istniejącego kodu, używam języka pisanego statycznie. Kompilator poprawia mnie, gdy się mylę.
•  Ponieważ popełniam błędy w myśleniu, pracuję z parą. Moja para poprawia mnie, gdy się mylę.
•  Ponieważ moja para jest człowiekiem i też popełnia błędy, piszemy testy jednostkowe. Nasze testy jednostkowe poprawiają nas, gdy się mylimy.
•  Ponieważ mamy zespół, który również koduje, integrujemy się z ich kodem. Nasz kod nie skompiluje się, jeśli się mylimy.
•  Ponieważ nasz zespół popełnia błędy, piszemy testy akceptacyjne, które sprawdzają cały system. Nasze testy akceptacyjne zakończą się niepowodzeniem, jeśli się mylimy.
•  Ponieważ popełniamy błędy podczas pisania testów akceptacyjnych, spotykamy się z trzema towarzyszami, aby przez nie omówić. Nasi amigos powiedzą nam, czy się mylimy.
•  Ponieważ zapominamy o uruchomieniu testów akceptacyjnych, otrzymujemy naszą kompilację, która je za nas uruchomi. Nasza kompilacja powie nam, czy się mylimy.
•  Ponieważ nie pomyśleliśmy o każdym scenariuszu, zachęcamy testerów do zbadania systemu. Testerzy powiedzą nam, czy to źle.
•  Ponieważ uruchomiliśmy go tylko na laptopie Henry′ego, wdrażamy system w realistycznym środowisku. Testy powiedzą nam, czy to źle.
•  Ponieważ czasami źle rozumiemy naszego menedżera produktu i innych interesariuszy, prezentujemy system. Nasi interesariusze powiedzą nam, czy się mylimy.
•  Ponieważ nasz menedżer produktu czasami źle rozumie ludzi, którzy chcą systemu, uruchamiamy system produkcyjny. Ludzie, którzy tego chcą, mówią nam, czy się mylimy.
•  Ponieważ ludzie bardziej zauważają, że coś idzie źle niż dobrze, nie polegamy tylko na opiniach. Korzystamy z analiz i danych. Dane powiedzą nam, czy się mylimy.
•  Ponieważ rynek ciągle się zmienia, nawet jeśli wcześniej mieliśmy rację, w końcu się mylimy.
•  Ponieważ pomyłka kosztuje pieniądze, robimy te wszystkie rzeczy tak często, jak to możliwe. W ten sposób mylimy się tylko trochę.
•  Nie martw się, że zrobisz to dobrze. Martw się o to, skąd będziesz wiedział, że to źle i jak łatwo będzie to naprawić, gdy się dowiesz. Ponieważ prawdopodobnie jest źle.
•  W porządku, że się mylisz.

Strzelanie do wszystkich silników

Tradycyjne profilery Java używają instrumentacji kodu bajtowego lub próbkowania (pobierania śladów stosu w krótkich odstępach czasu), aby określić, gdzie poświęcono czas. Oba podejścia dodają własne skosy i osobliwości. Zrozumienie wyników tych profilerów jest sztuką samą w sobie i wymaga sporego doświadczenia. Na szczęście Brendan Gregg, inżynier ds. wydajności w Netflix, wymyślił wykresy płomieniowe, pomysłowy rodzaj diagramu śladów stosu, który można zebrać z prawie każdego systemu. Wykres płomienia sortuje i agreguje ślady aż do każdego poziomu stosu, tak aby ich liczba na poziom przedstawiała procent całkowitego czasu spędzonego w tej części kodu. Bardzo przydatne okazało się renderowanie tych bloków jako rzeczywistych bloków (prostokątów) o szerokości proporcjonalnej do wartości procentowych i układanie bloków jeden na drugim. "Płomienie" reprezentują od dołu do góry postęp od punktu wejścia programu lub wątku (pętla główna lub pętla zdarzeń) do liści wykonania na końcach płomieni. Zauważ, że kolejność od lewej do prawej nie ma znaczenia; często jest to po prostu sortowanie alfabetyczne. To samo dotyczy kolorów. Istotne są tylko względne szerokości i głębokości stosów. Możesz od razu zobaczyć, czy niektóre części programu zajmują nieoczekiwanie dużo czasu. Im wyżej na diagramie się dzieje, tym gorzej. Zwłaszcza jeśli masz bardzo szeroki płomień na górze, wiesz, że znalazłeś wąskie gardło, które nie deleguje pracy gdzie indziej. Po rozwiązaniu problemu ponownie wykonaj pomiar, a jeśli ogólny problem z wydajnością będzie się powtarzał, wróć do diagramu w celu uzyskania nowych wskazań. Aby rozwiązać wady tradycyjnych profilerów, wiele nowoczesnych narzędzi korzysta z wewnętrznej funkcji JVM (AsyncGetCallTrace), która umożliwia zbieranie śladów stosu poza punktami bezpieczeństwa. Dodatkowo łączą pomiar operacji JVM z kodem natywnym i wywołaniami systemowymi do systemu operacyjnego, dzięki czemu czas spędzony w sieci, wejścia/wyjścia (I/O) lub wyrzucanie śmieci może również stać się częścią wykresu płomienia. Narzędzia takie jak Honest Profiler, perf-map-agent, async-profiler, a nawet IntelliJ IDEA sprawiają, że przechwytywanie informacji i generowanie wykresów płomienia jest naprawdę łatwe. W większości przypadków wystarczy pobrać narzędzie, podać identyfikator procesu (PID) procesu Java i powiedzieć narzędziu, aby uruchomiło się przez określony czas i wygenerowało interaktywną, skalowalną grafikę wektorową (SVG):

# download and unzip async profiler for your OS from:
# https://github.com/jvm-profiling-tools/async-profiler
./profiler.sh -d -f flamegraph.svg -s -o svg && \
open flamegraph.svg -a "Google Chrome"

SVG, które tworzą narzędzia, jest nie tylko kolorowe, ale także interaktywne. Możesz powiększać sekcje, wyszukiwać symbole i nie tylko. Wykresy płomienia są imponująco potężnym narzędziem, które pozwala szybko uzyskać przegląd charakterystyki wydajności twoich programów; możesz natychmiast zobaczyć hotspoty i skupić się na nich. Uwzględnienie aspektów innych niż JVM pomaga również uzyskać szerszy obraz.

Postępuj zgodnie z nudnymi standardami

Na początku ery Javy na rynku istniały dziesiątki niekompatybilnych serwerów aplikacji, a producenci serwerów kierowali się zupełnie innymi paradygmatami. Niektóre serwery zostały nawet częściowo zaimplementowane w językach natywnych, takich jak C++. Zrozumienie wielu serwerów było trudne, a przeniesienie aplikacji z jednego serwera na drugi było prawie niemożliwe. Interfejsy API, takie jak JDBC (wprowadzone z JDK 1.1), JNDI (wprowadzone z JDK 1.3), JMS, JPA lub Servlets, streszczają, upraszczają i ujednolicają już ustanowione produkty. EJB i CDI sprawiły, że modele wdrażania i programowania były niezależne od dostawców. J2EE, później Java EE, a teraz Jakarta EE i MicroProfile zdefiniowały minimalny zestaw interfejsów API, który serwer aplikacji musiał zaimplementować. Wraz z nadejściem J2EE programista musiał jedynie znać zestaw interfejsów API J2EE, aby opracować i wdrożyć aplikację. Chociaż serwery ewoluowały, interfejsy API J2EE i Java EE pozostały kompatybilne. Nigdy nie trzeba było migrować aplikacji, aby działała na nowszej wersji serwera aplikacji. Nawet aktualizacja do wyższej wersji Java EE była bezbolesna. Wystarczyło tylko ponownie przetestować aplikację, nawet bez jej ponownej kompilacji. Tylko jeśli chciałeś skorzystać z nowszych interfejsów API, musiałeś dokonać refaktoryzacji aplikacji. Wraz z wprowadzeniem J2EE programiści mogli opanować wiele serwerów aplikacji bez zbytniego zagłębiania się w ich specyfikę. Mamy teraz bardzo podobną sytuację w ekosystemie web/JavaScript. Frameworki takie jak jQuery, Backbone.js, AngularJS 1, Angular 2+ (zupełnie inne niż AngularJS 1), ReactJS, Polymer, Vue.js i Ember.js stosują zupełnie inne konwencje i paradygmaty. Trudno jest opanować wiele frameworków jednocześnie. Początkowym celem wielu frameworków było rozwiązanie problemów z niekompatybilnością między różnymi przeglądarkami. Gdy przeglądarki stały się zaskakująco kompatybilne, frameworki zaczęły obsługiwać wiązanie danych, jednokierunkowy przepływ danych, a nawet korporacyjne funkcje Java, takie jak wstrzykiwanie zależności. W tym samym czasie przeglądarki stały się nie tylko bardziej kompatybilne, ale także udostępniały funkcje dostępne wcześniej tylko we frameworkach innych firm. Funkcja querySelector jest dostępna we wszystkich przeglądarkach i zapewnia funkcjonalność porównywalną do możliwości dostępu DOM jQuery. Web Components z Custom Elements, Shadow DOM i Templates umożliwiają programistom definiowanie nowych elementów zawierających interfejs użytkownika i zachowanie, a nawet strukturyzację całych aplikacji. Począwszy od ECMAScript 6, JavaScript stał się bardziej podobny do Javy, a moduły ES6 sprawiły, że bundling stał się opcjonalny. MDN (Mozilla Developer′s Framework) stał się wspólnym wysiłkiem Google, Microsoft, Mozilli, W3C i Samsunga, aby zapewnić dom dla standardów internetowych. Teraz możliwe jest również budowanie frontendów bez frameworków. Przeglądarki mają doskonałe osiągnięcia w zakresie kompatybilności wstecznej. Wszystkie frameworki muszą niezależnie korzystać z interfejsów API przeglądarki, więc poznając standardy, lepiej rozumiesz te frameworki. Dopóki przeglądarki nie wprowadzają żadnych przełomowych zmian, wystarczy oparcie się na standardach internetowych bez żadnych frameworków, aby Twoja aplikacja była trwała. Skupienie się na standardach pozwala na stopniowe zdobywanie wiedzy w czasie - skuteczny sposób uczenia się. Ocena popularnych frameworków jest ekscytująca, ale zdobyta wiedza niekoniecznie ma zastosowanie do następnej "gorącej rzeczy".

Częste wydania zmniejszają ryzyko

"Częste zwolnienia zmniejszają ryzyko" - to coś, co słyszysz cały czas w rozmowach o ciągłej dostawie. Jak dokładnie tak jest? Brzmi to sprzecznie z intuicją. Z pewnością częstsze wydawanie wprowadza do produkcji większą zmienność? Czy nie jest mniej ryzykowne wstrzymanie się z wydaniem tak długo, jak to możliwe, poświęcając czas na testowanie, aby zagwarantować zaufanie do pakietu? Zastanówmy się, co rozumiemy przez ryzyko.

Co to jest ryzyko?

Ryzyko jest czynnikiem prawdopodobieństwa wystąpienia awarii w połączeniu z najgorszym możliwym wpływem tej awarii:

Ryzyko = prawdopodobieństwo awarii × najgorszy przypadek wpływu awarii

Dlatego działalność o bardzo niskim ryzyku ma miejsce, gdy awaria jest niewiarygodnie mało prawdopodobna, a jej wpływ jest znikomy. Działania o niskim ryzyku obejmują również te, w których jeden z tych czynników - prawdopodobieństwo lub wpływ - jest tak niski, że poważnie ogranicza wpływ drugiego. Gra na loterii wiąże się z niskim ryzykiem: prawdopodobieństwo porażki (tj. nie wygranej) jest bardzo wysokie, ale wpływ porażki (tj. utraty kosztu losu) jest minimalny, więc granie w loterię ma niewiele negatywnych konsekwencji. Latanie wiąże się również z niskim ryzykiem, ponieważ czynniki równoważą się w odwrotny sposób. Szansa na awarię jest wyjątkowo niska, ma bardzo dobre wyniki w zakresie bezpieczeństwa - ale wpływ awarii jest niezwykle wysoki. Latamy często, ponieważ uważamy, że ryzyko jest bardzo niskie. Działania wysokiego ryzyka mają miejsce, gdy obie strony produktu są wysokie - wysokie prawdopodobieństwo awarii i duży wpływ. Na przykład obejmują sporty ekstremalne, takie jak wspinaczka w pojedynkę i nurkowanie jaskiniowe.

Duże, rzadkie wydania są bardziej ryzykowne

Skonsolidowanie zestawu zmian w jednym pakiecie wydawniczym zwiększa prawdopodobieństwo wystąpienia awarii - wiele zmian dzieje się jednocześnie. Najgorszy możliwy wpływ awarii obejmuje wydanie powodujące awarię lub poważną utratę danych. Każda zmiana w wydaniu może to spowodować. Reakcja próbowania i testowania na każdą awarię jest rozsądna, ale jest to niemożliwe. Możemy testować pod kątem znanych scenariuszy, ale nie możemy testować pod kątem scenariuszy, o których nie wiemy, dopóki nie zostaną napotkane ("nieznane niewiadome"). Nie oznacza to, że testowanie jest bezcelowe - wręcz przeciwnie, daje pewność, że zmiany nie złamały oczekiwanego, znanego zachowania. Trudną częścią jest zrównoważenie chęci przeprowadzenia dokładnych testów z prawdopodobieństwem wykrycia niepowodzenia testów oraz czasem poświęconym na ich wykonanie i utrzymanie. Twórz zautomatyzowany zestaw testów, które chronią przed scenariuszami awarii, o których wiesz. Za każdym razem, gdy napotkasz nową awarię, dodaj ją do zestawu testów. Zwiększ zestaw testów regresji, ale staraj się, aby były lekkie, szybkie i powtarzalne. Bez względu na to, ile testujesz, produkcja jest jedynym miejscem, w którym liczy się sukces. Małe, częste wydania zmniejszają prawdopodobieństwo awarii. Wersja zawierająca możliwie najmniejszą zmianę zmniejsza prawdopodobieństwo, że wersja będzie zawierać awarię. Nie ma sposobu, aby zmniejszyć wpływ awarii - w najgorszym przypadku wciąż jest to, że wydanie może spowodować awarię całego systemu i spowodować poważną utratę danych - ale zmniejszamy ogólne ryzyko przy mniejszych wydaniach. Często wprowadzaj małe zmiany, aby zmniejszyć prawdopodobieństwo niepowodzenia, a tym samym ryzyko zmiany.

Od puzzli do produktów

Zająłem się programowaniem, ponieważ było to łatwe. Cały dzień rozwiązywałem łamigłówki, po czym o piątej trzydzieści wracałem do domu i spotykałem się z przyjaciółmi. Dwadzieścia lat później pozostaję w oprogramowaniu, ponieważ jest to trudne. To trudne, bo od rozwiązywania łamigłówek przeszłam do uprawy produktów, od obsesji na punkcie poprawności optymalizacji pod kątem zmian. Na początku mojej kariery skoncentrowałem się na jednym obszarze systemu. Mój lider zespołu dał mi wymagania dotyczące nowych funkcji. To zdefiniowało "poprawne", a kiedy kod to osiągnął, moje zadanie zostało wykonane. Dostępne środki były ograniczone: pracowaliśmy w C, ze standardową biblioteką plus Oracle. Jeśli chodzi o punkty bonusowe, sprawiliśmy, że kod wyglądał jak wszyscy inni. W ciągu kilku lat moja perspektywa poszerzyła się: spotkałem się z klientami; Uczestniczyłem w negocjacjach pomiędzy projektem a realizacją. Jeśli konkretna nowa funkcja kierowała kod w niewygodnym kierunku, wracaliśmy do klienta z innymi sugestiami rozwiązania tego samego problemu. Teraz pomagam zdefiniować łamigłówki, a także je rozwiązać. Rozwiązywanie łamigłówek to warunek wstępny, a nie istota mojej pracy. Istotą mojej pracy jest zapewnienie zdolności reszcie organizacji (lub światu); Robię to, obsługując użyteczny produkt. Celem łamigłówek jest stan końcowy - podobnie jak gra w baseball, jest ustalony koniec. W przypadku produktów celem jest dalsze bycie użytecznym - podobnie jak kariera w baseballu, chcemy nadal grać. Łamigłówki mają określone środki, jak gra planszowa. Rozwijając produkty, mamy świat bibliotek i usług, mnóstwo rozwiązanych dla nas zagadek. To bardziej gra w udawanie, otwarta na to, co możemy znaleźć. W dalszej karierze moja perspektywa się poszerzyła. Kiedy wciskam satysfakcjonujący kod, to dopiero początek mojej pracy. Chcę czegoś więcej niż zmiany kodu: dążę do zmiany systemu. Nowa funkcja w mojej aplikacji musi działać z obecnymi systemami, które zależą od mojej. Współpracuję z osobami, które są właścicielami tych systemów, aby pomóc im zacząć korzystać z nowej funkcji. Teraz postrzegam swoją pracę jako projektowanie zmian, a nie kodowanie. Kod to szczegół. Projektowanie zmian oznacza flagi funkcji, zgodność wsteczną, migracje danych i stopniowe wdrażanie. Oznacza to dokumentację, pomocne komunikaty o błędach i kontakt społeczny z sąsiednimi zespołami. Plus: wszystkie te instrukcje if dotyczące flag funkcji, przestarzałych metod i obsługi kompatybilności wstecznej? Nie są już brzydkie. Wyrażają zmianę - a zmiana jest istotą, a nie jakimś konkretnym stanem kodu. Projektowanie zmiany oznacza budowanie obserwowalności, dzięki czemu mogę stwierdzić, kto nadal korzysta z przestarzałej funkcji, a kto czerpie wartość z nowej. Rozwiązując łamigłówki, nie musiałem dbać o to, czy ludziom podobała się ta funkcja, a nawet czy była w produkcji. Bardzo mi zależy na uprawie produktu. Z doświadczenia w produkcji uczymy się, jak uczynić nasze produkty bardziej użytecznymi. Produkty nie mają jednej definicji "poprawności". Wiele rzeczy jest zdecydowanie niepoprawnych, więc możemy uważać na "nie zepsute". Poza tym dążymy do "lepszego". Wyhodowanie produktu jest trudne w inny sposób niż rozwiązywanie zagadek. Zamiast ciężkiej pracy, po której następuje poczucie spełnienia, jest trud bzdurnej pracy, poprzez dwuznaczność, politykę i kontekst. Jednak nagroda to coś więcej niż uczucie: może mieć realny wpływ na Twoją firmę, a tym samym na świat. To jest bardziej satysfakcjonujące niż zwykła zabawa.

"Pełny programista" to sposób myślenia

W 2007 roku - w którym zacząłem swoją pierwszą pracę jako programista Java - spektrum technologii związanych z codziennym tworzeniem stron internetowych było dość wąskie. W większości przypadków relacyjne bazy danych były jedynym typem bazy danych, który programista musiał znać. Tworzenie frontendu ograniczało się do HTML i CSS, doprawione odrobiną JavaScriptu. Sam programowanie w języku Java oznaczało przede wszystkim pracę z Hibernate oraz Spring lub Struts. Ten zestaw technologii obejmował prawie wszystko, co w tamtym czasie było potrzebne do budowania aplikacji. Większość programistów Javy była w rzeczywistości programistami full-stack, chociaż termin ten nie został jeszcze ukuty. Od 2007 roku sytuacja znacznie się zmieniła. Zaczęliśmy budować coraz bardziej złożone interfejsy użytkownika i radzić sobie z tą złożonością za pomocą zaawansowanych frameworków JavaScript. Używamy teraz baz danych NoSQL i prawie każda z nich bardzo różni się od pozostałych. Przesyłamy dane za pomocą Kafki, wiadomości za pomocą RabbitMQ i robimy dużo więcej. W wielu przypadkach jesteśmy również odpowiedzialni za konfigurację lub utrzymanie infrastruktury za pomocą Terraform lub CloudFormation, a także używamy lub nawet konfigurujemy klastry Kubernetes. Ogólna złożoność wzrosła do tego stopnia, że mamy osobne stanowiska dla programisty frontendu, programisty backendu i inżyniera DevOps. Czy nadal można być programistą full-stack? To zależy od tego, jak rozumiesz ten termin. Nie możesz być ekspertem we wszystkim. Biorąc pod uwagę, jak bardzo urósł ekosystem Javy, trudno jest nawet być ekspertem w samej Javie. Dobrą rzeczą jest to, że nie musisz nim być. W przypadku wielu projektów, zwłaszcza w mniejszych firmach, najkorzystniejsza konfiguracja zespołu polega na tym, że każdy obszar wiedzy jest obsługiwany przez co najmniej jednego eksperta, ale ci eksperci nie ograniczają się do pracy tylko w tym jednym obszarze. Deweloperzy specjalizujący się w tworzeniu usług backendowych mogą pisać kod frontendowy - nawet jeśli kod nie jest doskonały - i to samo dotyczy programistów frontendowych. Pomaga to przyspieszyć realizację projektów, ponieważ jedna osoba może opracować zmianę, która wymaga dotknięcia każdej warstwy aplikacji. Prowadzi to również do większego zaangażowania podczas spotkań doskonalących, ponieważ nie ma zadań wydzielonych tylko do określonej grupy osób. Co najważniejsze, nieograniczanie się ściśle do jednego obszaru zmienia sposób podejścia do zadań. Nie ma już dyskusji "To nie jest moja praca" - programiści są zachęcani do nauki. Wyjazd jednej osoby na wakacje nie stanowi problemu, ponieważ zawsze są inne, które mogą je kryć - może nie tak skutecznie, a może z wynikami, które nie są tak dobre, ale wystarczające, aby wszystko posuwało się do przodu. Oznacza to również, że gdy istnieje potrzeba wprowadzenia nowej technologii do stosu, nie trzeba szukać nowego członka zespołu, ponieważ obecni członkowie zespołu już teraz swobodnie opuszczają strefę komfortu swojej wiedzy. Programista full-stack jest zatem nastawiony na sposób myślenia. To bycie seniorem i juniorem w tym samym czasie, z nastawieniem na wszystko.

Wywóz śmieci jest twoim przyjacielem

Biedny stary zbiór śmieci. Jeden z niedocenionych bohaterów Jawy, często obwiniany, rzadko chwalony. Zanim Java stała się głównym nurtem zbierania śmieci, programiści nie mieli innego wyboru, jak tylko śledzić całą pamięć, którą przydzielili ręcznie, i cofać jej alokację, gdy nic już jej nie używa. To jest trudne. Nawet przy dyscyplinie ręczne cofanie alokacji jest częstą przyczyną wycieków pamięci (jeśli jest za późno) i awarii (jeśli jest za wcześnie). Java GC (zbieranie śmieci) jest często uważane za niezbędny koszt, a "skrócenie czasu spędzonego w GC" jest powszechną wskazówką dotyczącą wydajności. Jednak współczesne usuwanie śmieci może być szybsze niż malloc/free, a czas spędzony w GC może przyspieszyć wszystko. Czemu? Odśmiecacze zajmują się czymś więcej niż usuwaniem alokacji pamięci: zajmują się również alokacją pamięci i rozmieszczeniem obiektów w pamięci. Dobry algorytm zarządzania pamięcią może usprawnić alokację, zmniejszając fragmentację i rywalizację. Może również zwiększyć przepustowość i skrócić czasy odpowiedzi, zmieniając rozmieszczenie obiektów. Dlaczego lokalizacja obiektu w pamięci wpływa na wydajność aplikacji? Duża część czasu wykonywania programu jest zablokowana na sprzęcie, czekając na dostęp do pamięci. Dostęp do sterty jest geologicznie powolny w porównaniu z przetwarzaniem instrukcji, dlatego współczesne komputery używają pamięci podręcznych. Kiedy obiekt jest pobierany do pamięci podręcznej procesora, jego sąsiedzi są również wprowadzani; jeśli zdarzy się, że będą dostępne w następnej kolejności, dostęp ten będzie szybki. Posiadanie w pamięci obiektów, które są używane w tym samym czasie, nazywa się lokalizacją obiektu i jest to wygrana wydajności. Korzyści płynące z efektywnej alokacji są bardziej oczywiste. Jeśli sterta jest pofragmentowana, gdy program próbuje utworzyć obiekt, będzie musiał długo szukać, aby znaleźć wystarczająco duży fragment wolnej pamięci, a alokacja staje się kosztowna. Jako eksperyment możesz zmusić GC do większego kompaktowania; znacznie zwiększy obciążenie GC, ale często wydajność aplikacji ulegnie poprawie. Strategie GC różnią się w zależności od implementacji JVM, a każda JVM oferuje szereg konfigurowalnych opcji. Domyślne ustawienia JVM to zazwyczaj dobry początek, ale warto poznać niektóre z możliwych mechanizmów i wariacji. Przepustowość można skompensować z opóźnieniem, a obciążenie pracą wpływa na optymalny wybór. Kolekcjonerzy zatrzymujący świat wstrzymują wszelką aktywność programu, aby mogli bezpiecznie zbierać. Współbieżne kolektory odciążają prace związane z gromadzeniem danych na wątki aplikacji, dzięki czemu nie występują globalne pauzy; zamiast tego każdy wątek będzie miał niewielkie opóźnienia. Chociaż nie mają wyraźnych przerw, współbieżne kolektory są mniej wydajniejsze niż stop-the-world, dzięki czemu nadają się do zastosowań, w których zostaną zauważone przerwy (takich jak odtwarzanie muzyki lub GUI). Samo zbieranie odbywa się poprzez kopiowanie lub przez znakowanie i zamiatanie. Dzięki markand-sweep sterta jest przeszukiwana w celu zidentyfikowania wolnego miejsca, a nowe obiekty są przydzielane do tych luk. Kolektory kopiujące dzielą hałdę na dwa obszary. Obiekty są alokowane w "nowej przestrzeni". Gdy ta przestrzeń jest pełna, jej nieśmieciowa zawartość jest kopiowana do przestrzeni rezerwowej, a spacje są zamieniane. Przy typowym obciążeniu pracą większość obiektów umiera młodo (jest to znane jako hipoteza pokoleniowa). W przypadku obiektów krótko żyjących krok kopiowania będzie bardzo szybki (nie ma co kopiować!). Jeśli jednak obiekty będą się kręcić, zbieranie będzie nieefektywne. Kolektory kopiujące są świetne w przypadku niezmiennych obiektów i katastrofy z "optymalizacją" puli obiektów (i tak zwykle jest to zły pomysł). Jako bonus, kolekcjonerzy kopiujący kompaktują stertę, co umożliwia niemal natychmiastową alokację obiektów i szybki dostęp do obiektów (mniej chybień w pamięci podręcznej). Oceniając wyniki, należy je odnieść do wartości biznesowej. Optymalizuj transakcje na sekundę, średni czas obsługi lub opóźnienie w najgorszym przypadku. Ale nie próbuj mikrooptymalizować czasu spędzonego w GC, ponieważ czas zainwestowany w GC może faktycznie pomóc przyspieszyć program.

Bądź lepszy w nazywaniu rzeczy

Lepsze nazewnictwo poprawia łatwość utrzymania kodu, który piszesz bardziej niż cokolwiek innego. Kod, który można utrzymać, to coś więcej niż dobre nazewnictwo, ale nazywanie rzeczy jest trudne i zwykle zaniedbywane. Na szczęście programiści lubią wyzwania. Po pierwsze, unikaj nazw, które są bez znaczenia (foo) lub zbyt abstrakcyjne (dane), powielone (data2) lub niejasne (DataManager), skrócone lub krótkie (dat). Najgorsze są pojedyncze litery (d). Nazwy te są niejednoznaczne, co spowalnia wszystkich, ponieważ programiści spędzają więcej czasu na czytaniu kodu niż na pisaniu kodu. Następnie przyjmij wytyczne dotyczące lepszych nazw - słów o precyzyjnym znaczeniu, które sprawiają, że kod mówi, co oznacza. Użyj do czterech słów dla każdej nazwy i nie używaj skrótów (z wyjątkiem id i tych, które adoptujesz z problematycznej domeny). Jedno słowo rzadko wystarcza; używanie więcej niż czterech jest niezdarne i przestaje dodawać znaczenie. Programiści Java używają długich nazw klas, ale często preferują krótkie nazwy zmiennych lokalnych, nawet jeśli są gorsze. Naucz się i używaj terminologii domeny problemowej - wszechobecnego słownictwa związanego z projektowaniem opartym na domenach. Często jest to zwięzłe: w publikacji, poprawny termin w przypadku zmian tekstu może być korekta lub edycja, w zależności od tego, kto wprowadza zmianę. Zamiast wymyślać słowa, przeczytaj stronę Wikipedii danego tematu, porozmawiaj z osobami pracującymi w tej domenie i dodaj używane przez nich słowa do swojego glosariusza. Zastąp liczbę mnogą rzeczownikami zbiorowymi (np. zmień nazwę lista_spotkań na kalendarz). Ogólnie rzecz biorąc, poszerz swoje słownictwo angielskie, dzięki czemu możesz skrócić i skrócić imiona. Jest to trudniejsze, jeśli nie jesteś native speakerem języka angielskiego, ale i tak każdy musi nauczyć się żargonu domenowego. Zmień nazwy par jednostek z nazwami relacji (na przykład zmień nazwę firmy_osoba na pracownik, właściciel, udziałowiec). Kiedy to jest pole, nazywasz relację między typem pola a klasą, do której należy. Ogólnie rzecz biorąc, często warto wyodrębnić nową zmienną, metodę lub klasę tylko po to, aby wyraźnie ją nazwać. Java pomaga w dobrym nazewnictwie, ponieważ nazywasz klasy niezależnie od obiektów. Nie zapomnij nazwać swoich typów zamiast polegać na klasach prymitywnych i JDK: zamiast String powinieneś zwykle wprowadzić klasę o bardziej szczegółowej nazwie, np. CustomerName. W przeciwnym razie potrzebujesz komentarzy, aby udokumentować niedopuszczalne ciągi, takie jak puste. Nie mieszaj nazw klas i obiektów: zmień nazwę pola daty o nazwie dateCreated na utworzone i pola logicznego o nazwie isValid na prawidłowe, aby uniknąć duplikatów typu. Nadaj obiektom różne nazwy: zamiast klienta o nazwie klient użyj bardziej szczegółowej nazwy, takiej jak odbiorca podczas wysyłania powiadomienia lub recenzent podczas publikowania recenzji produktu. Pierwszym krokiem w nazewnictwie jest zastosowanie podstawowych konwencji nazewnictwa, takich jak używanie wyrażeń rzeczownikowych dla nazw klas. Następnym krokiem jest dobra technika nazewnictwa przy użyciu takich wskazówek. Ale wytyczne mają swoje granice. Specyfikacja JavaBeans nauczyła pokolenie programistów Javy, jak łamać enkapsulację obiektów i używać niejasnych nazw metod, jak na przykład setRating, gdy szybkość może być lepsza. Nie musisz nazywać metod, które nie są konieczne w przypadku fraz czasownikowych, jak w przypadku interfejsów API konstruktora, takich jak Customer.instance().rating(FIVE_STARS).active(). Ostatecznie mistrzostwo w nazewnictwie polega na wyborze zasad, które należy złamać.

Hej Fred, czy możesz podać mi hashmapę?

Wyobraź sobie tę scenę: stare, ciasne biuro z kilkoma starymi drewnianymi biurkami ustawionymi jeden przy drugim. Na każdym biurku znajdował się stary czarny telefon obrotowy i popielniczki. Na jednym z biurek znajduje się czarna mapa HashMap zawierająca tablicę ArrayList wypełnioną danymi klientów. Sam, chcąc skontaktować się z Acme Inc., skanuje biuro w poszukiwaniu HashMap. Z rozbieganymi oczami dostrzega HashMapę i krzyczy: "Hej Fred, czy możesz przekazać mi HashMapę?". Czy możesz to sobie wyobrazić, tak, nie myślałem. Ważną częścią pisania programu jest rozwijanie słownictwa. Każde słowo w tym słowniku powinno być wyrazem czegoś, co jest częścią domeny, którą modelujemy. W końcu jest to wyrażenie kodu naszego modelu, które inni będą musieli przeczytać i zrozumieć. W związku z tym nasz wybór słownictwa może pomóc lub utrudnić zrozumienie naszego kodu. Co dziwne, wybór słownictwa ma znacznie większy wpływ niż czytelność: słowa, których używamy, wpływają na to, jak myślimy o danym problemie, co z kolei wpływa na strukturę naszego kodu, nasz wybór algorytmów, jak kształtujemy nasze API, jak dobrze system będzie odpowiadał naszym celom, jak łatwo będzie go konserwować i rozbudowywać i wreszcie, jak dobrze będzie działać. Tak, słownictwo, które rozwijamy podczas pisania kodu, ma duże znaczenie. Tak bardzo, że trzymanie słownika pod ręką może być dziwnie przydatne podczas pisania kodu. Wracając do śmiesznego przykładu, oczywiście nikt nie poprosiłby o HashMap. Najprawdopodobniej zwróciłbyś pusty wzrok Freda, gdybyś poprosił go o zdanie HashMapy. Kiedy jednak przyglądamy się modelowaniu domeny, słyszymy o potrzebie wyszukiwania danych kontaktowych klientów uporządkowanych według nazwy. To krzyczy HashMap. Jeśli zagłębimy się w tę domenę, prawdopodobnie odkryjemy, że informacje kontaktowe są zapisane na karcie indeksowej, która jest starannie zapakowana w Rolodex. Zastąpienie słowa HashMap słowem Rolodex nie tylko zapewnia lepszą abstrakcję w naszym kodzie, ale również będzie miało natychmiastowy wpływ na to, jak myślimy o danym problemie, i oferuje lepszy sposób na wyrażenie naszych myśli czytelnikowi naszego kod. Wniosek jest taki, że zajęcia techniczne rzadko mają miejsce w słownictwie dziedzin, w których pracujemy. Zamiast tego oferują cegiełki do głębszych, bardziej znaczących abstrakcji. Potrzeba klas użytkowych powinna być sygnałem ostrzegawczym, że brakuje abstrakcji. Dodatkowo klasy techniczne w API również powinny być sygnałem ostrzegawczym. Rozważmy na przykład przypadek, w którym podpis metody przyjmuje String do reprezentowania imienia i String do nazwiska. Są one używane do wyszukiwania danych przechowywanych w HashMap:

return listOfNames.get(firstName + lastName); Pytanie brzmi, czym jest brakująca abstrakcja? Posiadanie dwóch pól tworzących klucz jest powszechnie znane jako klucz złożony. Korzystając z tej abstrakcji otrzymujemy: return listOfNames.get(new CompositeKey(firstName, lastName)); Po wprowadzeniu tej zmiany w teście, kod działa trzy razy szybciej. Twierdzę, że jest to również bardziej wyraziste: używanie CompositeKey lepiej wyraża istotę problemu.

Jak uniknąć null

Tony Hoare nazywa zerem "błąd miliarda dolarów". To pomyłka i dlatego powinieneś wyrobić w sobie nawyk zabraniania kodowi używania wartości null. Jeśli masz odwołanie do obiektu, który może mieć wartość null, musisz pamiętać o sprawdzeniu wartości null przed próbą wywołania jakiejkolwiek jego metody. Ale ponieważ nie ma oczywistej różnicy między odwołaniem o wartości null a niepustą, zbyt łatwo jest zapomnieć i uzyskać wyjątek NullPointerException. Najbardziej przyszłościowym sposobem uniknięcia problemów jest skorzystanie z alternatywy, jeśli to możliwe.

Unikaj inicjowania zmiennych do wartości Null

Zwykle nie jest dobrym pomysłem deklarowanie zmiennej, dopóki nie wiadomo, jaką wartość powinna ona posiadać. W przypadku złożonej inicjalizacji przenieś całą logikę inicjalizacji do metody. Na przykład zamiast tego:

public String getEllipsifiedPageSummary(Path path) {
String summary = null;
Resource resource = this.resolver.resolve(path);
if (resource.exists()) {
ValueMap properties = resource.getProperties();
summary = properties.get("summary");
} else {
summary = "";
}
return ellipsify(summary);
}
Do the following:
public String getEllipsifiedPageSummary(Path path) {
var summary = getPageSummary(path);
return ellipsify(summary);
}p
ublic String getPageSummary(Path path) {
var resource = this.resolver.resolve(path);
if (!resource.exists()) {
return "";
}
var properties = resource.getProperties();
return properties.get("summary");
}

Inicjowanie zmiennej do wartości null może przypadkowo przeciekać wartość null, jeśli nie jesteś ostrożny z kodem obsługi błędów. Inny programista może zmienić przepływ sterowania, nie zdając sobie sprawy z problemu - a ten inny programista może być tobą trzy miesiące po pierwszym napisaniu kodu.

Unikaj zwracania wartości Null

Kiedy czytasz sygnaturę metody, powinieneś być w stanie zrozumieć, czy zawsze zwraca ona T, czy czasami nie. Zwrócenie Optional to lepsza opcja, która sprawia, że kod jest bardziej jawny. Interfejs API Optional bardzo ułatwia radzenie sobie ze scenariuszem, w którym nie został wyprodukowany T.

Unikaj przekazywania i odbierania parametrów zerowych

Jeśli potrzebujesz T, poproś o to; jeśli możesz obejść się bez niego, nie proś o to. W przypadku operacji, która może mieć opcjonalny parametr, utwórz dwie metody: jedną z parametrem i drugą bez. Na przykład metoda drawImage z klasy Graphics w JDK ma wersję, która otrzymuje pięć parametrów i szósty parametr, ImageObserver, który jest opcjonalny. Jeśli nie masz ImageObserver, musisz przekazać null w ten sposób:

g.drawImage(original, X_COORD, Y_COORD, IMG_WIDTH, IMG_HEIGHT, null);

Byłoby lepiej mieć inną metodę tylko z pierwszymi pięcioma parametrami.

Dopuszczalne wartości null

Kiedy w takim razie dopuszczalne jest użycie wartości null? Jako szczegół implementacji klasy, tj. wartość atrybutu. Kod, który musi być świadomy braku wartości, znajduje się w tym samym pliku i znacznie łatwiej jest to uzasadnić i nie przeciekać wartości null. Pamiętaj więc, że jeśli nie masz atrybutu, zawsze można uniknąć znakowania null przy użyciu lepszej konstrukcji w kodzie. Jeśli przestaniesz używać wartości null tam, gdzie jej nie potrzebujesz, wyciek null i wyjątek NullPointerException staną się niemożliwe. A jeśli unikniesz tych wyjątków, będziesz częścią rozwiązania problemu miliardów dolarów, zamiast być jego częścią.

Jak zepsuć maszynę JVM?

Jest tak wiele nowych interfejsów API, fajnych bibliotek i technik, które musisz wypróbować, że trudno jest być na bieżąco. Ale czy to naprawdę wszystko, co musisz wiedzieć jako programista Java? A co ze środowiskiem, w którym działa Twoje oprogramowanie? Czy nie może być tak, że problem tutaj może spowodować awarię twojego oprogramowania, a ty nawet nie będziesz w stanie zrozumieć lub znaleźć tego problemu, ponieważ jest on poza światem bibliotek i kodu? Czy jesteś gotów rozważyć inną perspektywę? Oto wyzwanie: spróbuj znaleźć sposoby na awarię wirtualnej maszyny Java! (Albo przynajmniej zatrzymaj jego normalne działanie w nagłym i nieoczekiwanym zatrzymaniu). Im więcej sposobów znasz, tym lepiej rozumiesz swoje otoczenie i doceniasz, co może się nie udać z działającym systemem oprogramowania. Oto kilka, od których możesz zacząć:

1. Spróbuj przydzielić jak najwięcej pamięci. Pamięć RAM nie jest nieskończona - jeśli nie można przydzielić więcej pamięci RAM, alokacja zakończy się niepowodzeniem.

2. Spróbuj zapisywać dane na dysku twardym, aż się zapełni. Ten sam problem, co w przypadku pamięci RAM: choć większa niż pamięć RAM, przestrzeń dyskowa też nie jest nieskończona.

3. Spróbuj otworzyć jak najwięcej plików. Czy znasz maksymalną liczbę deskryptorów plików dla twojego środowiska?

4. Spróbuj stworzyć jak najwięcej wątków. W systemie Linux, możesz zajrzeć do /proc/sys/kernel/pid_max, a zobaczysz, ile procesów może działać w twoim systemie. Ile wątków możesz utworzyć w swoim systemie?

5. Spróbuj zmodyfikować własne pliki .class w systemie plików - bieżące uruchomienie Twojej aplikacji będzie ostatnim!

6. Spróbuj znaleźć własny identyfikator procesu, a następnie spróbuj go zabić za pomocą Runtime.exec (np. wywołując kill -9 na swoim identyfikatorze procesu).
7. Spróbuj utworzyć klasę w czasie wykonywania, która wywołuje tylko System.exit, załaduj tę klasę dynamicznie za pomocą programu ładującego klas, a następnie wywołaj ją.

8. Spróbuj otworzyć jak najwięcej połączeń gniazdowych. W systemie Unix maksymalna liczba możliwych połączeń gniazd jest równa maksymalnej liczbie deskryptorów plików (często 2048). Ile jest dostępnych tam, gdzie działa Twoja aplikacja?

9. Spróbuj włamać się do systemu. Pobierz exploita za pomocą kodu lub za pomocą wget. Uruchom exploit, a następnie wywołaj shutdown -h jako root w systemie Unix lub shutdown /s jako administrator w systemie Windows.

10. Spróbuj skakać bez siatki zabezpieczającej. Część bezpieczeństwa Javy pochodzi z projektu języka, a część z weryfikacji kodu bajtowego w Twojej JVM. Uruchom maszynę JVM z opcją -noverify lub -Xverify:none, która wyłącza weryfikację kodu bajtowego, i napisz coś, czego w innym przypadku nie można by uruchomić.

11. Spróbuj użyć niebezpiecznego. Ta klasa backdoora służy do uzyskiwania dostępu do obiektów niskiego poziomu, takich jak zarządzanie pamięcią. Cała składnia Java, całe bezpieczeństwo C!

12. Spróbuj przejść na natywny. Napisz jakiś kod natywny. Cała składnia C, całe bezpieczeństwo C! Spróbuj znaleźć własne sposoby na awarię JVM i poproś współpracowników o ich pomysły. Zastanów się również nad pytaniem kandydatów na rozmowę kwalifikacyjną, jak mogą się do tego zabrać. Niezależnie od odpowiedzi, wkrótce dowiesz się, czy rozmówca jest w stanie zobaczyć świat za oknem IDE.

PS Jeśli znajdziesz inne kreatywne sposoby na awarię JVM, daj mi znać!

powtarzalności i audytowalności dzięki ciągłemu dostarczaniu

Rękodzieło jest cenione ze względu na włożony czas i wysiłek oraz drobne niedoskonałości, które nadają charakteru i niepowtarzalności. Chociaż te cechy mogą być cenione w żywności, meblach lub sztuce, jeśli chodzi o dostarczanie kodu, te cechy są poważną przeszkodą dla sukcesu organizacji. Ludzie nie są dobrze przystosowani do wykonywania powtarzalnych zadań. Bez względu na to, jak bardzo dana osoba jest zorientowana na szczegóły, podczas wykonywania szeregu złożonych kroków wymaganych do wdrożenia aplikacji zdarzają się błędy. Krok może zostać pominięty, uruchomiony w niewłaściwym środowisku lub w inny sposób wykonany nieprawidłowo, prowadząc do niepowodzenia wdrożenia. W przypadku niepowodzenia wdrożenia można poświęcić znaczną ilość czasu na zbadanie, co poszło nie tak. Ten proces badawczy jest utrudniony, ponieważ procesy ręczne często nie mają centralnego punktu kontroli i mogą być nieprzejrzyste. Po ustaleniu pierwotnej przyczyny typowym rozwiązaniem jest dodanie większej liczby warstw kontroli, aby zapobiec ponownemu wystąpieniu problemu, ale zwykle udaje się to tylko uczynić proces wdrażania bardziej skomplikowanym i bolesnym! Organizacje walczące o dostarczenie kodu nie są nowością, więc aby rozwiązać ten problem, organizacje zaczęły migrować do ciągłego dostarczania (CD). CD to podejście polegające na automatyzacji etapów dostarczania kodu do produkcji. Od momentu, gdy programista zatwierdzi zmianę, do momentu wdrożenia tej zmiany w środowisku produkcyjnym, każdy krok, który można zautomatyzować, powinien być testowaniem, kontrolą zmian, procesem wdrażania itp. Podczas migracji na CD główną motywacją jest skrócić czas i wysiłek wymagany do wdrożenia kodu. Chociaż skrócony czas i wysiłek to znaczące zalety płyt CD, nie są one jedynymi! CD poprawia również powtarzalność i możliwość kontroli procesu wdrażania. Oto dlaczego powinieneś dbać o te cechy.

Powtarzalne

Automatyzacja kroków wdrażania kodu oznacza skryptowanie każdego kroku, aby mógł być wykonywany przez komputer zamiast człowieka. To znacznie poprawia powtarzalność procesu wdrażania, ponieważ komputery doskonale sprawdzają się w wykonywaniu powtarzalnych zadań. Powtarzalny proces jest z natury mniej ryzykowny, co może zachęcać organizacje do częstszego publikowania i przy mniejszych zestawach zmian. Może to prowadzić do drugorzędnych korzyści związanych z kierowaniem wersji w celu rozwiązania określonych problemów, takich jak wydajność. Wydanie może zawierać tylko zmiany wydajności, co umożliwia zmierzenie, czy te zmiany uległy poprawie, pogorszeniu lub nie miały wpływu na wydajność.

Audytowalny

Automatyzacja wdrożeń znacznie poprawia przejrzystość, co w naturalny sposób poprawia możliwości inspekcji. Skrypty używane do wykonywania kroków i dostarczanych do nich wartości mogą być przechowywane w kontroli wersji, co pozwala na łatwe przeglądanie. Zautomatyzowane wdrożenia mogą również generować raporty, które mogą również pomóc w inspekcji. Ulepszona możliwość kontroli procesu wdrażania sprawia, że CD z koncepcji niszowej dla start-upów i aplikacji, które nie mają kluczowego znaczenia, staje się niezbędne w nawet najbardziej ściśle regulowanych i kontrolowanych branżach. Kiedy po raz pierwszy usłyszałem o CD, koncepcja wdrożeń na żądanie mnie odurzyła. Po przeczytaniu Continuous Delivery autorstwa Jeza Humble'a i Davida Farley'a (Addison-Wesley), dowiedziałem się, że skrócony czas i wysiłek są pod wieloma względami drugorzędne w stosunku do powtarzalności i sprawdzalności, jakie oferuje płyta CD. Jeśli Twoja organizacja ma problemy z dostarczeniem kodu do produkcji, mam nadzieję, że pomoże to w stworzeniu sprawy dla kierownictwa, dlaczego powinieneś przejść na CD.

W wojnach językowych Java ma swoją własną

Wszyscy wybieramy nasze ulubione i bagatelizujemy inne opcje (kolory, samochody, drużyny sportowe itd.). Wybór języka programowania nie jest zwolniony. Niezależnie od tego, czy jest to ten, z którym czujemy się najbardziej komfortowo, czy ten, który dał nam pracę, trzymamy się tego wyboru. Dzisiaj skupimy się na Javie. Dla tego języka są jak najbardziej uzasadnione skargi i pochwały. To są moje doświadczenia, a inni mogą widzieć rzeczy inaczej.

Moja historia z Javą

Najpierw spójrzmy przez pryzmat, przez który patrzę na ten język. Moje wprowadzenie do programowania aplikacji odbyło się na studiach przy użyciu - poczekaj na to - Java. Wcześniej miałem kilka klas wprowadzających używających HTML, Alice i Visual Basic. Żaden z nich nie został zaprojektowany, aby zanurzyć się w złożonych strukturach kodu. Tak więc Java była moim pierwszym kontaktem z programowaniem dla środowisk korporacyjnych i krytycznych procesów. Od tego czasu mam doświadczenie z wieloma innymi językami, ale wciąż wracam do Javy.

Projekt i tło Java

Java została stworzona w 1995 roku ze składnią podobną do C i zgodnie z zasadą WORA (napisz raz, uruchom wszędzie). Jego celem było uproszczenie złożonego programowania wymaganego w językach z rodziny C i osiągnięcie niezależności platformy za pośrednictwem JVM. Myślę, że znajomość historii języka pomaga umieścić pozytywy i negatywy w kontekście, ponieważ zrozumienie tła pokazuje, co twórcy poświęcili, aby osiągnąć inne cele.

Wady Javy

Większość skarg dotyczy tego, że wdrożenia są większe, a składnia jest pełna. Chociaż jest słuszny, myślę, że poprzedni akapit dotyczący historii Javy wyjaśnia, dlaczego takowe istnieją. Po pierwsze, wdrożenia Java są ogólnie większe. Jak widzieliśmy w historii Javy, została ona stworzona, aby "napisać raz, uruchomić w dowolnym miejscu", aby ta sama aplikacja mogła działać na dowolnej JVM. Oznacza to, że wszystkie zależności muszą zostać uwzględnione w celu wdrożenia, niezależnie od tego, czy są one wprowadzane do pojedynczego pliku JAR, czy w różnych komponentach (plik WAR + serwer aplikacji + JRE + zależności). Ma to wpływ na rozmiar wdrożenia. Po drugie, Java jest gadatliwa. Ponownie przypisuję to jego projektowi. Powstał, gdy C i podobne języki rządziły przestrzenią, co wymagało od programistów określenia szczegółów niskopoziomowych. Celem Javy było być bardziej przyjaznym dla użytkownika poprzez ** abstrakcję niektórych z tych szczegółów.

Dlaczego lubię Javę

•  Java mówi mi, co buduję i jak. W przypadku innych języków mogę być w stanie napisać coś w mniejszej liczbie linijek, ale mniej jestem pewien, co robi pod maską, co nie lubię tak bardzo.
•  To szeroko stosowana umiejętność. Zajmowanie się Javą na różnych stanowiskach dało mi wiedzę zarówno na rynku biznesowym, jak i technicznym. Java nie jest jedynym językiem posiadającym tę zaletę, ale wydaje się, że jest to najtrwalszy język z tą właściwością.
•  Java pozwala mi bawić się technologią we wszystkich stosach i obszarach. Wydaje się, że łączy to wszystko. Lubię bawić się i odkrywać, a Java umożliwiła to.

Co to oznacza dla programistów?

Rynek jest zróżnicowany, z wieloma opcjami dopasowanymi do potrzeb biznesowych. Jeden rozmiar nie pasuje (i nie powinien) do wszystkich, więc każdy programista musi wybrać najlepszy język do pracy. Nawet jeśli nie preferujesz Java jako podstawowego języka, nadal uważam, że posiadanie tej umiejętności jest cenną umiejętnością

Myślenie w linii

Komputery się zmieniły. Zmieniły się na wiele sposobów, ale na potrzeby tego tekstu zmieniły się w jeden istotny: względny koszt odczytu z pamięci RAM stał się niezwykle wysoki. To było coś, co działo się stopniowo, dopóki dostęp do pamięci RAM nie mógł całkowicie zdominować wskaźników wydajności aplikacji. Procesor ciągle czekał na zakończenie dostępu do pamięci. A ponieważ koszt korzystania z pamięci RAM w stosunku do rejestrów rósł i rósł, producenci chipów wprowadzali coraz więcej poziomów pamięci podręcznej i zwiększali je. A skrytki są świetne! Jeśli to, czego potrzebujesz, jest w nich. Pamięci podręczne są złożone, ale z reguły przewidują, że kolejny dostęp do pamięci będzie blisko, a najlepiej obok ostatniego, poprzedniego dostępu. Odbywa się to poprzez pobranie z pamięci nieco więcej niż potrzeba i przechowywanie tego nadmiaru w pamięci podręcznej, często nazywanej pobieraniem wstępnym. Jeśli późniejszy dostęp może uzyskać swoją wartość z pamięci podręcznej zamiast z pamięci RAM, jest określany jako "przyjazny dla pamięci podręcznej" dostęp. Wyobraź sobie, że musisz przejść przez dużą liczbę stosunkowo małych obiektów, może kilka trójkątów. W dzisiejszej Javie tak naprawdę nie ma tablicy trójkątów; masz tablicę wskaźników do obiektów trójkątów, ponieważ zwykłe obiekty w Javie są "typami referencyjnymi", co oznacza, że uzyskujesz do nich dostęp poprzez wskaźniki/referencje Java. Tak więc, mimo że tablica jest prawdopodobnie ciągłą sekcją pamięci, same obiekty trójkąta mogą znajdować się w dowolnym miejscu na stercie Java. Pętla w tej tablicy będzie "nieprzyjazna dla pamięci podręcznej", ponieważ będziemy przeskakiwać w pamięci od obiektu trójkąta do obiektu trójkąta, a pobieranie z wyprzedzeniem pamięci podręcznej prawdopodobnie nam nie pomoże. Wyobraź sobie, że tablica zawierała rzeczywiste obiekty trójkąta, a nie wskaźniki do nich. Teraz są blisko pamięci, a pętla nad nimi jest znacznie bardziej "przyjazna dla pamięci podręcznej". Następny trójkąt może na nas czekać w skrytce. Typy obiektów, które mogą być przechowywane bezpośrednio w takiej tablicy, nazywane są "typami wartości" lub "typami wbudowanymi". Java ma już kilka wbudowanych typów, na przykład int i char, i wkrótce będą miały zdefiniowane przez użytkownika, prawdopodobnie nazywane "klasami wbudowanymi". Będą one podobne do zwykłych klas, ale prostsze. Innym sposobem na bycie przyjaznym dla pamięci podręcznej jest przechowywanie obiektów w ramce stosu lub bezpośrednio w rejestrach. Różnica między typami wbudowanymi a typami referencyjnymi polega na tym, że nie musisz alokować typów wbudowanych na stercie. Jest to przydatne w przypadku obiektów, które żyją tylko w zakresie tego wywołania metody. Ponieważ odpowiednie części stosu prawdopodobnie znajdują się w pamięci podręcznej, dostęp do obiektów na stosie będzie raczej przyjazny dla pamięci podręcznej. Jako bonus, obiekty, które nie są przydzielone na stercie Java, nie muszą być zbierane. Te przyjazne dla pamięci podręcznej zachowania są już obecne w Javie podczas używania tak zwanych "typów pierwotnych", takich jak ints i chars. Typy pierwotne są typami wbudowanymi i mają wszystkie swoje zalety. Więc nawet jeśli typy wbudowane mogą wydawać się obce na początku, pracowałeś z nimi już wcześniej; po prostu mogłeś nie myśleć o nich jako o przedmiotach. Tak więc, gdy "zajęcia inline" wydają się mylące, możesz spróbować pomyśleć: "Co by zrobił int?"

Współpraca z Kotlin

W ostatnich latach Kotlin był gorącym tematem w społeczności JVM; użycie języka stale rośnie, od projektów mobilnych po backendowe. Jedną z zalet Kotlina jest jego duża interoperacyjność z Javą od samego początku. Wywołanie dowolnego kodu Java z Kotlina po prostu działa. Kotlin doskonale rozumie Javę, ale jest jedna niewielka irytacja, która może się pojawić, jeśli nie przestrzegasz najlepszych praktyk Javy co do joty: brak typów niepodlegających wartości null w Javie. Jeśli nie zastosujesz adnotacji dotyczących wartości null w Javie, Kotlin zakłada, że wszystkie te typy mają nieznaną wartość nullability - są to tak zwane typy platform. Jeśli masz pewność, że nigdy nie będą null, możesz je zmusić do typu innego niż null za pomocą !! operatora lub rzutując je na typ inny niż null. W obu przypadkach nastąpi awaria, jeśli wartość w czasie wykonywania jest równa null. Najlepszym sposobem obsługi tego scenariusza jest dodanie adnotacji dotyczących wartości null, takich jak @Nullable i @NotNull, do interfejsów API języka Java. Istnieje wiele obsługiwanych adnotacji: JetBrains, Android, JSR-305, FindBugs i inne. W ten sposób Kotlin będzie znał dopuszczalność typu null, a podczas kodowania w Javie otrzymasz dodatkowe wglądy IDE i ostrzeżenia o potencjalnych wartościach null. Wygrana-wygrana! Podczas wywoływania kodu Kotlin z Javy powinieneś zauważyć, że chociaż większość kodu będzie działać dobrze, możesz zobaczyć dziwactwa z niektórymi zaawansowanymi funkcjami języka Kotlin, które nie mają bezpośredniego odpowiednika w Javie. Kompilator Kotlin musi przyjąć kilka kreatywnych rozwiązań, aby zaimplementować je w kodzie bajtowym. Są one ukryte w Kotlinie, ale Java nie jest świadoma tych mechanizmów i odsłania je, co skutkuje użytecznym, ale nieoptymalnym interfejsem API. Przykładem są deklaracje najwyższego poziomu. Ponieważ kod bajtowy JVM nie obsługuje metod i pól poza klasami, kompilator Kotlin umieszcza je w klasie syntetycznej o tej samej nazwie, co plik, w którym się znajdują. Na przykład wszystkie symbole najwyższego poziomu w pliku FluxCapacitor.kt pojawią się jako statyczne elementy klasy FluxCapacitorKt z Javy. Możesz zmienić syntetyczną nazwę klasy na coś ładniejszego, dodając do pliku Kotlin adnotację @file:JvmName("FluxCapacitorFuncs"). Możesz oczekiwać, że elementy członkowskie zdefiniowane w obiekcie (towarzyszącym) będą statyczne w kodzie bajtowym, ale tak nie jest. Kotlin pod maską przenosi je w pole o nazwie INSTANCE, czyli syntetyczna klasa wewnętrzna Companion. Jeśli potrzebujesz dostępu do nich jako członków statycznych, po prostu dodaj do nich adnotacje @JvmStatic. Możesz również sprawić, by właściwości obiektów (towarzyszących) były wyświetlane jako pola w Javie, dodając do nich adnotacje jako @JvmField. Wreszcie Kotlin oferuje parametry opcjonalne z wartościami domyślnymi. To bardzo wygodna funkcja, ale niestety Java jej nie obsługuje. W Javie musisz podać wartości dla wszystkich parametrów, łącznie z tymi, które mają być opcjonalne. Aby tego uniknąć, możesz użyć adnotacji @JvmOverloads, która nakazuje kompilatorowi generowanie przeciążeń teleskopowych dla wszystkich parametrów opcjonalnych. Kolejność parametrów jest ważna, ponieważ nie otrzymujesz wszystkich możliwych permutacji w przeciążeniach, ale raczej jedno dodatkowe przeciążenie dla każdego opcjonalnego parametru, w kolejności, w jakiej pojawiają się w Kotlinie. Podsumowując, Kotlin i Java są prawie całkowicie interoperacyjne po wyjęciu z pudełka: to jedna z przewag Kotlina nad innymi językami JVM. Jednak w niektórych scenariuszach minuta pracy nad Twoimi interfejsami API sprawi, że korzystanie z niego będzie znacznie przyjemniejsze z innego języka. Naprawdę nie ma powodu, aby nie iść o krok dalej, biorąc pod uwagę, jak duży wpływ możesz wywrzeć przy tak niewielkim wysiłku!

Gotowe, ale …

Ile razy byłeś na stand-upie, codziennym spotkaniu Scrum lub statusie i słyszałeś zdanie "Zrobione, ale…"? Kiedy to słyszę, moja pierwsza myśl brzmi: "Więc to jeszcze nie koniec". Istnieją trzy problemy z używaniem słowa gotowe, gdy nie jest zrobione.

1. Komunikacja i przejrzystość: Idealnie, aby Twój zespół miał definicję ukończenia. Ale nawet jeśli nie, prawdopodobnie istnieje pewne oczekiwanie, co oznacza. A jeszcze lepiej, osoba zgłaszająca status wie o tym. W przeciwnym razie nie mielibyśmy zastrzeżenia dotyczącego wykonania zadania. Typowe rzeczy, których się nie robi, to pisanie testów, dokumentacja i przypadki brzegowe. Poświęć chwilę i zobacz, czy możesz pomyśleć o czymś więcej. Podobnie nie podoba mi się termin "skończone". Domyślnie błogosławi ideę, że zrobione, w rzeczywistości nie oznacza zrobione. Bądź jasnym komunikatorem. Jeśli coś nie zostało zrobione, nie mów, że zostało to zrobione. To okazja do przekazania większej ilości informacji. Na przykład "Zakodowałem szczęśliwą ścieżkę, a następnie dodam walidację" lub "Skończyłem cały kod - pozostało mi tylko zaktualizować instrukcję obsługi" lub nawet "Myślałem, że skończyłem, a potem odkryłem widżet nie działa we wtorki." Wszystko to dostarcza informacji Twojemu zespołowi.

2. Percepcja: Menedżerowie lubią słuchać słowa "skończone". Oznacza to, że możesz podjąć więcej pracy. Lub pomóż koledze z drużyny. Lub prawie wszystko, co nie obejmuje spędzania więcej czasu na zadaniu. Gdy tylko usłyszą, że zrobiono, staje się to percepcją. Ale albo zostaje zapomniane, albo staje się drobnostką. Teraz przechodzisz do następnej rzeczy, kiedy nie ukończyłeś pierwszej. Stąd bierze się dług techniczny! Czasami dług techniczny jest wyborem. Jednak dokonanie tego wyboru poprzez omówienie go jest o wiele lepsze niż dokonanie tego za ciebie, ponieważ twierdziłeś, że zostało to zrobione. OK. Skończyłem z tym artykułem, ale muszę jeszcze napisać ostatnią część. Zobacz jak to się udało? Właściwie to wcale nie skończyłem.

3. Nie ma częściowego kredytu za ukończenie: Gotowe jest stanem binarnym. Albo jest zrobione, albo nie. Nie ma czegoś takiego jak zrobienie połowy. Załóżmy, że budujesz parę szczudeł i mówisz, że skończyłeś w 50%. Zastanów się, co to znaczy. Może to oznaczać, że masz jedno szczudło. Niezbyt przydatne. Bardziej prawdopodobne jest to, że myślisz, że masz jedno szczudło, ale nadal musisz zbudować drugie, a następnie przetestować. Testy prawdopodobnie ujawnią, że musisz wrócić i coś zmienić. Ta przeróbka oznacza, że nie zrobiłeś nawet 50%. Byłeś optymistą.

Pamiętaj: nie mów, że skończyłeś, dopóki nie skończysz!

Certyfikaty Java: kamień probierczy w technologii

Wyobraź sobie, że musisz przejść operację z użyciem robota. Chirurg jest doświadczony i wykwalifikowany, ale nie ma uprawnień do korzystania ze sprzętu zrobotyzowanego do operacji. Czy nadal posuwałbyś się naprzód z chirurgią zrobotyzowanym z tym chirurgiem? Dopóki nie byłabym przekonana o umiejętnościach chirurga na sprzęcie zrobotyzowanym, nie zrobiłabym tego. Kontynuując analogię, w jaki sposób sprawdziłbyś umiejętności kandydata przed dodaniem ich do swoich krytycznych projektów? Wyższy stopień naukowy w dziedzinie informatyki to za mało. Luka w umiejętnościach zdobytych na uczelni w programie nauczania i wymagania pracy są szerokie. Niezależne organizacje szkolące umiejętności wkraczają, aby wypełnić tę lukę. Ale to nie wystarczy. Kto i jak mierzy jakość swoich treści? W tym miejscu wkracza branża. Trafną metaforą byłby kamień probierczy - cudowny kamień używany w starożytności do pomiaru czystości złota i innych metali szlachetnych, które były używane jako waluta. Metalową monetę pocierano o ciemny kamień krzemionkowy podobny do jaspisu, a kolorowa pozostałość wskazywałaby na czystość metalu. Organizacje takie jak Oracle określiły te wzorce w formie profesjonalnych certyfikatów, aby odgrywać rolę punktów kontrolnych, mierzących umiejętności IT w ustandaryzowany sposób. Ludzie często pytają, czy te certyfikaty zawodowe są niezbędne absolwentom lub doktorantom informatyki. Czy program uniwersytecki obejmował już treści? W tym miejscu należy spojrzeć z perspektywy na cele krótko- i długoterminowe. Ukończenie lub ukończenie studiów podyplomowych w dziedzinie informatyki na uniwersytecie może być strategicznym wyborem, aby wytyczyć długoterminową ścieżkę kariery, podczas gdy zdobywanie certyfikatów zawodowych jest taktycznym wyborem, aby zdobyć sprawdzone umiejętności w zakresie technologii, które należy zastosować natychmiast w projekcie i osiąganiu celów krótkoterminowych. Profesjonalne certyfikaty w języku Java wydane przez Oracle Corporation cieszą się dużym zainteresowaniem. Są przyznawane, gdy kandydat spełnia określone wymagania. W zależności od certyfikatu od kandydata może być wymagane ukończenie kursu lub projektu lub zdanie egzaminu. Celem jest ustalenie, czy dana osoba jest wykwalifikowana do zajmowania określonych rodzajów stanowisk lub pracy przy określonych projektach. Certyfikowane umiejętności wypełniają lukę między ich istniejącymi umiejętnościami a umiejętnościami wymaganymi przez branżę, co skutkuje wyższym wskaźnikiem powodzenia projektów. Certyfikaty te są regularnie aktualizowane. Oracle oferuje wiele opcji certyfikatów Java, które definiują tematy i ścieżkę, którą powinni podążać programiści. Deweloperzy mogą wybrać odpowiednią certyfikację zgodnie z ich zainteresowaniami. Potwierdzone umiejętności określają wiarygodność umiejętności danej osoby w zakresie programowania w określonym języku lub zrozumienia platformy, metodologii lub praktyki dla potencjalnych pracodawców. Pomagają profesjonalistom pokonać początkową przeszkodę, jaką jest przegląd życiorysów i wybór na rozmowy kwalifikacyjne. Certyfikaty Java pomagają poszczególnym osobom awansować w karierze. Gdy ludzie szukają pracy, organizacji i zespołów próbują znaleźć talenty o zweryfikowanych umiejętnościach, te certyfikaty mogą być pierwszym krokiem.

Java to dziecko lat 90

Są tylko dwa rodzaje języków: te, na które ludzie narzekają i te, których nikt nie używa. -Bjarne Stroustrup

Nie jestem pewien, czy spostrzeżenia Stroustrupa mówią więcej o językach programowania, czy o ludzkiej naturze. Zwraca jednak uwagę na często zapominany truizm, że projektowanie języków programowania jest ludzkim przedsięwzięciem. Jako takie, języki zawsze noszą ślady środowiska i kontekstu, w którym powstały. Nie powinno więc dziwić, że ślady późnych lat 90. można zobaczyć wszędzie w projektowaniu Javy, jeśli wiesz, gdzie szukać. Na przykład sekwencja bajtów do załadowania odwołania do obiektu ze zmiennej lokalnej 0 do tymczasowego stosu oceny jest następującą sekwencją dwóch bajtów:

19 00 // wsad 00

Jednak zestaw instrukcji kodu bajtowego maszyny JVM zapewnia postać wariantu, która jest o jeden bajt krótsza:

2A // aload_0

Zapisany jeden bajt może nie brzmieć zbyt dużo, ale może zacząć się sumować w całym pliku klasy. Pamiętajcie, że pod koniec lat 90. klasy Java (często aplety) były pobierane przez modemy telefoniczne, niesamowite urządzenia, które były w stanie osiągnąć zawrotną prędkość 14,4 kilobitów na sekundę. Przy takiej przepustowości oszczędzanie bajtów tam, gdzie to możliwe, było ogromną motywacją dla Javy. Można nawet argumentować, że cała koncepcja typów prymitywnych jest kombinacją chwytu wydajnościowego i podróbki dla programistów C++, którzy niedawno przybyli do świata Javy - produktów z lat 90., kiedy powstała Jawa. Nawet "magiczna liczba" (pierwsze kilka bajtów pliku, które pozwalają systemowi operacyjnemu zidentyfikować typ pliku) dla wszystkich plików klas Java wydaje się przestarzała:

CA FE BA BE

"Cafe babe" może nie jest dziś świetnym wyglądem dla Javy. Niestety nie jest to coś, co realnie można teraz zmienić. To nie tylko kod bajtowy: w standardowej bibliotece Java (zwłaszcza w jej starszych częściach) API, które replikują równoważne API C, są wszędzie. Każdy programista, który został zmuszony do ręcznego odczytania zawartości pliku, wie o tym aż za dobrze. Co gorsza, sama wzmianka o java.util.Date wystarczy, aby wielu programistów Javy wpadło w pośpiech. Przez pryzmat roku 2020 i później Java jest czasami postrzegana jako język głównego nurtu, środkowy. To, czego brakuje tej narracji, to fakt, że świat oprogramowania radykalnie się zmienił od czasu debiutu Javy. Wielkie pomysły, takie jak maszyny wirtualne, dynamiczne samozarządzanie, kompilacja JIT i zbieranie śmieci, są teraz częścią ogólnego krajobrazu języków programowania. Chociaż niektórzy mogą postrzegać Javę jako The Establishment, tak naprawdę jest to główny nurt, który przeniósł się do przestrzeni, w której Java zawsze była. Pod przykrywką korporacyjnego szacunku Java wciąż jest dzieckiem lat 90.

Programowanie Java z perspektywy wydajności JVM

Wskazówka 1: Nie obsesyjnie śmieciami : Zauważyłem, że czasami programiści Java mają obsesję na punkcie ilości śmieci, które produkują ich aplikacje. Bardzo niewiele przypadków uzasadnia taką obsesję. Moduł odśmiecania pamięci (GC) pomaga wirtualnej maszynie Java (JVM) w zarządzaniu pamięcią. W przypadku maszyny wirtualnej OpenJDK HotSpot GC wraz z dynamicznym kompilatorem warstwowym just-in-time (JIT) (klient (C1) + klasa serwera (C2)) oraz interpreter tworzą jego silnik wykonawczy. Istnieje wiele optymalizacji, które dynamiczny kompilator może wykonać w Twoim imieniu. Na przykład C2 może wykorzystywać dynamiczne przewidywanie rozgałęzień i mieć prawdopodobieństwo ("zawsze" lub "nigdy") dla pobranych (lub nie) rozgałęzień kodu. Podobnie C2 przoduje w optymalizacjach związanych ze stałymi, pętlami, kopiami, deoptymalizacją i tak dalej. Zaufaj kompilatorowi adaptacyjnemu, ale w razie wątpliwości zweryfikuj za pomocą "serwisowalności", "obserwowalności", rejestrowania i wszystkich innych tego typu narzędzi, które mamy dzięki naszemu bogatemu ekosystemowi. To, co ma znaczenie dla GC, to żywotność/wiek obiektu, jego "popularność", "żywy rozmiar zestawu" dla twojej aplikacji, długowieczne stany nieustalone, szybkość alokacji, narzut znakowania, twój wskaźnik promocji (dla kolekcjonera pokoleniowego) i itd.

Wskazówka nr 2: Scharakteryzuj i zweryfikuj swoje benchmarki: Pewien mój współpracownik przyniósł kiedyś pewne obserwacje zestawu benchmarkingowego z różnymi podbenchmarkami. Jeden z nich został scharakteryzowany jako benchmark "start-up i pokrewny". Po przyjrzeniu się liczbom wydajności i założeniu, jakim było porównanie między wydaniami OpenJDK 8u i OpenJDK 11u LTS, zdałem sobie sprawę, że różnica w liczbach mogła być spowodowana zmianą domyślnego GC z Parallel GC na G1 GC. Wydaje się więc, że (pod)benchmark nie został odpowiednio scharakteryzowany lub nie został zweryfikowany. Oba są ważnymi ćwiczeniami porównawczymi i pomagają zidentyfikować i odizolować "jednostkę testu" (UoT) od innych elementów systemu testowego, które mogą działać jako krytycy.

Wskazówka 3: Rozmiar alokacji i stawki nadal mają znaczenie : Aby móc dotrzeć do sedna problemu omówionego powyżej, poprosiłem o przejrzenie dzienników GC. W ciągu kilku minut stało się jasne, że (stały) rozmiar regionu, który jest oparty na rozmiarze sterty aplikacji, klasyfikuje "zwykłe" obiekty jako "ogromne". W przypadku G1 GC, ogromne obiekty to obiekty, które obejmują 50% lub więcej regionu G1. Takie obiekty nie podążają szybką ścieżką alokacji i są przydzielane ze starej generacji. W związku z tym wielkość alokacji ma znaczenie dla zregionalizowanych GC. GC nadąża za mutacją wykresu żywych obiektów i przenosi obiekty z przestrzeni "Od" do przestrzeni "Do". Jeśli Twoja aplikacja dokonuje alokacji z szybkością wyższą niż może nadążyć algorytm znakowania (współbieżnego) GC, może to stać się problemem. Ponadto pokoleniowy GC może przedwcześnie promować obiekty o krótkim czasie życia lub nie starzeć się prawidłowo ze względu na napływ alokacji. G1 GC OpenJDK wciąż pracuje nad tym, aby nie być zależnym od swojego awaryjnego, bezpiecznego, nieprzyrostowego, pełnego przechodzenia przez stertę, (równoległego) kolektora zatrzymującego świat.

Wskazówka 4: Adaptacyjna JVM to Twoje prawo i powinieneś tego wymagać : Wspaniale jest widzieć adaptacyjny JIT i wszystkie ulepszenia ukierunkowane na uruchamianie, przyspieszanie, dostępność JIT i optymalizację zasięgu. Podobnie dostępne są różne algorytmy inteligencji na poziomie GC. Te GC, których jeszcze nie ma, powinny się tam wkrótce dostać, ale to się nie stanie bez naszej pomocy. Jako programiści Java prosimy o przekazywanie społeczności opinii na temat swojego przypadku użycia i pomoc w tworzeniu innowacji w tym obszarze. Przetestuj również funkcje, które są stale dodawane do JIT.

Java powinna być zabawna

Swoją karierę w Javie zacząłem od J2EE 1.2. Miałem pytania. Dlaczego dla każdego ziarna były cztery klasy i setki wierszy wygenerowanego kodu? Dlaczego kompilacja maleńkich projektów zajęła pół godziny? To nie było produktywne i nie było zabawne. Te dwie rzeczy często idą w parze: rzeczy są nieprzyjemne, ponieważ wiemy, że są marnotrawstwem. Pomyśl o spotkaniach, na których nic nie jest postanowione, raporty o stanie, których nikt nie czyta…

Jeśli nie-zabawa jest zła, co jest zabawą? Czy to jest dobre? A jak to otrzymujemy? Zabawa może mieć różne oblicza:

•  Eksploracja (dochodzenie skoncentrowane)
•  Graj (dla samego siebie, bez celu)
•  Puzzle (zasady i cel)
•  Gry (zasady i zwycięzca)
•  Praca (satysfakcjonujący cel)

Java pozwala na to wszystko - część dotycząca pracy jest oczywista i każdy, kto debugował program Java wie o części układanki. (Debugowanie niekoniecznie jest zabawne, ale znalezienie rozwiązania jest świetne.) Uczymy się poprzez eksplorację (kiedy jesteśmy w czymś nowi) i bawimy się (kiedy wiemy wystarczająco dużo, by coś zrobić). Pomijając zabawę, jaką możemy z nią czerpać, czy Java jest z natury zabawna? Java jest gadatliwa w porównaniu z młodszymi językami. Boilerplate nie jest zabawny, ale niektóre z nich można naprawić. Na przykład Lombok zgrabnie generuje metody pobierające i ustawiające, a także metody hashCode i equals (w przeciwnym razie żmudne i podatne na błędy). Ręczne pisanie śladu wejścia i wyjścia nie jest zabawne, ale aspekty lub biblioteki śledzenia mogą być instrumentowane dynamicznie (i znacznie poprawić czytelność kodu). Co sprawia, że używanie czegoś jest zabawne? Po części chodzi o bycie ekspresyjnym i zrozumiałym, ale jest w tym coś więcej. Nie jestem przekonany, że lambdy są ogólnie krótsze lub jaśniejsze niż alternatywy oparte na klasach. Ale są zabawne! Kiedy pojawiła się Java 8, programiści zagłębili się w lambdy jak dzieci w dole z piłeczkami. Chcieliśmy dowiedzieć się, jak to działa (eksploracja) i wyzwaniem wyrażania algorytmów w stylu funkcjonalnym (łamigłówki). W przypadku Javy często najlepszą rzeczą do zrobienia jest zabawa (wygrana). Śledzenie autoinstrumentacji omija nieprzyjemne rzeczy, eliminując błędy kopiowania i wklejania nazw metod oraz poprawiając przejrzystość. Albo rozważ wydajność. W przypadku scenariuszy niszowych potrzebny jest dziwny, skomplikowany kod, aby wydrapać każdy centymetr prędkości. W większości przypadków jednak najprostszy kod jest również najszybszy. (Co niekoniecznie jest prawdą w przypadku języków takich jak C.) Java JIT optymalizuje kod podczas jego działania; jest najmądrzejszy do czystego, idiomatycznego kodu. Prosty kod jest czytelny, więc błędy będą bardziej oczywiste. Kod powodujący nieszczęście ma efekt domina. Badania psychologiczne pokazują, że szczęście i sukces w miejscu pracy idą w parze. Jedno z badań wykazało, że ludzie o pozytywnym nastawieniu byli o 31% bardziej produktywni niż osoby o neutralnym lub negatywnym nastawieniu. Osiągniesz mniej, używając źle zaprojektowanych bibliotek, a potem będziesz nadal osiągać mniej, ponieważ zły kod sprawił, że będziesz nieszczęśliwy. Czy "zabawa jest dobra" to wymówka, by być nieodpowiedzialnym? Zupełnie nie! Zastanów się, czy wszyscy dobrze się bawią: wszyscy to klienci, współpracownicy i przyszli opiekunowie Twojego kodu. W porównaniu z dynamicznie pisanymi językami skryptowymi, które mogą być szybkie i luźne, Java już zaznacza bezpieczne i odpowiedzialne pole. Ale programy, które piszemy, również muszą być odpowiedzialnie zakodowane. Dobrą wiadomością jest to, że w przypadku prawie wszystkich nudnych zadań komputery mogą wykonywać pracę szybciej i dokładniej niż ludzie. Komputery nie oczekują (jeszcze) dobrej zabawy, więc skorzystaj z nich! Nie akceptuj nudy. Jeśli coś wydaje się nieprzyjemne, poszukaj lepszego sposobu. Jeśli go nie ma, wymyśl jeden. Jesteśmy programistami: potrafimy naprawić nudę.

Niewypowiedziane typy Javy

Nowi programiści Java często borykają się z tym pomysłem. Prosty przykład ujawnia prawdę:

String s = null;
Integer i = null;
Object o = null;

Symbol null musi zatem być wartością. Ponieważ każda wartość w Javie ma typ, null musi mieć typ. Co to jest? Oczywiście nie może to być żaden typ, z którym zwykle się spotykamy. Zmienna typu String nie może przechowywać wartości typu Object - właściwości podstawienia Liskov po prostu nie działają w ten sposób. Również wnioskowanie o typie zmiennej lokalnej w języku Java 11 nie pomaga:

jshell> var v = null;
| Error:
| cannot infer type for local variable v
| (variable initializer is 'null')
| var v = null;
| ^ - - - - - -^

Pragmatyczny programista Javy może po prostu podrapać się po głowie i zdecydować, jak wielu to zrobiło, że tak naprawdę nie ma to aż takiego znaczenia. Zamiast tego mogą udawać, że "null to tylko specjalny literał, który może być dowolnego typu referencyjnego". Jednak dla tych z nas, którzy uważają to podejście za niezadowalające, prawdziwą odpowiedź można znaleźć w specyfikacji języka Java (JLS), w sekcji 4.1:

Istnieje również specjalny typ null, typ wyrażenia null (§3.10.7, §15.8.1), który nie ma nazwy.
Ponieważ typ null nie ma nazwy, nie można zadeklarować zmienną typu null lub do rzutowania na typ null.
To jest . Java pozwala nam zapisywać wartości, których typów nie możemy zadeklarować jako typy zmiennych. Możemy nazwać te "typy niewypowiedziane" lub, formalnie, typy nieoznaczalne.
Jak pokazuje zero, właściwie używamy ich przez cały czas. Są jeszcze dwa oczywiste miejsca, w których pojawiają się tego rodzaju typy. Pierwszy pojawił się w Javie 7, a JLS ma o nich do powiedzenia:
Parametr wyjątku może oznaczać jego typ jako pojedynczą klasę type lub połączenie dwóch lub więcej typów klas (zwanych alternatywami).
Prawdziwym typem parametru multicatch jest połączenie różnych możliwych do przechwycenia typów. W praktyce skompilowany zostanie tylko kod zgodny z kontraktem API najbliższego wspólnego nadtypu alternatyw. Rzeczywisty typ parametru nie jest czymś, co możemy wykorzystać jako typ zmiennej. W dalszej części, jaki jest typ o?

jshell> var o = new Object() {
…> public void bar() { System.out.println("bar!"); }
…> }
o ==> $0@3bfdc050jshell> o.bar();
bar!

Nie może to być Object, ponieważ możemy na nim wywołać bar(), a typ Object nie ma takiej metody. Zamiast tego, prawdziwy typ jest nieoznaczalny - nie ma nazwy, której moglibyśmy użyć jako typu zmiennej w kodzie Javy. W czasie wykonywania typ jest po prostu symbolem zastępczym przypisanym do kompilatora (w naszym przykładzie $0). Używając var jako "magicznego typu", programista może zachować informacje o typie dla każdego odrębnego użycia var, aż do końca metody. Nie możemy przenosić typów od metody do metody. Aby to zrobić, musielibyśmy zadeklarować typ zwracany - i właśnie tego nie możemy zrobić! Możliwość zastosowania tych typów jest zatem ograniczona - system typów Javy pozostaje w dużej mierze systemem nominalnym i wydaje się mało prawdopodobne, aby prawdziwe typy strukturalne kiedykolwiek pojawiły się w języku. Na koniec powinniśmy zwrócić uwagę, że wiele bardziej zaawansowanych zastosowań rodzajów generycznych (w tym tajemnicze "wychwytywanie?" błędów) jest naprawdę najlepiej rozumiane w kategoriach typów nieoznaczalnych - ale to już inna historia.

JVM to platforma wieloparadygmatyczna: użyj jej, aby ulepszyć swoje programowanie

Java jest językiem imperatywnym: programy Java informują maszynę JVM, co ma robić i kiedy. Ale komputeryzacja polega na budowaniu abstrakcji. Java jest reklamowana jako język zorientowany obiektowo: abstrakcjami Javy są obiekty, metody i przekazywanie komunikatów poprzez wywołanie metody. Przez lata ludzie budowali coraz większe systemy przy użyciu obiektów, metod, stanu aktualizowalnego i jawnej iteracji, i pojawiły się pęknięcia. Wiele z nich jest "zatapianych" przy użyciu wysokiej jakości testów, ale programiści wciąż kończą "hakowaniem", aby obejść różne problemy. Wraz z pojawieniem się Javy 8, Java przeszła niezwykle rewolucyjną zmianę: wprowadziła odwołania do metod, wyrażenia lambda, domyślne metody interfejsów, funkcje wyższego rzędu, niejawną iterację i wiele innych rzeczy. Java 8 wprowadziła zupełnie inny sposób myślenia o implementacji algorytmów. Myślenie imperatywne i deklaratywne to bardzo różne sposoby wyrażania algorytmów. W latach 80. i 90. ten sposób myślenia był postrzegany jako odrębny i nie do pogodzenia: mieliśmy do czynienia z wojną zorientowaną obiektowo na programowanie funkcjonalne. Smalltalk i C++ byli mistrzami obiektowej orientacji, a Haskell mistrzem funkcjonalności. Później C++ przestał być językiem obiektowym i reklamował się jako język wieloparadygmatyczny; Java przejęła rolę mistrza zorientowania obiektowego. Jednak wraz z Javą 8 Java stała się wieloparadygmatyczna. Na początku lat 90. JVM została skonstruowana jako sposób na uczynienie Javy przenośnym - możemy pominąć historię projektu Green i języka programowania Oak. Początkowo służyło to tworzeniu wtyczek do przeglądarek internetowych, ale szybko przeszło do tworzenia systemów po stronie serwera. Java kompiluje się do kodu bajtowego maszyny JVM niezależnego od sprzętu, a interpreter wykonuje kod bajtowy. Kompilatory just-in-time (JIT) umożliwiają znacznie szybsze wykonanie całego modelu interpretacji bez zmiany modelu obliczeniowego maszyny JVM. Ponieważ JVM stała się niezwykle popularną platformą, stworzono inne języki, które wykorzystywały kod bajtowy jako platformę docelową: Groovy, JRuby i Clojure to języki dynamiczne wykorzystujące JVM do wykonywania; Scala, Cejlon i Kotlin to języki statyczne. Scala w szczególności wykazał pod koniec XXI wieku, że orientację obiektową i programowanie funkcjonalne można zintegrować w jeden, wieloparadygmatyczny język. Podczas gdy Clojure jest językiem funkcjonalnym, Groovy i JRuby od samego początku były wieloparadygmatyczne. Kotlin bierze lekcje Javy, Scali, Groovy itp., aby tworzyć języki na lata 2010 i 2020 na JVM. Aby jak najlepiej wykorzystać JVM, powinniśmy dobrać odpowiedni język programowania do problemu. Nie musi to oznaczać jednego języka dla całego problemu: możemy używać różnych języków dla różnych bitów - wszystko dzięki JVM. Tak więc możemy użyć Javy lub Kotlina do bitów, które najlepiej wyrazić jako kod statyczny, a Clojure lub Groovy do bitów, które najlepiej obsługuje kod dynamiczny. Próba pisania dynamicznego kodu w Javie jest uciążliwa, więc użyj odpowiedniego narzędzia do tego zadania, biorąc pod uwagę, że wszystkie języki programowania mogą współpracować z JVM.

Trzymaj palec na pulsie

Nauczyłem się Javy w wersji 1.1 na uniwersytecie (chciałbym, żeby to było dlatego, że moja uczelnia używa starej technologii, a nie dlatego, że jestem stary). W tamtym czasie Java była wystarczająco mała, a ja byłem na tyle naiwny, że można było uwierzyć, że nauczyłem się wszystkiego, czego potrzebowałem, i że zostałem zaprogramowany na całe życie jako programista Java. Podczas mojej pierwszej pracy, gdy byłem jeszcze na uniwersytecie i korzystałem z Javy od niecałego roku, wydano Javę 1.2. Miał zupełnie inną bibliotekę interfejsu użytkownika (UI), o nazwie Swing, więc spędziłem lato na nauce Swinga, aby używać go do zapewniania naszym użytkownikom lepszych wrażeń. Kilka lat później, w mojej pierwszej pracy jako absolwent, odkryłem, że aplety są niedostępne, a serwlety są włączone. Następne sześć miesięcy spędziłem na nauce o serwletach i stronach JSP, abyśmy mogli dać naszym użytkownikom formularz rejestracyjny online. W kolejnej pracy dowiedziałem się, że najwyraźniej nie używaliśmy już Vectora - użyliśmy ArrayList. To wstrząsnęło mną do głębi. Jak same podstawy języka, same struktury danych, mogą się zmieniać pode mną? Moje pierwsze dwa odkrycia dotyczyły uczenia się dodatków do języka. Ten trzeci dotyczył zmian w rzeczach, o których myślałem, że już wiem. Skoro nie byłam już na uniwersytecie i nie byłam już uczona, jak miałabym po prostu wiedzieć o tych rzeczach? Miałem szczęście w tych wczesnych pracach, że miałem wokół siebie ludzi, którzy byli świadomi zmian technologicznych, które wpłynęły na projekty Java, nad którymi pracowałem. Taka powinna być rola starszych członków zespołu - nie tylko robienie tego, co im każą, ale także przedstawianie sugestii, jak to zrobić i pomaganie także reszcie zespołu w poprawie. Aby przetrwać jako programista Java, musisz zaakceptować fakt, że Java nie jest językiem stacjonarnym. Ewoluuje nie tylko w nowe wersje, ale także jako biblioteki, frameworki, a nawet nowe języki JVM. Na początku może to być onieśmielające i przytłaczające. Jednak bycie na bieżąco nie oznacza, że musisz uczyć się wszystkiego, co jest dostępne - oznacza to po prostu trzymanie ręki na pulsie, słuchanie popularnych słów kluczowych i zrozumienie trendów technologicznych. Musisz zagłębić się głębiej tylko wtedy, gdy jest to istotne dla twojej pracy lub gdy jest to coś, co jest dla ciebie osobiście interesujące (lub najlepiej jedno i drugie). Wiedza o tym, co jest dostępne w aktualnej wersji Javy i co jest planowane w przyszłych, może pomóc w implementacji funkcji lub funkcjonalności, które pomogą Twoim użytkownikom robić to, co muszą. Co oznacza, że pomaga to programiście być bardziej produktywnym. Java wydaje teraz nową wersję co sześć miesięcy. Trzymanie palca na tym pulsie może rzeczywiście ułatwić Ci życie.

Rodzaje komentarzy

Załóżmy, że chcesz umieścić kilka komentarzy w swoim kodzie Java. Czy używasz /**, /* czy //? A gdzie dokładnie je umieszczasz? Poza składnią istnieją ustalone praktyki, które wiążą semantykę, do której jest używana gdzie.

Komentarze Javadoc dla kontraktów

Komentarze Javadoc (zawarte w /** ... */) są używane wyłącznie na klasach, interfejsach, polach i metodach i są umieszczane bezpośrednio nad nimi. Oto przykład z Map::size:

/**
* Zwraca liczbę mapowań klucz-wartość na tej mapie. Jeśli
* mapa zawiera więcej niż Integer.MAX_VALUE elementów, zwraca
* Liczba całkowita.MAX_VALUE.
*
* @zwróć liczbę mapowań klucz-wartość na tej mapie
*/
int rozmiar();

Przykład demonstruje składnię i semantykę: komentarz Javadoc jest kontraktem. Obiecuje użytkownikom API to, czego mogą się spodziewać, zachowując jednocześnie centralną abstrakcję typu, nie mówiąc o szczegółach implementacji. Jednocześnie wiąże realizatorów, aby zapewnić określone zachowanie. Java 8 nieco złagodziła tę surowość, jednocześnie formalizując różne interpretacje, wprowadzając (niestandardowe) tagi @apiNote, @implSpec i @implNote. Prefiksy, api lub impl, określają, czy komentarz jest adresowany do użytkowników, czy implementatorów. Przyrostki, Spec lub Note, wyjaśniają, czy jest to faktycznie specyfikacja, czy tylko dla ilustracji. Zauważ jak brakuje @apiSpec? Dzieje się tak dlatego, że nieoznakowany tekst komentarza ma pełnić tę rolę: określanie API.

Blokuj komentarze dla kontekstu

Komentarze blokowe są zawarte w /* … */. Nie ma ograniczeń co do tego, gdzie je umieścić, a narzędzia zwykle je ignorują. Powszechnym sposobem ich użycia jest na początku klasy lub nawet metoda dająca wgląd w jej implementację. Mogą to być szczegóły techniczne, ale mogą również nakreślić kontekst, w którym powstał kod (słynne dlaczego z kodu mówi ci co, komentarze mówią dlaczego) lub ścieżki, których nie obrałeś. Dobry przykład na podanie szczegółów implementacji można znaleźć w HashMap, który zaczyna się tak:

/*
* Uwagi dotyczące wdrożenia.
*
* Ta mapa zwykle działa jako binned (bucketed) hash table,
* ale gdy kosze stają się zbyt duże, zamieniają się w kosze
* TreeNodes, każdy zbudowany podobnie do tych w
* java.util.Mapa drzewa.
* […]
*/

Z reguły, gdy Twoje pierwsze rozwiązanie nie jest ostatnim, gdy dokonujesz kompromisu lub gdy dziwny wymóg lub niezręczny interfejs API zależności kształtuje Twój kod, rozważ udokumentowanie tego kontekstu. Twoi koledzy i twoje przyszłe ja podziękują ci. (Bezgłośnie.)

Komentarze linii dla dziwnych rzeczy

Komentarze linii zaczynają się od //, które muszą być powtórzone w każdej linii. Nie ma ograniczeń co do tego, gdzie ich używać, ale często umieszcza się je nad komentowaną linią lub blokiem (w przeciwieństwie do na końcu). Narzędzia je ignorują - wielu programistów też to robi. Komentarze liniowe są często używane do narracji, co robi kod, co słusznie zostało uznane za ogólnie złą praktykę. Nadal może być pomocny w określonych przypadkach, na przykład tam, gdzie kod musi używać funkcji języka tajemnego lub łatwo je złamać w subtelny sposób (współbieżność jest tego najlepszym przykładem).

Ostatnie słowa

•  Upewnij się, że wybrałeś odpowiedni rodzaj komentarza.
•  Nie przekraczaj oczekiwań.
•  Skomentuj swój kod &#!*@$!

Poznaj swoje flatMap

Tytuły stanowisk stale się zmieniają. Podobnie jak w środowisku medycznym, gdzie koncentracja może być szersza lub bardziej wyspecjalizowana, niektórzy z nas, którzy kiedyś byli tylko programistami, teraz zajmują inne stanowiska. Jedną z najnowszych specjalistycznych dyscyplin jest inżynieria danych. Inżynier danych nadzoruje dane, budując potoki, filtrując dane, przekształcając je i formując w to, czego potrzebują oni lub inni, aby podejmować decyzje biznesowe w czasie rzeczywistym dzięki przetwarzaniu strumieniowemu. Zarówno ogólny programista, jak i inżynier danych muszą opanować flatMap, jedno z najważniejszych narzędzi dla każdego funkcjonalnego, zdolnego języka, takiego jak naszauwielbiana Java, ale także dla frameworków Big Data i bibliotek strumieniowych. flatMap, podobnie jak mapowanie i filtrowanie partnerów, ma zastosowanie do wszystkiego, co jest "kontenerem czegoś" - na przykład Stream< T > i CompletableFuture< T >. Jeśli chcesz wyjść poza standardową bibliotekę, istnieje również Observable (RXJava) i Flux (Project Reactor). W Javie użyjemy Stream. Pomysł na mapę jest prosty - weź wszystkie elementy strumienia lub kolekcji i zastosuj do nich funkcję:

Stream.of(1, 2, 3, 4).map(x -> x * 2)).collect(Collectors.toList())
Daje to:

[2, 4, 6, 8]

Co się stanie, jeśli zrobimy co następuje?

Stream.of(1, 2, 3, 4)
.map(x -> Stream.of(-x, x, x + 1))
.collect(Collectors.toList())

Niestety otrzymujemy Listę potoków Stream:

[java.util.stream.ReferencePipeline$Head@3532ec19,
java.util.stream.ReferencePipeline$Head@68c4039c,
java.util.stream.ReferencePipeline$Head@ae45eb6,
java.util.stream.ReferencePipeline$Head@59f99ea]

Ale myśląc o tym, oczywiście dla każdego elementu Strumienia tworzymy kolejny Strumień. I spójrz głębiej na mapę(x -> Stream.of(...)). Dla każdego pojedynczego elementu tworzymy liczbę mnogą. Jeśli wykonasz mapę z liczbą mnogą, nadszedł czas, aby wydzielić flatMap:

Stream.of(1, 2, 3, 4)
.flatMap(x -> Stream.of(-x, x, x+1))
.collect(Collectors.toList())

Dzięki temu uzyskamy to, do czego dążyliśmy:

[-1, 1, 2, -2, 2, 3, -3, 3, 4, -4, 4, 5]

Możliwości wykorzystania flatMap są ogromne. Przejdźmy do czegoś trudniejszego, co jest odpowiednie dla każdego zadania programowania funkcjonalnego lub inżynierii danych. Rozważ następującą relację, w której gettery, settery i toString są elided :

class Employee {
private String firstName, lastName;
private Integer yearlySalary;
// getters, setters, toString
}c
lass Manager extends Employee {
private List employeeList;
// getters, setters, toString
}

Załóżmy, że mamy tylko Stream, a naszym celem jest określić wszystkie wynagrodzenia wszystkich pracowników, w tym Menedżerów i ich Pracowników. Możemy ulec pokusie, aby przejść od razu do forEach i zacząć przekopywać się przez te pensje. To niestety modelowałoby nasz kod do struktury danych i powodowałoby niepotrzebną komplikację. Lepszym rozwiązaniem byłoby pójść w odwrotną stronę i ustrukturyzować dane w stosunku do naszego kodu. Tutaj wkracza flatMap:

List.of(manager1, manager2).stream()
.flatMap(m ->
Stream.concat(m.getEmployeeList().stream(), Stream.of(m)))
.distinct()
.mapToInt(Employee::getYearlySalary)
.sum();

Ten kod bierze każdego menedżera i zwraca liczbę mnogą - menedżera i jego pracowników. Następnie wykonujemy flatMap te kolekcje, aby utworzyć jeden strumień i wykonujemy odrębne, aby odfiltrować wszystkie duplikaty. Teraz możemy traktować je wszystkie jako jedną kolekcję. Reszta jest łatwa. Najpierw wykonujemy wywołanie specyficzne dla Javy, mapToInt, które wyodrębnia ich roczne wynagrodzenie i zwraca IntStream, wyspecjalizowany typ Stream dla liczb całkowitych. Na koniec sumujemy Strumień. Zwięzły kod. Niezależnie od tego, czy używasz Stream, czy innego rodzaju C, gdzie C jest dowolnym strumieniem lub kolekcją, kontynuuj przetwarzanie danych przy użyciu map, filter, flatMap lub groupBy przed osiągnięciem forEach lub dowolnej innej operacji terminalu, takiej jak Collect. Jeśli przedwcześnie przejdziesz do operacji terminala, stracisz wszelkie lenistwo i optymalizację, które zapewniają strumienie Java, biblioteki strumieniowe lub frameworki Big Data.

Poznaj swoje kolekcje

Kolekcje są podstawą w każdym języku programowania. Stanowią jeden z podstawowych elementów budulcowych powszechnie tworzonego kodu. Język Java wprowadził framework Collections dawno temu w JDK 1.2. Wielu programistów sięga po ArrayList jako swoją de facto kolekcję do wykorzystania. Jednak kolekcje to coś więcej niż ArrayList, więc przyjrzyjmy się. Kolekcje można sklasyfikować jako uporządkowane lub nieuporządkowane. Uporządkowane kolekcje mają przewidywalną kolejność iteracji; nieuporządkowane kolekcje nie mają przewidywalnej kolejności iteracji. Innym sposobem klasyfikowania kolekcji jest sortowanie lub nieposortowanie. Elementy w posortowanej kolekcji są sekwencjonowane od początku do końca na podstawie komparatora; nieposortowane kolekcje nie mają określonej kolejności opartej na elementach. Chociaż posortowane i uporządkowane mają podobne znaczenie w języku angielskim, nie zawsze można ich używać zamiennie w przypadku kolekcji. Ważnym rozróżnieniem jest to, że uporządkowane kolekcje mają przewidywalną kolejność iteracji, ale nie mają kolejności sortowania. Posortowane kolekcje mają przewidywalny porządek sortowania, dlatego mają przewidywalną kolejność iteracji. Pamiętaj: wszystkie posortowane kolekcje to kolekcje uporządkowane, ale nie wszystkie uporządkowane kolekcje są kolekcjami posortowanymi. W JDK istnieją różne kolekcje uporządkowane, nieuporządkowane, posortowane i nieposortowane. Przyjrzyjmy się kilku z nich. List to interfejs dla uporządkowanych kolekcji ze stabilną kolejnością indeksowania. Listy umożliwiają wstawianie zduplikowanych elementów i zapewniają przewidywalną kolejność iteracji. JDK oferuje implementacje List, takie jak ArrayList i LinkedList. Aby znaleźć konkretny element, można użyć metody zawiera. Operacja zawiera przegląda listę od początku, stąd znajdowanie elementów w liście jest operacją O(n). Mapa to interfejs, który utrzymuje relacje klucz-wartość i zachowuje tylko unikatowe klucze. Jeśli do mapy zostanie dodany ten sam klucz i inna wartość, stara wartość zostanie zastąpiona nową wartością. JDK oferuje implementacje map, takie jak HashMap, LinkedHashMap i TreeMap. HashMap jest nieuporządkowany, podczas gdy LinkedHashMap jest uporządkowany; oba opierają się na hashCode i równają się określaniu unikalnych kluczy. TreeMap jest sortowane: klucze są sortowane według komparatora lub według kolejności sortowania kluczy porównywalnych. TreeMap opiera się na CompareTo w celu określenia kolejności sortowania i unikalności kluczy. Aby znaleźć określony element, Map udostępnia metody ContainsKey i ContainsValue. W przypadku HashMap ContainsKey wyszukuje klucz w wewnętrznej tabeli skrótów. Jeśli wyszukiwanie daje w wyniku obiekt inny niż null, jest sprawdzany pod kątem równości z obiektem przekazanym do ContainsKey. Operacja zawieraValue przeszukuje wszystkie wartości od początku. Stąd znajdowanie kluczy w HashMap jest operacją O(1), podczas gdy znajdowanie wartości w HashMap jest operacją O(n). Zestaw to interfejs do kolekcji unikalnych elementów. W JDK zestawy są poparte mapami, w których kluczami są elementy, a wartości są null. JDK oferuje implementacje zestawów, takie jak HashSet (wspierane przez HashMap), LinkedHashSet (wspierane przez LinkedHashMap) i TreeSet (wspierane przez TreeMap). Aby znaleźć konkretny element, można użyć metody Contains dla Set. Metoda zawiera na delegatach Set do ContainsKey mapy i dlatego jest operacją O(1). Kolekcje to ważny element układanki programowej. Aby efektywnie z nich korzystać, konieczne jest zrozumienie ich funkcjonalności, ich implementacji, a także konsekwencji użycia wzorca iteracji. Pamiętaj, aby czytać dokumentację i pisać testy, korzystając z tych wszechstronnych i podstawowych bloków konstrukcyjnych kodu.

Kotlin to coś

Java jest prawdopodobnie najbardziej dojrzałym i sprawdzonym językiem, który wciąż jest powszechnie używany i jest mało prawdopodobne, aby zmienił się radykalnie w przewidywalnej przyszłości. Aby ułatwić nowoczesne wyobrażenie o tym, co powinien robić język programowania, niektórzy sprytni ludzie postanowili napisać nowy język, który wykona wszystkie rzeczy Java, a także kilka fajnych nowych rzeczy, których nauczenie się byłoby dość bezbolesne i w dużej mierze interoperacyjne. Ktoś taki jak ja, który od lat pracuje nad tą samą ogromną aplikacją na Androida, może zdecydować się na napisanie jednej klasy w Kotlinie bez angażowania się w całkowitą migrację. Kotlin ma na celu umożliwienie pisania krótszego, czystszego i bardziej nowoczesnego kodu. Podczas gdy nowoczesne i podglądowe wersje Javy rozwiązują wiele problemów Kotlin zarządza, Kotlin może być szczególnie przydatny dla programistów Androida, którzy utknęli gdzieś między Javą 7 a Javą 8. Spójrzmy na kilka przykładów, takich jak wzorzec konstruktora właściwości Kotlina dla modeli, zaczynając od prostego przykładu tego, jak może wyglądać model Java :

public class Person {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}

Moglibyśmy stworzyć specjalny konstruktor, który przyjmie kilka początkowych wartości:

public class Person {
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}

}

Nieźle, ale prawdopodobnie możesz zobaczyć, jak kilka dodatkowych właściwości może sprawić, że definicja tej dość prostej klasy zostanie bardzo szybko rozdęta. Przyjrzyjmy się tej klasie w Kotlin:

class Person(val name:String, var age:Int)

Otóż to! Innym ciekawym przykładem jest delegacja. Delegaci Kotlin pozwalają zapewnić logikę dla dowolnej liczby operacji odczytu. Jednym z przykładów jest leniwa inicjalizacja, koncepcja z pewnością znana programistom Java. Może to wyglądać tak:

public class SomeClass {
private SomeHeavyInstance someHeavyInstance = null;
public SomeHeavyInstance getSomeHeavyInstance() {
if (someHeavyInstance == null) {
someHeavyInstance = new SomeHeavyInstance();
}
return someHeavyInstance;
}
}

Ponownie, nie jest to zbyt straszne, zrobione prosto i bez konfiguracji, ale są szanse, że powtórzysz ten sam kod kilka razy w swoim kodzie, łamiąc zasadę DRY (Don′t Repeat Yourself). Również nie jest bezpieczny wątkowo. Oto wersja Kotlina:

val someHeavyInstance by lazy {
return SomeHeavyInstance()
}

Krótka, słodka i czytelna. Cała ta płyta jest ładnie schowana pod kołdrą. Aha, i jest też bezpieczny dla wątków. zerowe bezpieczeństwo to także duża aktualizacja. Zobaczysz wiele operatorów znaków zapytania po odwołaniu null w Kotlin:

val something = someObject?.someMember?.anotherMember

Oto to samo w Javie:
Object something = null;
if (someObject != null) {
if (someObject.someMember != null) {
if (someObject.someMember.anotherMember != null) {
something = someObject.someMember.anotherMember;
}
}
}

Operator null-check (?) natychmiast przestanie oceniać i zwróci wartość null, gdy tylko którykolwiek z referentów w łańcuchu zostanie oznaczony jako null. Zakończmy kolejną zabójczą cechą: współprogramami. Krótko mówiąc, współprogram wykonuje pracę asynchronicznie do kodu wywołującego, chociaż ta praca może być przekazana pewnej liczbie wątków. Należy zauważyć, że nawet jeśli pojedynczy wątek obsługuje wiele współprogramów, Kotlin wykonuje pewną magię przełączania kontekstu, która uruchamia wiele zadań jednocześnie. Podczas gdy określone zachowanie jest konfigurowalne, współprogramy naturalnie używają dedykowanej puli wątków, ale używają przełączania kontekstu w ramach jednego wątku (tak gorąco). Ponieważ są Kotlin, mogą być również wymyślne, wyrafinowane i przeprojektowane, ale domyślnie są też bardzo proste:

launch {
println("Hi from another context")
}

Należy jednak pamiętać o różnicach między wątkami a współprogramami - na przykład wywołanie object.wait() w jednym zadaniu spowoduje wstrzymanie wszystkich innych zadań działających w wątku zawierającym. Zakręć Kotlinem i zobacz, co myślisz.

się idiomów Java i pamięci podręcznej w swoim mózgu

Jako programiści musimy często wykonywać pewne zadania. Na przykład przeglądanie danych i stosowanie warunku są powszechne. Oto dwa sposoby obliczania liczby liczb dodatnich na liście:

public int loopImplementation(int[] nums) {
int count = 0;
for (int num : nums) {
if (num > 0) {
count++;
}
}
return count;
}
public long streamImplementation(int[] nums) {
return Arrays.stream(nums)
.filter(n -> n > 0)
.count();
}

Oba osiągają to samo i oba używają wspólnych idiomów Java. Idiom to powszechny sposób wyrażania niewielkiej części funkcjonalności, co do której społeczność ma ogólną zgodę. Wiedza o tym, jak szybko je napisać, bez konieczności myślenia o nich, umożliwia znacznie szybsze pisanie kodu. Podczas pisania kodu szukaj takich wzorców. Możesz nawet ćwiczyć je, aby być szybszym i uczyć się ich na pamięć. Niektóre idiomy, takie jak pętle, warunki i strumienie, dotyczą wszystkich programistów Java. Inne są bardziej specyficzne dla typów kodu, nad którymi pracujesz. Na przykład dużo robię z wyrażeniami regularnymi i plikami I/O. Poniższy idiom jest tym, którego często używam w plikach I/O. Czyta plik, usuwa puste wiersze i zapisuje go z powrotem:

Path path = Paths.get("words.txt");
List lines = Files.readAllLines(path);
lines.removeIf(t -> t.trim().isEmpty());
Files.write(path, lines);

Gdybym był w zespole, w którym pliki nie mieściły się w pamięci, musiałbym użyć a inny idiom programowania. Jednak mam do czynienia z małymi plikami, w których nie jest to problemem, więc wygoda czterech linii do zrobienia czegoś potężnego jest tego warta. Zauważ, że dzięki tym idiomom większość kodu jest wspólna, niezależnie od zadania. Jeśli chcę uzyskać liczby ujemne lub nieparzyste, po prostu zmieniam instrukcję if lub filtr. Jeśli chcę usunąć wszystkie linie, które mają więcej niż 60 znaków, po prostu zmieniam warunek w removeIf:

lines.removeIf(t -> t.length() <= 60);
Niezależnie od tego myślę o tym, co chcę osiągnąć. Nie szukam sposobu odczytywania pliku ani liczenia wartości. To idiom, którego nauczyłem się dawno temu. Ciekawą rzeczą w idiomach jest to, że nie zawsze uczysz się ich celowo. Nigdy nie usiadłem i postanowiłem nauczyć się idiomu do czytania/zapisywania pliku. Nauczyłem się tego dużo, używając go. Wielokrotne wyszukiwanie informacji pomaga ci się ich nauczyć. A przynajmniej pomaga wiedzieć, gdzie go znaleźć. Na przykład mam problemy z zapamiętaniem flag wyrażeń regularnych. Wiem, co robią, ale mylą ?s i ?m. Szukałem go wystarczająco dużo razy, aby wiedzieć, że powinienem wygooglować "wzorzec javadoc", aby uzyskać odpowiedź. Podsumowując, niech twój mózg służy jako pamięć podręczna. Poznaj idiomy i wywołania interfejsu API wspólnych bibliotek. Wiedz, gdzie szybko sprawdzić resztę. To cię uwolni aby twój mózg pracował nad trudnymi rzeczami!

Naucz się Kata i Kata się uczyć

Każdy programista Java musi nauczyć się nowych umiejętności i zachować ostrość swoich istniejących umiejętności. Ekosystem Javy jest ogromny i wciąż ewoluuje. Mając tak wiele do nauczenia, perspektywa nadążania za nimi może wydawać się zniechęcająca. Możemy pomóc sobie nawzajem nadążyć w tej szybko zmieniającej się przestrzeni, jeśli będziemy pracować razem jako społeczność, dzieląc się wiedzą i praktyką. Przyjmowanie, tworzenie i udostępnianie kodów kata to jeden ze sposobów, w jaki możemy to zrobić. Kod kata to praktyczne ćwiczenie programowania, które pomaga doskonalić określone umiejętności poprzez praktykę. Niektóre kata kodu zapewnią ci strukturę, która pozwoli sprawdzić, czy umiejętność została nabyta poprzez zaliczenie testów jednostkowych. Kody kata to świetny sposób dla programistów na dzielenie się ćwiczeniami praktycznymi z ich przyszłych ja i innych programistów, od których można się uczyć. Oto jak stworzyć swoje pierwsze kodowe kata:

1. Wybierz temat, którego chcesz się nauczyć.
2. Napisz zaliczeniowy test jednostkowy, który demonstruje część wiedzy.
3. Refaktoryzuj kod wielokrotnie, aż będziesz zadowolony z ostatecznego rozwiązania. Upewnij się, że test przechodzi po każdej refaktoryzacji.
4. Usuń rozwiązanie w ćwiczeniu i pozostaw test niezaliczony.
5. Zatwierdź nieudany test z kodem pomocniczym i artefaktami kompilacji w systemie kontroli wersji (VCS).
6. Otwórz kod źródłowy, aby udostępnić go innym.

Teraz zademonstruję, jak stworzyć małe kata, wykonując pierwsze cztery kroki:

1. Temat: Dowiedz się, jak łączyć ciągi na liście.

2. Napisz przechodzący test JUnit, który pokazuje, jak łączyć ciągi na liście:

@Test
public void joinStrings() {
List names = Arrays.asList("Sally", "Ted", "Mary");
StringBuilder builder = new StringBuilder();
for (int i = 0; i < names.size(); i++) {
if (i > 0) {
builder.append(", "); }
builder.append(names.get(i));
}
String joined = builder.toString();
Assert.assertEquals("Sally, Ted, Mary", joined);
}

3. Refaktoryzuj kod, aby użyć StringJoiner w Javie 8. Ponownie uruchom test:

StringJoiner joiner = new StringJoiner(", ");
for (String name : names) {
joiner.add(name);
}S
tring joined = joiner.toString();
Refactor the code to use Java 8 streams. Rerun the test:
String joined = names.stream().collect(Collectors.joining(", "));
Refactor the code to use String.join. Rerun the test:
String joined = String.join(", ", names);

4. Usuń rozwiązanie i zostaw nieudany test z komentarzem:

@Test
public void joinStrings() {
List names = Arrays.asList("Sally", "Ted", "Mary");
// Join the names and separate them by ", "
String joined = null;
Assert.assertEquals("Sally, Ted, Mary", joined);
}

Przekaż dalej - kroki 5 i 6 zostawię jako ćwiczenie dla czytelnika.

Ten przykład powinien być na tyle prosty, aby zilustrować, jak tworzyć własne kata o różnej złożoności, wykorzystując testy jednostkowe, aby zapewnić strukturę niezbędną do budowania pewności siebie i zrozumienia. Doceń własną naukę i wiedzę. Kiedy nauczysz się czegoś pożytecznego, zapisz to. Zapisywanie ćwiczeń praktycznych, aby przypomnieć sobie, jak to działa, może być bardzo pomocne. Uchwyć swoją wiedzę i eksplorację w kodzie kata. Kata, których użyłeś do wyostrzenia własnych umiejętności, mogą być również cenne dla innych. Wszyscy mamy rzeczy do nauczenia się i których możemy uczyć. Dzieląc się tym, czego się uczymy z innymi, ulepszamy całą społeczność Java. Jest to niezwykle ważne, aby pomóc nam i innym programistom Javy wspólnie doskonalić nasze umiejętności kodowania.

Naucz się kochać swój stary kod

Co to jest stary system? Jest to stare oprogramowanie, które jest bardzo trudne w utrzymaniu, rozszerzaniu i ulepszaniu. Z drugiej strony jest to również system, który działa i służy biznesowi; inaczej by nie przetrwał. Być może, kiedy po raz pierwszy powstał, stary system miał doskonały projekt, projekt tak dobry, że ludzie zaczęli mówić: "OK, może możemy go użyć również do tego, tego i tego". Staje się przeciążony długiem technicznym, ale to nadal działa. Systemy te mogą być niezwykle odporne. Mimo to programiści nienawidzą pracy na starszych systemach. Może się wydawać, że długu technicznego jest więcej, niż ktokolwiek mógłby kiedykolwiek spłacić. Może powinniśmy po prostu ogłosić upadłość i iść dalej. Dużo łatwiej. A jeśli naprawdę musisz to utrzymać? Co robisz, gdy musisz naprawić błąd? Rozwiązanie numer jeden: taśma klejąca. Zatkaj nos, napraw usterkę - "OK, możemy tego żałować pewnego dnia, ale zróbmy teraz to kopiuj-wklej, żeby to naprawić". Od tego momentu będzie tylko gorzej. Podobnie jak w opuszczonym budynku, może przez długi czas pozostać nieuszkodzony, ale gdy tylko wybije jedno okno, wkrótce pozostanie bez okien nienaruszonych. Już samo zobaczenie jednego rozbitego okna zachęca ludzi do łamania innych. To jest prawo rozbitych okien. Rozwiązanie numer dwa: zapomnij o starym systemie i przepisz go od nowa. Może wyobrażasz sobie na czym polega problem z tym rozwiązaniem? Najczęściej przepisywanie nie działa lub nigdy nie zostanie ukończone. Wynika to z błędu przetrwania. Widzisz stary kod systemowy i mówisz: "Och, daj spokój, jeśli ktokolwiek napisał ten okropny kod, był w stanie go uruchomić, to musi być całkiem proste". Ale nie jest. Możesz uznać ten kod za okropny, ale jest to kod, który przetrwał już wiele bitew. Kiedy zaczynasz od zera, nie znasz historii bitewnych i straciłeś dużo wiedzy na temat domeny. Więc co powinniśmy zrobić? W Japonii istnieje sztuka zwana kintsugi. Kiedy cenny przedmiot pęka, zamiast go wyrzucać, składa się go z powrotem za pomocą złotego proszku wzdłuż linii pęknięć. Złoto podkreśla, że zostało złamane, ale nadal jest piękne. Być może patrzymy na stary kod z niewłaściwego punktu widzenia? Nie mówię, że powinniśmy pozłacać stary kod, ale powinniśmy nauczyć się go naprawiać w taki sposób, że jesteśmy z niego dumni. Wzór dusiciela pozwala nam to zrobić dokładnie. Nazwa pochodzi od drzewa figowego (nie od zabójstwa!), które owija się wokół innych drzew. Jego wzrost stopniowo otacza drzewo żywicielskie, które usycha, aż wszystko, co pozostaje, to figowe pnącza wokół wydrążonego rdzenia. Podobnie zaczynamy zastępować śmierdzącą linię kodu nową, czystą, dokładnie przetestowaną. A następnie, idąc od tego miejsca, tworzymy nową aplikację, która wkrada się na poprzednią, aż całkowicie zastąpi starą. Ale nawet jeśli tego nie dokończymy, połączenie nowego i starego jest o wiele lepsze niż pozwolenie staremu na gnicie. Jest to o wiele bezpieczniejsze niż całkowite przepisanie, ponieważ będziemy stale sprawdzać nowe zachowanie i zawsze możemy cofnąć najnowszą wersję w przypadku wprowadzenia błędów. Starszy kod zasługuje na odrobinę miłości.

Naucz się korzystać z nowych funkcji Java

Java 8 wprowadziła lambdy i strumienie, dwie funkcje zmieniające grę, które dają programistom Java znaczące konstrukcje językowe. Począwszy od Javy 9, cykle wydawnicze występują co sześć miesięcy, a w każdym wydaniu pojawia się więcej funkcji. Powinieneś zainteresować się tymi nowymi funkcjami, ponieważ pomagają pisać lepszy kod. A Twoje umiejętności poprawią się, gdy włączysz nowe paradygmaty językowe do swojego arsenału programowania. Wiele napisano o strumieniach i o tym, jak obsługują one funkcjonalny styl programowania, redukują obszerny kod i sprawiają, że kod jest bardziej czytelny. Spójrzmy więc na przykład ze strumieniami, nie tyle po to, by przekonać Cię do używania strumieni wszędzie, ale by zachęcić Cię do zapoznania się z tą i innymi funkcjami Javy wprowadzonymi od wersji Java 8. Nasz przykład oblicza maksimum, średnie i minimum dla skurczu, wartości rozkurczowe i tętna z zebranych danych z monitorowania ciśnienia krwi. Chcemy zwizualizować te obliczone statystyki podsumowujące za pomocą wykresu słupkowego JavaFX. Oto część naszej klasy modelu BPData, pokazująca tylko potrzebne metody pobierające:

public class BPData {
….
public final Integer getSystolic() {
return systolic.get();
}
public final Integer getDiastolic() {
return diastolic.get();
}
public final Integer getPulse() {
return pulse.get();
}

}

Wykres słupkowy JavaFX tworzy magię tej wizualizacji. Najpierw musimy zbudować prawidłową serię i przesłać nasze przekształcone dane do obiektu wykresu słupkowego. Ponieważ operacja jest powtarzana dla każdej serii, sensowne jest utworzenie pojedynczej metody do parametryzacji zarówno serii wykresu słupkowego, jak i konkretnego pobierającego dane BPData wymaganego do uzyskania dostępu do tych danych. Nasze dane źródłowe są przechowywane w zmiennej sortedList, posortowanej według daty kolekcji elementów BPData. Oto metoda computeStatData, która buduje nasze dane wykresu:

private void computeStatData(
XYChart.Series targetList,
Function f) {
// Set Maximum
targetList.getData().get(MAX).setYValue(sortedList.stream()
.mapToInt(f::apply)
.max()
.orElse(1));
// Set Average
targetList.getData().get(AVG).setYValue(sortedList.stream()
.mapToInt(f::apply)
.average()
.orElse(1.0));
// Set Minimum
targetList.getData().get(MIN).setYValue(sortedList.stream()
.mapToInt(f::apply)
.min()
.orElse(1));
}

Parametr targetList to dane serii wykresu słupkowego, które odpowiadają jednemu z danych dotyczących ciśnienia skurczowego, rozkurczowego lub tętna. Chcemy stworzyć wykres słupkowy z maksimum, średnią i minimum odpowiadającym każdej z tych serii. W związku z tym ustawiamy wartość Y wykresu na te obliczone wartości. Drugim parametrem jest konkretny pobierający z BPData, przekazany jako odwołanie do metody. Używamy tego w strumieniowej metodzie mapToInt, aby uzyskać dostęp do określonych wartości dla tej serii. Każda sekwencja strumienia zwraca maksimum, średnią lub minimum danych źródłowych. Każda metoda strumienia kończącego zwraca obiekt opcjonalny orElse, dzięki czemu nasz wykres słupkowy wyświetla wartość zastępczą 1 (lub 1,0), jeśli strumień danych źródłowych jest pusty. Oto jak wywołać tę metodę computeStatData. Wygodna notacja referencyjna metody ułatwia określenie, którą metodę pobierającą BPData należy wywołać dla każdej serii danych:

computeStatData(systolicStats, BPData::getSystolic);
computeStatData(diastolicStats, BPData::getDiastolic);
computeStatData(pulseStats, BPData::getPulse);

Przed Javą 8 ten kod był znacznie bardziej żmudny do napisania. Tak więc nauka i korzystanie z nowych funkcji Java jest wartościową umiejętnością do opanowania w miarę ciągłego doskonalenia Java. Jeśli chodzi o następną funkcję, co powiesz na sprawdzenie składni rekordów Java 14, funkcję podglądu, aby uprościć klasę BPData?

Naucz się swojego IDE, aby zmniejszyć obciążenie poznawcze

Pracuję dla firmy, która sprzedaje IDE, więc oczywiście powiem, że powinieneś wiedzieć, jak działa Twoje IDE i używać go właściwie. Wcześniej spędziłem 15 lat pracując z wieloma IDE, ucząc się, jak pomagają programistom tworzyć coś użytecznego i jak ich używać do łatwej automatyzacji zadań. Wszyscy wiemy, że IDE zapewniają podświetlanie kodu i pokazują błędy oraz potencjalne problemy, ale każde Java IDE może zrobić o wiele więcej. Poznanie możliwości Twojego środowiska IDE i korzystanie z funkcji, które mają zastosowanie w codziennej pracy, może pomóc w zwiększeniu wydajności. Na przykład twoje IDE:

•  Może wygenerować dla Ciebie kod, dzięki czemu nie musisz go wpisywać. Najczęstsze przykłady to gettery i settery, equals i hashCode oraz toString.
•  Posiada narzędzia do refaktoryzacji, które mogą automatycznie przesuwać kod w określonym kierunku, jednocześnie zapewniając zadowolenie kompilatora.
•  Może przeprowadzać testy i pomagać w debugowaniu problemów. Jeśli używasz System.out do debugowania, zajmie Ci to znacznie więcej czasu niż w przypadku sprawdzania wartości obiektów w czasie wykonywania.
•  Powinien być zintegrowany z systemem zarządzania kompilacją i zależnościami, aby środowisko programistyczne działało w taki sam sposób, jak środowiska testowe i produkcyjne.
•  Może nawet pomóc w przypadku narzędzi lub systemów zewnętrznych w stosunku do kodu aplikacji, na przykład kontroli wersji, dostępu do bazy danych lub przeglądu kodu (pamiętaj, że I w IDE oznacza zintegrowany). Nie musisz opuszczać narzędzia, aby pracować ze wszystkimi aspektami potoku dostarczania oprogramowania.

Korzystając ze środowiska IDE, możesz poruszać się po kodzie w sposób naturalny - znajdując metody, które wywołują ten fragment kodu, lub przechodząc do metody, którą ten kod wywołuje. Możesz przejść bezpośrednio do plików (lub nawet do określonych fragmentów kodu), używając kilku naciśnięć klawiszy zamiast myszy, aby poruszać się po strukturze plików. Narzędzie, w którym zdecydujesz się pisać kod, powinno pomagać Ci skoncentrować się na tym, co rozwijasz. Nie powinieneś myśleć o zawiłościach tego, jak to kodujesz. Przerzucając żmudne rzeczy na IDE, zmniejszasz obciążenie poznawcze i możesz poświęcić więcej mocy mózgu na problem biznesowy, który próbujesz rozwiązać.

Zawrzyjmy umowę: sztuka projektowania interfejsu Java API

API jest tym, czego programiści używają do wykonania jakiegoś zadania. Dokładniej, ustanawia umowę między nimi a projektantami oprogramowania, udostępniając swoje usługi za pośrednictwem tego interfejsu API. W tym sensie wszyscy jesteśmy projektantami API: nasze oprogramowanie nie działa w izolacji, ale staje się przydatne tylko wtedy, gdy wchodzi w interakcję z innym oprogramowaniem napisanym przez innych programistów. Pisząc oprogramowanie jesteśmy nie tylko konsumentami, ale także dostawcami jednego lub więcej API, dlatego każdy programista powinien znać cechy dobrych API i wiedzieć, jak je osiągnąć. Po pierwsze, dobry interfejs API powinien być łatwo zrozumiały i wykrywalny. Powinno być możliwe rozpoczęcie korzystania z niego i, najlepiej, aby dowiedzieć się, jak działa bez czytania jego dokumentacji. W tym celu ważne jest stosowanie spójnego nazewnictwa i konwencji. Brzmi to dość oczywiste; niemniej jednak łatwo jest znaleźć, nawet w standardowym Java API, sytuacje, w których ta sugestia nie została zastosowana. Na przykład, ponieważ możesz wywołać skip(n), aby pominąć pierwsze n elementów Stream, jaka może być dobra nazwa dla metody, która pomija wszystkie elementy Stream, dopóki jeden z nich nie spełni predykatu p? Rozsądną nazwą może być skipWhile(p), ale w rzeczywistości ta metoda nazywa się dropWhile(p). Nie ma nic złego w nazwie dropWhile per se, ale nie jest to zgodne z pominięciem wykonującym bardzo podobną operację. Nie rób tego. Utrzymanie minimalnego interfejsu API to kolejny sposób na ułatwienie korzystania z niego. Zmniejsza to zarówno koncepcje, których należy się nauczyć, jak i koszty ich utrzymania. Po raz kolejny przykłady łamiące tę prostą zasadę można znaleźć w standardowym Java API. Opcjonalne ma statyczną metodę fabryczną (object), która tworzy Opcjonalne zawijanie przekazanego do niego obiektu. Nawiasem mówiąc, używanie metod fabrycznych zamiast konstruktorów jest kolejną cenną praktyką, ponieważ pozwala na większą elastyczność: w ten sposób można również zwrócić instancję podklasy lub nawet null, gdy metoda jest wywoływana z niedozwolonymi argumentami. Niestety, Optional.of zgłasza wyjątek NullPointerException po wywołaniu z wartością null, coś nieoczekiwanego z klasy zaprojektowanej w celu zapobiegania wyjątkom NullPointerException (NPE). To nie tylko łamie zasadę najmniejszego zdziwienia - kolejna rzecz do rozważenia podczas projektowania interfejsu API - ale wymaga wprowadzenia drugiej metody o wartości Nullable, która zwraca pustą wartość Optional po wywołaniu z wartością null. Metoda of ma niespójne zachowanie i jeśli zostanie poprawnie zaimplementowana, metoda ofNullable może zostać pominięta. Inne dobre wskazówki, które mogą ulepszyć Twoje API, to: rozbijaj duże interfejsy na mniejsze części; rozważ zaimplementowanie płynnego API, dla którego tym razem Java Streams jest bardzo dobrym przykładem; nigdy nie zwracaj wartości null, zamiast tego używaj pustych kolekcji i opcji Optional; ograniczyć korzystanie z wyjątków i ewentualnie unikać zaznaczonych. Odnośnie argumentów metod: unikaj długich ich list, zwłaszcza tego samego typu; użyj najsłabszego możliwego typu; utrzymuj je w spójnej kolejności wśród różnych przeciążeń; rozważ varargs. Co więcej, fakt, że dobre API nie wymaga wyjaśnień, nie oznacza, że nie należy go jasno i wyczerpująco dokumentować. Wreszcie, nie oczekuj, że za pierwszym razem napiszesz świetne API. Projektowanie API to proces iteracyjny, a testowanie to jedyny sposób na jego zweryfikowanie i ulepszenie. Napisz testy i przykłady dotyczące Twojego interfejsu API i omów je ze współpracownikami i użytkownikami. Powtarzaj wiele razy, aby wyeliminować niejasne intencje, nadmiarowy kod i nieszczelną abstrakcję.

Spraw, aby kod był prosty i czytelny

jestem wielkim fanem prostego i czytelnego kodu. Każda linijka kodu powinna być jak najbardziej zrozumiała. Każda linia kodu powinna być konieczna. Aby uzyskać czytelny i prosty kod, istnieją dwa aspekty: format i treść. Oto kilka wskazówek, które pomogą Ci napisać czytelny i prosty kod:

Użyj wcięcia, aby wyraźnie ułożyć kod. : Używaj go konsekwentnie. Jeśli pracujesz w projekcie, powinien istnieć szablon kodu. Wszyscy w zespole powinni przyjąć ten sam format kodu. Nie mieszaj spacji z tabulatorami. Zawsze mam skonfigurowane IDE do wyświetlania spacji i kart, dzięki czemu mogę wykryć miks i je naprawić. (Osobiście uwielbiam spacje.) Wybierz spacje lub karty i trzymaj się ich.
Używaj znaczących nazw zmiennych i nazw metod. : Kod jest znacznie łatwiejszy w utrzymaniu, jeśli nie wymaga wyjaśnień. Dzięki znaczącym identyfikatorom Twój kod może mówić sam za siebie, zamiast potrzebować osobnej linii komentarza, aby wyjaśnić, co robi. Unikaj jednoliterowych nazw zmiennych. Jeśli nazwy zmiennych i metod mają jasne znaczenie, zwykle nie będziesz potrzebować komentarzy, aby wyjaśnić, co robi Twój kod.
W razie potrzeby skomentuj swój kod. : Jeśli logika jest bardzo złożona, na przykład zapytania regex itp., użyj dokumentacji, aby wyjaśnić, co próbuje zrobić kod. Gdy pojawią się komentarze, musisz upewnić się, że zostaną zachowane. Nieutrzymane komentarze powodują zamieszanie. Jeśli chcesz ostrzec o czymś opiekuna, upewnij się, że to udokumentowałeś i wyróżniłeś, na przykład dodając "OSTRZEŻENIE" na początku komentarza. Czasami błąd można wykryć i naprawić łatwiej, jeśli oryginalny autor wyrazi swoją intencję lub gdzieś umieści ostrzeżenie.
Nie wpisuj zakomentowanego kodu. : Usuń go, aby poprawić czytelność. Jednym z powszechnych argumentów przemawiających za zakomentowanym kodem jest to, że pewnego dnia zakomentowany kod może być potrzebny. Prawda jest taka, że może pozostać tam przez lata, nieutrzymywana i powodując zamieszanie. Nawet jeśli pewnego dnia zechcesz to odkomentować, blok kodu może nie skompilować się lub nie działać zgodnie z oczekiwaniami, ponieważ baza mogła ulec znacznej zmianie. Nie wahaj się. Po prostu go usuń.
Nie przesadzaj z dodawaniem kodu, który może się przydać w przyszłości. : Jeśli masz za zadanie dostarczyć jakąś funkcjonalność, nie przesadzaj, włączając dodatkową logikę spekulatywną. Każdy dodatkowy kod wiąże się z ryzykiem wprowadzenia błędów i kosztów utrzymania.
Unikaj pisania pełnego kodu. : Staraj się napisać mniej linii kodu, aby wykonać zadanie. Więcej linii wprowadza więcej błędów. Prototypuj najpierw za pomocą burzy mózgów, aby wykonać zadanie, a następnie dopracuj kod. Upewnij się, że każda linia ma silny powód do istnienia. Jeśli jesteś menedżerem lub architektem, nie oceniaj swoich programistów na podstawie liczby dostarczanych wierszy kodu, ale po tym, jak czysty i czytelny jest ich kod.
Naucz się programowania funkcjonalnego, jeśli jeszcze tego nie zrobiłeś. : Jedną z zalet korzystania z funkcji wprowadzonych w Javie 8, takich jak lambdy i strumienie, jest to, że te funkcje mogą pomóc w poprawie czytelności kodu.
Zastosuj programowanie par. : Programowanie w parach to świetny sposób, aby młodszy programista mógł uczyć się od kogoś bardziej doświadczonego. Jest to również świetny sposób na napisanie sensownego kodu, ponieważ musisz wyjaśnić drugiej osobie swoje wybory i rozumowanie. Świetny proces zachęca do ostrożnego pisania kodu zamiast zrzucania kodu.

Kod będzie zawierał mniej błędów, jeśli będzie prosty i czytelny: kod złożony prawdopodobnie będzie zawierał więcej błędów; kod, który nie jest łatwy do zrozumienia, prawdopodobnie zawiera więcej błędów. Mamy nadzieję, że te wskazówki pomogą Ci poprawić Twoje umiejętności i poprawić kod, aby dostarczać kod, który jest prosty i czytelny!

Stwórz swoją Java Groovier

Ekran miał kolor cyberpunkowej powieści otwartej do pierwszej linijki. Wpatrywałem się w to, martwiąc się, że nigdy nie skończę tej nocy. Rozległo się pukanie w ścianę mojego boksu. Mój szef stał tam i czekał.
"Jak leci?" powiedziała.
- Java jest taka gadatliwa - westchnęłam. "Chcę tylko pobrać dane z usługi i zapisać je w bazie danych. Pływam w budowniczych, fabrykach, kodzie bibliotecznym, blokach try/catch…"
"Po prostu dodaj Groovy".
"Hę? Jak by to pomogło?"
Ona usiadła. "Mogę prowadzić?"
"Proszę."
"Pozwól, że przedstawię ci szybkie demo". Otworzyła wiersz polecenia i wpisała groovyConsole. Na ekranie pojawił się prosty GUI. "Powiedz, że chcesz wiedzieć, ilu astronautów jest w tej chwili w kosmosie. W Open Notify jest usługa, która Ci to daje. Wykonała następujące czynności w konsoli Groovy:
v def jsonTxt = 'http://api.open-notify.org/astros.json'.toURL().text

Odpowiedź JSON wróciła z liczbą astronautów, komunikatem o stanie i zagnieżdżonymi obiektami odnoszącymi każdego astronautę do statku.
"Groovy dodaje toURL do String, aby wygenerować java.net.URL i getText do adresu URL, aby pobrać dane, do których uzyskujesz dostęp jako tekst".
- Słodko - powiedziała. "Teraz muszę zmapować to do klas Java i użyć biblioteki takiej jak Gson lub Jackson…"
"Nie. Jeśli potrzebujesz tylko liczby osób w kosmosie, użyj JsonSlurper.
"Że co?"
Wpisała:

def number = new JsonSlurper().parseText(jsonTxt).number

"Metoda parseText zwraca Object", powiedziała, "ale nas to nie obchodzi typ tutaj, więc po prostu przeanalizuj".
Okazało się, że w kosmosie znajdowało się sześć osób, wszyscy na Międzynarodowej Stacji Kosmicznej.
- Dobrze - powiedziała. "Powiedz, że chcę podzielić odpowiedź na zajęcia. Co wtedy? Czy jest port Gson do Groovy?
Potrząsnęła głową. "Nie potrzebuję tego. To wszystko kody bajtowe pod maską. Wystarczy utworzyć instancję klasy Gson i wywołać metody jak zwykle:

@Canonical
class Assignment { String name; String craft }
@Canonical
class Response { String message; int number; Assignment[] people }
new Gson().fromJson(jsonTxt, Response).people.each { println it }

"Adnotacja kanoniczna dodaje doString, equals, hashCode, a konstruktor domyślny, konstruktor nazwanych argumentów i konstruktor krotek dla każdej klasy."
"Wspaniały! Jak mam zapisać astronautów w bazie danych?"
"Wystarczająco łatwe. Użyjmy H2 dla tej próbki:

Sql sql = Sql.newInstance(url: 'jdbc:h2:~/astro',
driver: 'org.h2.Driver')
sql.execute '''
create table if not exists ASTRONAUTS(
id int auto_increment primary key,
name varchar(50),
craft varchar(50)
)
'''
response.people.each {
sql.execute "insert into ASTRONAUTS(name, craft)" +
"values ($it.name, $it.craft)"
}s
ql.close()

"Klasa Groovy Sql tworzy tabelę za pomocą wielowierszowego ciągu i wstawia wartości za pomocą interpolowanych ciągów:

sql.eachRow('select * from ASTRONAUTS') {
row -> println "${row.name.padRight(20)} aboard ${row.craft}"
}

- Gotowe - powiedziała - ze sformatowanym drukiem i wszystkim.

Patrzyłem na wynik. "Czy masz pojęcie, ile linijek Javy by to było?" Zapytałem. Uśmiechnęła się. "Dużo. Nawiasem mówiąc, wszystkie wyjątki w Groovy są odznaczone, więc nie potrzebujesz nawet bloku try/catch. Jeśli użyjemy withInstance zamiast newInstance, połączenie również zostanie automatycznie zamknięte. Wystarczająco dobry?" Ukłoniem się. "Teraz po prostu zapakuj różne części w klasę i możesz ją wywołać z Javy". Wyszła, a ja nie mogłem się doczekać, aż reszta mojej Javy będzie bardziej ciekawa.

Minimalne konstruktory

Wzorzec, który regularnie widzę, to znacząca praca wykonana w konstruktorze: weź zestaw argumentów i przekonwertuj je na wartości dla pól. Często wygląda to tak:

public class Thing {
private final Fixed fixed;
private Details details;
private NotFixed notFixed;
// more fields
public Thing(Fixed fixed,
Dependencies dependencies,
OtherStuff otherStuff) {
this.fixed = fixed;
setup(dependencies, otherStuff);
}
}

Zakładam, że setup inicjuje pozostałe pola na podstawie zależności i innych rzeczy, ale z sygnatury konstruktora nie jest dla mnie jasne, jakie wartości są potrzebne do utworzenia nowej instancji. Nie jest też oczywiste, które pola mogą się zmienić w trakcie życia obiektu, ponieważ nie mogą być ostateczne, chyba że zostaną zainicjowane w konstruktorze. Wreszcie, ta klasa jest trudniejsza do testowania jednostkowego niż powinna, ponieważ jej utworzenie wymaga utworzenia odpowiedniej struktury w argumentach, które mają zostać przekazane do konfiguracji. Co gorsza, od czasu do czasu widziałem konstruktorów takich jak ten:

public class Thing {
private Weather currentWeather;
public Thing(String weatherServiceHost) {
currentWeather = getWeatherFromHost(weatherServiceHost);
}
}

który wymaga połączenia internetowego i usługi do utworzenia instancji. Na szczęście jest to teraz rzadkie. Wszystko to zostało zrobione w najlepszych intencjach, aby ułatwić tworzenie instancji poprzez zachowanie "hermetyzujące". Uważam, że to podejście jest spuścizną po C++, gdzie programiści mogą kreatywnie używać konstruktorów i destruktorów do kontrolowania zasobów. Łatwiej jest łączyć klasy w hierarchii dziedziczenia, jeśli każda z nich zarządza własnymi zależnościami wewnętrznymi. Wolę stosować podejście inspirowane moimi doświadczeniami z Modula-3, które polega na tym, że konstruktor jedynie przypisuje wartości do pól: jego jedynym zadaniem jest stworzenie poprawnej instancji. Jeśli jest więcej do zrobienia, używam metody fabrycznej:

public class Thing {
private final Fixed fixed;
private final Details details;
private NotFixed notFixed;
public Thing(Fixed fixed, Details details, NotFixed notFixed) {
this.fixed = fixed;
this.details = details;
this.notFixed = notFixed;
}
public static Thing forInternationalShipment(
Fixed fixed,
Dependencies dependencies,
OtherStuff otherStuff) {
final var intermediate = convertFrom(dependencies, otherStuff);
return new Thing(fixed,
intermediate.details(),
intermediate.initialNotFixed());
}
public static Thing forLocalShipment(Fixed fixed,
Dependencies dependencies) {
return new Thing(fixed,
localShipmentDetails(dependencies),
NotFixed.DEFAULT_VALUE);
}
}
final var internationalShipment =
Thing.forInternationalShipment(fixed, dependencies, otherStuff);
final var localShipment = Thing.forLocalShipment(fixed, dependencies);

Zaletami są to, że:

•  Teraz jestem bardzo jasny co do cyklu życia pól instancji.
•  Oddzieliłem kod do tworzenia instancji obiektu od jego użycia.
•  Nazwa metody fabrycznej sama się opisuje, w przeciwieństwie do konstruktora.
•  Klasę i jej instancję łatwiej testować osobno.

Wadą jest brak możliwości udostępnienia konstruktora

implementacja w hierarchiach dziedziczenia, ale można to rozwiązać, udostępniając pomocnicze metody pomocnicze i, co bardziej przydatne, korzystając z podpowiedzi, aby uniknąć głębokiego dziedziczenia. Wreszcie, dla mnie jest to również powód, aby uważać na to, jak pracować z frameworkami wstrzykiwania zależności. Jeśli tworzenie obiektu jest skomplikowane, umieszczenie wszystkiego w konstruktorze, ponieważ ułatwia to korzystanie z narzędzi opartych na odbiciach, wydaje mi się zacofane. Zwykle zamiast tego można zarejestrować metodę fabryczną jako sposób na tworzenie nowych instancji. Podobnie, użycie refleksji do ustawienia pól prywatnych bezpośrednio do "enkapsulacji" (lub w celu uniknięcia pisania konstruktora) łamie system typów i utrudnia testowanie jednostkowe; lepiej ustawić pola za pomocą minimalnego konstruktora. Używaj @Inject lub @Autowired ostrożnie i wyraź wszystko.

Nazwij dane

Ponieważ java.util.Date jest powoli, ale zdecydowanie wycofywany ze zbioru Sun, a java.time przejmuje jego płaszcz, warto zatrzymać się, aby nauczyć się kilku lekcji z jego niespokojnego życia, zanim pozwolisz mu odpocząć w spokoju. Najbardziej oczywistą lekcją jest to, że obsługa daty i czasu jest trudniejsza niż ludzie się spodziewają - nawet jeśli się tego spodziewają. Powszechnie uznaną prawdą jest to, że pojedynczy programista mający przekonanie, że rozumie daty i godziny, musi potrzebować przeglądu kodu. Ale nie na tym chcę się tutaj skupić, ani na znaczeniu niezmienności dla typów wartości, co sprawia, że klasa (nie) nadaje się do tworzenia podklas, ani na tym, jak używać klas zamiast liczb całkowitych do wyrażenia bogatej domeny. Kod źródłowy składa się z odstępów, interpunkcji i nazw. Wszystkie one przekazują czytelnikowi znaczenie, ale w imionach niesie się (lub odrzuca) większość znaczenia. Nazwy mają znaczenie. Dużo. Biorąc pod uwagę jego nazwę, byłoby miło, gdyby Data reprezentowała datę kalendarzową, tj. Konkretny dzień… ale tak nie jest. Reprezentuje punkt w czasie, który może być postrzegany jako posiadający składnik daty. Jest to częściej określane jako data - godzina lub, jeśli chcesz umieścić ją w kodzie, DateTime. Czas też działa, ponieważ jest nadrzędną koncepcją. Czasami znalezienie właściwej nazwy jest trudne; w tym przypadku tak nie jest. Teraz rozumiemy, co rozumiemy przez datę, datę-godzinę i datę, co robi getDate? Czy zwraca całą wartość daty i godziny? A może tylko składnik daty? Ani: zwraca dzień miesiąca. W kręgach programistycznych ta wartość jest częściej i konkretnie określana jako dzień miesiąca, a nie data, termin zwykle zarezerwowany do reprezentowania daty kalendarzowej. A skoro już tu jesteśmy, tak, getDay lepiej byłoby nazwać getDayOfWeek. Ważne jest nie tylko wybranie poprawnej nazwy, ale także rozpoznawanie i rozwiązywanie niejednoznacznych terminów, takich jak dzień (tydzień, miesiąc, rok…?). Zwróć uwagę, że lepiej jest rozwiązać problemy z nazewnictwem, wybierając lepszą nazwę niż za pomocą Javadoc. Nazwy są powiązane z konwencjami, a konwencje z imionami. Jeśli chodzi o konwencje, wolą jedną (niewiele), wolą wyrażać ją wyraźnie i wolą taką, która jest powszechnie rozpoznawana i łatwa w użyciu niż taka, która jest niszowa i podatna na błędy (tak, C, patrzę na ciebie). Na przykład Apollo 11 wylądował na Księżycu o godzinie 20:17 dwudziestego dnia lipca (siódmego miesiąca) w 1969 r. (CE, UTC itp.). Ale jeśli wywołasz getTime, getDate, getMonth i getYear oczekując tych liczb, spodziewaj się rozczarowania: getTime zwraca ujemną liczbę milisekund od początku 1970 roku; getDate zwraca 20 (zgodnie z oczekiwaniami liczy od 1); getMonth zwraca 6 (miesięcy liczą się od 0); a getYear zwraca 69 (lata liczą się od 1900, a nie 0 i nie 1970). Dobre nazewnictwo jest częścią projektowania. Wyznacza oczekiwania i komunikuje model, pokazując, jak coś należy rozumieć i używać. Jeśli chcesz powiedzieć czytelnikowi getMillisSince1970, nie mów getTime. Konkretne nazwy inspirują Cię do rozważenia alternatyw, do zakwestionowania, czy ujmujesz właściwą abstrakcję we właściwy sposób. To nie tylko etykietowanie i nie tylko java.util.Date: chodzi o kod, który piszesz i kod, którego używasz.

Konieczność technologii przemysłowych

Java mogła zostać nazwana następnym COBOL-em, ale niekoniecznie jest to zła rzecz. COBOL to niezwykle udana technologia. Niezawodny, spójny i łatwy do odczytania, był koniem roboczym ery informacyjnej, zarządzając większością systemów o znaczeniu krytycznym na świecie. Jeśli składnia wymaga dużo dodatkowego pisania, jest to równoważone przez samą liczbę czytelników, którzy musieli zastanowić się nad jej zachowaniem. Modne stosy oprogramowania brzmią fajnie, a ponieważ większość z nich jest dość niedojrzała, zawsze można się wiele nauczyć - ale świat potrzebuje niezawodnego oprogramowania, które może funkcjonować w przemyśle. Nowy sprytny idiom lub nieco zaciemniony paradygmat może być świetną zabawą, ale z definicji są one owiane niewiadomymi. Mamy obsesję na punkcie znalezienia jakiegoś magicznego sposobu na pstryknięcie palcami i czy powstanie kolejny system klasy korporacyjnej, ale ciągle zapominamy, że ponad trzy dekady temu Frederick Brooks Jr. nie może istnieć. Nie potrzebujemy kolejnej modnej zabawki, aby rozwiązywać prawdziwe problemy ludzi. Musimy włożyć w to myślenie i pracę, aby w pełni zrozumieć i skodyfikować niezawodne rozwiązania. Systemy, które działają tylko w słoneczne dni lub które muszą być przepisywane mniej więcej co roku, nie zaspokajają naszych rosnących potrzeb w zakresie zarządzania złożonością współczesnego społeczeństwa. Nie ma znaczenia, jak to działa, jeśli jest nieprzewidywalne, gdy zawiedzie. Zamiast tego musimy w pełni zawrzeć naszą wiedzę w niezawodnych, wielokrotnego użytku i nadających się do ponownego komponowania komponentach, wykorzystując je tak długo, jak to możliwe, aby nadążać za chaotyczną naturą naszego obecnego okresu w historii. Jeśli kod nie trwa, prawdopodobnie nie warto było go pisać. Java to świetna technologia do tego celu: wystarczająco nowa, aby zawierać nowoczesne funkcje językowe, ale wystarczająco dojrzała, aby była godna zaufania. Staliśmy się lepsi w organizowaniu dużych baz kodu, a istnieje wystarczająco dużo produktów, narzędzi i ekosystemów wspierających, aby skupić się z powrotem na rzeczywistych problemach biznesowych, a nie na czysto technicznych. Jest to solidny zestaw do oddzielenia systemów od ich środowisk, ale wystarczająco standardowy, aby znaleźć doświadczonych pracowników. Jeśli nie mówi się o tym w mieście, jest to przynajmniej bardzo niezawodna, stabilna platforma, na której można budować systemy, które przetrwają dziesięciolecia, i wydaje się, że jest to to, czego oboje chcemy i potrzebujemy w naszych obecnych wysiłkach rozwojowych. Moda nie powinna dyktować inżynierii. Tworzenie oprogramowania to dziedzina wiedzy i organizacji. Jeśli nie wiesz, jak zachowają się części, nie możesz zapewnić, że zachowa się całość. Jeśli rozwiązanie jest niewiarygodne, to tak naprawdę tylko powiększa problem, a nie go rozwiązuje. Może fajnie jest po prostu rzucić razem trochę kodu, który w pewnym sensie działa, ale jest to profesjonalne, jeśli tworzymy rzeczy, które mogą wytrzymać rzeczywistość i ciągle nucić.

Zbuduj tylko te części, które się zmieniają, a resztę wykorzystaj ponownie

Jako programiści Java spędzamy dużo czasu czekając na uruchomienie kompilacji, często dlatego, że nie uruchamiamy ich wydajnie. Możemy dokonać drobnych ulepszeń, zmieniając nasze zachowanie. Na przykład, moglibyśmy uruchomić tylko podmoduł zamiast całego projektu i nie uruchamiać czystego przed każdą kompilacją. Aby zrobić większą różnicę, powinniśmy skorzystać z buforowania kompilacji oferowanego przez nasze narzędzia do budowania, a mianowicie Gradle, Maven i Bazel. Buforowanie kompilacji to ponowne wykorzystanie wyników z poprzedniego uruchomienia w celu zminimalizowania liczby kroków kompilacji (np. zadań Gradle, celów Mavena, działań Bazela) wykonanych podczas bieżącego uruchomienia. Każdy krok kompilacji, który jest idempotentny, co oznacza, że generuje te same dane wyjściowe dla danego zestawu danych wejściowych, może być buforowany. Na przykład dane wyjściowe kompilacji Java to drzewo plików klas generowanych przez kompilator Java, a dane wejściowe to czynniki, które wpływają na tworzone pliki klas, takie jak sam kod źródłowy, wersja Java, system operacyjny i wszelkie flagi kompilatora . Biorąc pod uwagę te same warunki uruchamiania i kod źródłowy, krok kompilacji Java za każdym razem tworzy te same pliki klas. Dlatego zamiast uruchamiać etap kompilacji, narzędzie do budowania może wyszukać w pamięci podręcznej wszystkie poprzednie uruchomienia z tymi samymi danymi wejściowymi i ponownie wykorzystać dane wyjściowe. Buforowanie kompilacji nie ogranicza się do kompilacji. Narzędzia do kompilacji definiują standardowe dane wejściowe i wyjściowe dla innych typowych kroków kompilacji, takich jak analiza statyczna i generowanie dokumentacji, a także pozwalają nam skonfigurować dane wejściowe i wyjściowe dla dowolnego etapu kompilacji, który można buforować. Ten typ buforowania jest szczególnie przydatny w przypadku kompilacji wielomodułowych. W projekcie z 4 modułami, z których każdy ma 5 kroków kompilacji, czysta kompilacja musi wykonać 20 kroków. Jednak przez większość czasu modyfikujemy kod źródłowy tylko w jednym module. Jeśli żadne inne projekty nie zależą od tego modułu, oznacza to, że musimy tylko wykonać kroki w dół od generowania kodu źródłowego; w tym przykładzie tylko 4: wyjścia pozostałych 16 kroków można wyciągnąć z pamięci podręcznej, oszczędzając czas i zasoby. Kompilacja przyrostowa Gradle, którą w wynikach kompilacji widzimy jako AKTUALNA, implementuje buforowanie kompilacji na poziomie projektu. Lokalna pamięć podręczna, taka jak ta wbudowana w Gradle i dostępna jako rozszerzenie Mavena, działa nawet przy zmianie obszarów roboczych, gałęzi Git i opcji wiersza poleceń. Wspólny efekt zdalnego buforowania kompilacji dostępny w Gradle, Maven i Bazel zapewnia dodatkowe korzyści. Jednym z typowych przypadków użycia zdalnego buforowania jest pierwsza kompilacja po pobraniu ze zdalnego repozytorium kontroli wersji. Po ściągnięciu z pilota musimy zbudować projekt na naszej maszynie, aby skorzystać z tych zmian. Ale ponieważ nigdy nie zbudowaliśmy tych zmian na naszym komputerze, nie ma ich jeszcze w naszej lokalnej pamięci podręcznej. Jednak system ciągłej integracji już zbudował te zmiany i przesłał wyniki do udostępnionej zdalnej pamięci podręcznej, dzięki czemu otrzymujemy trafienie w pamięci podręcznej ze zdalnej pamięci podręcznej, oszczędzając czas potrzebny na wykonanie tych kroków kompilacji lokalnie. Korzystając z buforowania kompilacji w naszych kompilacjach Java, możemy udostępniać wyniki między naszymi lokalnymi kompilacjami, agentami serwera CI i całym zespołem, co skutkuje szybszymi kompilacjami dla wszystkich i mniejszą liczbą zasobów wykonujących te same operacje w kółko.

Projekty Open Source nie są magiczne

Jedną z moich największych bolączek jest słuchanie ludzi mówiących, że technologia X, język, narzędzie do budowania, itp. działa jak magia. Jeśli ten projekt jest open source, to to słyszę "Jestem zbyt leniwy, żeby sprawdzić jak to działa" i przypomina mi się Trzecie prawo Clarke′a, że każda wystarczająco zaawansowana technologia jest indistinguishable from magic.W czasach nowoczesnej sieci, łatwiej niż kiedykolwiek wcześniej jest zajrzeć do przewodników referencyjnych i kodu źródłowego i dowiedzieć się, jak ta technologia działa. Wiele projektów open source, takich jak język programowania Apache Groovy, na przykład, ma stronę internetową (w tym przypadku groovy-lang.org), która zawiera listę miejsc, gdzie można gdzie można znaleźć dokumentację, przewodniki, bug tracker, a nawet linki do samego kodu źródłowego. Jeśli szukasz pomocy w rozpoczęciu nauki, przewodniki i tutoriale są świetnym miejscem do rozpoczęcia. Jeśli jesteś bardziej wzrokowcem lub uczniem praktycznym, wiele internetowych platform edukacyjnych oferuje kursy wprowadzające do nauki nowych języków poprzez laboratoria, ćwiczenia i pracę w grupach. Czasami są one nawet swobodnie dostępne, aby technologie były szerzej znane. Po poznaniu podstawowej składni i struktur danych oraz rozpoczęciu używania ich w swoich własnych projektach, prawdopodobnie zaczniesz napotykać nieoczekiwane zachowania lub a nawet błędy. Niezależnie od tego, jaki ekosystem wybierzesz, w pewnym momencie to się stanie. w pewnym momencie. To po prostu część świata, w którym żyjemy. W pierwszej kolejności powinieneś poszukać issue tracker jak Jira lub GitHub issues, aby zobaczyć, czy inni mają ten sam problem. Jeśli tak, mogą istnieć obejścia, poprawki w nowszej wersji lub czas, kiedy ten problem zostanie naprawiony. Znalezienie miejsca, w którym społeczność Twojej technologii współpracuje. Czasami są to czaty, fora lub listy dyskusyjne. Projekty w szczególności w fundacji Apache, mają tendencję do korzystania z infrastruktury Apache a nie z produktów komercyjnych. Znalezienie tego miejsca jest najlepszym sposobem na przejście od "magii" do jasności. Nawet po opanowaniu danej technologii, nauka jest procesem ciągłym i trzeba ją kontynuować. procesem i będziesz musiał to robić dalej. Nowe wersje mogą dodawać nowe funkcje lub zmienić zachowania w sposób, który będziesz musiał zrozumieć. Dołącz do listy mailingowej lub weź udział w konferencjach z osobami odpowiedzialnymi za open source, aby dowiedzieć się tego, czego co jest potrzebne do uaktualniania projektów. Jeśli jesteś już ekspertem w danej dziedzinie, jest to świetny sposób, aby przyczynić się do odkrycia "magii" dla innych. wszystkich innych. Wreszcie, jeśli zauważysz, że coś jest niejasne lub brakuje, wiele projektów chętnie wiele projektów chętnie przyjmuje wkład, zwłaszcza w dokumentację. Osoby prowadzące projekt to Często są to ludzie z normalną pracą i innymi priorytetami, więc mogą nie odpowiedzieć od razu, ale jest to najlepszy sposób, aby pomóc wszystkim osiągnąć sukces i odkryć odkryć "magię" dla następnej generacji użytkowników.

Optional jest monadą łamiącą prawo, ale dobrym typem

W większości języków programowania, typy puste-lub-niepuste są dobrze zachowującymi się monadami. (Oznacza to, że ich mechanika spełnia kilka definicji i przestrzega kilku praw, które gwarantują bezpieczną (de)kompozycję obliczeń. Metody Opcji spełniają te definicje, ale łamią prawa. Nie bez konsekwencji…

Definicja monady

Do zdefiniowania monady - w ujęciu Optional - potrzebne są trzy rzeczy:

1. Sam typ Optional< >.
2. Metoda ofNullable(T), która opakowuje wartość T w typ Optional< T >.
3. Metoda flatMap(Function< T, Optional< U > >), która stosuje daną funkcję do wartości, która jest zawinięta przez Optional, na którym jest wywoływana Istnieje alternatywna definicja wykorzystująca map zamiast flatMap, ale jest ona zbyt długa, aby się tu zmieścić.

Prawa monad

Teraz robi się ciekawie - monada musi spełniać trzy prawa, aby być jednym z fajnych dzieciaków. W terminach opcjonalnych:

1. Dla Function< T, Optional < U > > f i wartości v, f.apply(v) musi być równe Optional.ofNullable(v).flatMap(f). Ta lewa tożsamość gwarantuje, że nie ma znaczenia, czy zastosujesz funkcję bezpośrednio, czy pozwolisz Optional to zrobić.
2. Wywołanie flatMap(Optional::ofNullable) zwraca Optional, które jest równe temu, na którym je wywołałeś. Ta prawa tożsamość gwarantuje, że zastosowanie no-ops nie zmienia niczego.
3. Dla Optional< T > o i dwóch funkcji Function< T,
Optional< U > > f i Function > g, wyniki o.flatMap(f).flatMap(g) i o.flatMap(v -> f.apply(v).flatMap(g)) muszą być równe. Ta asocjatywność gwarantuje, że nie ma znaczenia, czy funkcje są flatmapowane indywidualnie, czy jako kompozycja. Podczas gdy Optional trzyma się w większości przypadków, nie robi tego dla konkretnego przypadku krawędziowego.

Przyjrzyj się implementacji flatMap:

public < U > Optional< U > flatMap(Function< T, Optional< U >> f) {
if (!isPresent()) {
return empty();
} else {
return f.apply(this.value);
}
}

Widać, że nie stosuje funkcji do pustego Optional, co ułatwia złamanie lewej tożsamości:

Function< Integer, Optional< String > > f =.
i -> Optional.of(i == null ? "NaN" : i.toString());
// nie są równe
Optional< String > containsNaN = f.apply(null);
Optional< String > isEmpty = Optional.ofNullable(null).flatMap(f);

To nie jest wspaniałe, ale jest jeszcze gorsze dla mapy. Tutaj asocjatywność oznacza, że biorąc pod uwagę Optional< T > o i dwie funkcje Function< T , U > f i
Function< U, V > g, wyniki o.map(f).map(g) i o.map(f.andThen(g)) muszą być równe:
Function f = i -> i == 0 ? null : i;
Function g = i -> i == null ? "NaN" : i.toString();
// poniższe nie są równe
Optional< String > containsNaN = Optional.of(0).map(f.andThen(g));
Optional< String > isEmpty = Optional.of(0).map(f).map(g);

Czyli co?

Przykłady mogą wydawać się wymyślone, a znaczenie praw niejasne, ale wpływ jest prawdziwy: w łańcuchu Optional nie można mechanicznie łączyć i dzielić operacji, ponieważ może to zmienić zachowanie kodu. Jest to niefortunne, ponieważ właściwe monady pozwalają ci je zignorować, gdy chcesz skupić się na czytelności lub logice domeny. Ale dlaczego Optional jest złamaną monadą? Ponieważ null-safety jest bardziej ważne! Aby utrzymać prawa, Optional musiałoby być w stanie zawierać null, będąc jednocześnie niepustym. I musiałby przekazać go do funkcji przekazanych do map i flatMap. Wyobraź sobie, że wszystko, co zrobiłeś w mapie i flatMap, musiało sprawdzić, czy nie zawiera null! Ta Opcja byłaby świetnym onad, ale zapewnia zero null-safety. Nie, jestem szczęśliwy, że dostaliśmy Opcję, którą dostaliśmy.

Pakiet po funkcji z domyślnym modyfikatorem dostępu

Wiele aplikacji biznesowych jest pisanych przy użyciu architektury trójwarstwowej: warstwy widoku, biznesu i danych, a wszystkie obiekty modelu są używane przez wszystkie trzy warstwy. W niektórych bazach kodu, klasy dla tych aplikacji są zorganizowane według warstw. W niektórych aplikacjach, w których istnieje potrzeba rejestracji różnych użytkowników i firmy, w której pracują, struktura kodu skutkowałaby czymś takim jak:

tld.domain.project.model.Company
tld.domain.project.model.User
tld.domain.project.controllers.CompanyController
tld.domain.project.controllers.UserController
tld.domain.project.storage.CompanyRepository
tld.domain.project.storage.UserRepository
tld.domain.project.service.CompanyService
tld.domain.project.service.UserService

Używanie takiej struktury pakietowej dla twoich klas wymaga, aby wiele metod było publicznych. UserService musi być w stanie czytać i zapisywać Użytkowników do pamięci masowej, a ponieważ UserRepository znajduje się w innym pakiecie, prawie wszystkie metody UserRepository musiałyby być publiczne. Organizacja może mieć politykę wysyłania wiadomości e-mail do użytkownika, aby powiadomić go, gdy jego hasło zostało zmienione. Taka polityka może być zaimplementowana w UserService. Ponieważ metody w UserRepository są publiczne, nie ma zabezpieczenia przed tym, że inna część aplikacji wywoła metodę w UserRepository, która zmieni hasło, ale nie spowoduje wysłania powiadomienia. Kiedy ta aplikacja zostanie zaktualizowana w celu włączenia jakiegoś modułu obsługi klienta lub interfejsu obsługi internetowej, niektóre funkcje w tych modułach mogą chcieć zresetować hasło. Ponieważ te funkcje są budowane w późniejszym czasie, być może po tym, jak nowi programiści dołączyli do zespołu, ci programiści mogą być skłonni do uzyskania dostępu do UserRepository bezpośrednio z CustomerCareService zamiast wywoływania UserService i wywołując powiadomienie. Język Java dostarcza mechanizmu, który pozwala temu zapobiec: modyfikatory dostępu. Domyślny modyfikator dostępu oznacza, że nie deklarujemy jawnie modyfikatora dostępu dla klasy, pola, metody itp. Zmienna lub metoda zadeklarowana bez żadnego modyfikatora dostępu jest dostępna tylko dla innych klas w tym samym pakiecie. Jest to również nazywane package-private. Aby skorzystać z tego mechanizmu ochrony dostępu, baza kodu powinna być zorganizowana w hierarchię pakietów. Te same klasy co wcześniej byłyby opakowane w ten sposób:

tld.domain.project.company.Company
tld.domain.project.company.CompanyController
tld.domain.project.company.CompanyService
tld.domain.project.company.CompanyRepository
tld.domain.project.user.User
tld.domain.project.user.UserController
tld.domain.project.user.UserService
tld.domain.project.user.UserRepository

Kiedy zorganizowane w ten sposób, żadna z metod w UserRepository nie musiałaby być publiczna. Wszystkie mogłyby być pakietem prywatnym i nadal byłyby dostępne dla UserService. Metody UserService mogą być udostępnione publicznie. Każdy programista budujący CustomerCareService, w pakiecie tld.domain.project.support, nie mógłby wywoływać metod z UserRepository i powinien wywoływać metody UserService. W ten sposób struktura kodu i modyfikatory dostępu pomagają zapewnić, że aplikacja nadal przestrzega polityki wysyłania powiadomień. Ta strategia organizacji klas w twojej bazie kodowej pomoże zmniejszyć sprzężenie w twojej bazie kodowej.

Odkryj na nowo JVM dzięki Clojure

Jakiś czas temu, w 2007 roku, mój biurowy klub książkowy przeczytał książkę Java Concurrency in Practice autorstwa Briana Goetza (Addison-Wesley). Nie byliśmy jeszcze daleko od przedmowy tej ważnej książki, kiedy wpadliśmy w panikę na myśl o tym, jak błędne było nasze naiwne zrozumienie modelu pamięciowego Javy i jak łatwo wprowadzić błędy do kodu wielowątkowego. Były słyszalne odgłosy gazu i co najmniej jeden zgłoszony koszmar. Tworząc wysoce współbieżną ofertę w chmurze, potrzebowaliśmy języka, który nie zaśmiecałby naszej bazy kodu minami ze współdzielonego, zmiennego stanu. Wybraliśmy Clojure: ma solidne odpowiedzi na współbieżność i sprzyja funkcjonalnym, wydajnym transformacjom niezmiennych danych. Działa na dobrze znanej maszynie JVM, płynnie współpracując z ogromnym ekosystemem bibliotek Java. Chociaż niektórzy wahali się co do nieznanej składni Lispa i ponownego nauczenia się, jak programować bez mutowania zmiennych, była to świetna decyzja. Odkryliśmy zalety REPL-centrycznego (read-val-print loop) przepływu pracy:

•  Brak przebudowy lub ponownego uruchomienia w celu przetestowania zmian.
•  Eksploracja działającego systemu i natychmiastowe wypróbowywanie wariantów
•  Przyrostowe budowanie i udoskonalanie pomysłów

Doceniliśmy uprzedzenie Clojure do pracy z danymi za pomocą standardowych struktur i jego bogatej, opiniotwórczej biblioteki rdzeniowej. Nie trzeba tworzyć wielu klas - każda z własnym, niekompatybilnym API - aby wymodelować cokolwiek. Na nowo odkryłem radość i energię w programowaniu. Rozmowa na konferencji Strange Loop, na temat kodowania na żywo spektakli muzycznych w Clojure przy użyciu Overtone, skłoniła mnie do zastanowienia: jeśli Clojure jest wystarczająco szybki, by tworzyć muzykę, to z pewnością może też obsługiwać oświetlenie sceniczne? To doprowadziło do powstania Afterglow, projektu, który pochłonął mnie na jakiś czas. Znalezienie sposobu na napisanie efektów świetlnych w funkcjonalnym stylu było zagadką, ale funkcjonalny metronom Overtone zainspirował moje funkcje efektowe, mapujące czas muzyczny na pozycje, kolory i intensywność oświetlenia. Nauczyłem się na nowo trygonometrii i algebry liniowej, aby skierować różne światła na ten sam punkt w przestrzeni. ten sam punkt w przestrzeni. Odkryłem jak stworzyć pożądany kolor przy użyciu Wykorzystując diody LED o różnych odcieniach. Kodowanie na żywo oświetlenia scenicznego to mnóstwo zabawy. Następnie chciałem zsynchronizować metronom Afterglow z utworami odtwarzanymi na CDJ (dzisiejszych cyfrowych gramofonach DJ-skich), których używam do miksowania muzyki. Ich protokół jest zastrzeżony i nieudokumentowany, ale byłem zdeterminowany. Skonfigurowałem sniffer sieciowy i rozgryzłem go. Wczesny sukces doprowadził do ekscytującego wkładu z całego świata, więc napisałem bibliotekę Beat Link, aby ułatwić korzystanie z tego, czego się nauczyliśmy. Napisałem ją w Javie, aby była powszechnie zrozumiała, ale odkryłem, że używanie Clojure sprawiło, że pisanie w Javie stało się uciążliwe. Ludzie wykorzystali ją i przenieśli na inne języki. Stworzyłem szybkie demo dla producenta show na temat używania Beat Link do wyzwalania zdarzeń MIDI, na które mogło reagować jego oprogramowanie wideo i konsola oświetleniowa. Stało się to moim najpopularniejszym projektem, ponieważ jest użyteczne dla nie-programistów. Artyści wciąż robią nowe, fajne rzeczy z Beat Link Trigger, a ja, jako gość na festiwalach muzycznych i koncertach, widziałem rezultaty. Ponieważ jest to Clojure, użytkownicy mogą go rozszerzać, a ich kod jest kompilowany bajtowo i ładowany do JVM tak, jakby był częścią projektu. jak gdyby był częścią projektu - kolejna tajna broń, którą może dać Clojure. ci. Zachęcam każdego, kto pracuje w Javie, aby poważnie przyjrzał się Clojure i zobaczył jak może on zmienić twoje doświadczenie życia w maszynie JVM.

Produkcja to najszczęśliwsze miejsce na ziemi

Produkcja to moje pierwsze ulubione miejsce w internecie. Uwielbiam produkcję. Powinniście kochać produkcję. Idź tak wcześnie i jak najczęściej. Przyprowadź dzieci. Przyprowadź rodzinę. Pogoda jest niesamowita. To najszczęśliwsze miejsce na Ziemi. To lepsze niż Disneyland! Dotarcie tam nie zawsze jest łatwe, ale zaufaj mi: kiedy już tam dotrzesz, jedziesz, chcę zostać. To jak Mauritius. Pokochasz to! Oto kilka wskazówek, które sprawią, że Twoja podróż będzie tak przyjemna, jak to tylko możliwe:

Jedź autostradą z ciągłymi dostawami : Nie ma szybszego sposobu na produkcję. Ciągłe dostarczanie pozwala szybko i konsekwentnie przejść od najnowszego zobowiązania Git do produkcji. W ciągłym potoku dostarczania kod przechodzi automatycznie od programisty do wdrożenia, a każdy krok pomiędzy nimi jest wykonywany jednym płynnym ruchem. Narzędzia do ciągłej integracji, takie jak Travis CI lub Jenkins, pomagają, ale staraj się wydobywać informacje zebrane podczas produkcji. Wydania Canary to technika zmniejszająca ryzyko wprowadzenia nowej wersji oprogramowania do produkcji poprzez powolne wprowadzanie zmiany do małego przekroju użytkowników. Narzędzia do ciągłego dostarczania, takie jak Netflix Spinnaker może zautomatyzować tego rodzaju dopracowaną strategię wdrażania.
Produkcja może być zaskakująca. : Być przygotowanym! Usługi zawiodą. Nie zostawiaj swoich klientów na lodzie. Określ agresywne limity czasu po stronie klienta. Umowy dotyczące poziomu usług (SLA) dominują w wielu dyskusjach technicznych. Użyj zabezpieczenia usługi - wzorca, w którym uruchamianych jest wiele wywołań idempotentnych do identycznie skonfigurowanych wystąpień usług w węzłach dyskretnych i odrzucane są wszystkie odpowiedzi oprócz najszybszej - aby spełnić umowy SLA. Awarie będą się zdarzać. Użyj wyłączników automatycznych, aby wyraźnie zdefiniować tryby awarii i izolować awarie. Spring Cloud ma abstrakcję, Spring Cloud Circuit Breaker, która obsługuje obwody reaktywne i niereaktywne.
W produkcji nikt nie słyszy krzyku Twojej aplikacji. : Obejmij obserwowalność od samego początku. Produkcja to ruchliwe miejsce! Jeśli wszystko pójdzie dobrze, będziesz mieć więcej użytkowników i popytu, niż będziesz wiedział, co zrobić. Wraz ze wzrostem popytu skaluj. Infrastruktura chmurowa, taka jak Cloud Foundry, Heroku i Kubernetes, od dawna obsługuje skalowanie poziome poprzez obsługę zestawu węzłów z systemem równoważenia obciążenia. Jest to szczególnie łatwe, jeśli tworzysz bezstanowe mikrousługi w stylu 12-czynnikowym. Ta strategia działa nawet wtedy, gdy Twoja aplikacja monopolizuje cenne zasoby, takie jak wątki.
Twój kod nie powinien monopolizować wątków. : Wątki są bardzo drogie. Najlepsze rozwiązania tego problemu - wielowątkowość kooperacyjna - polegają na sygnalizowaniu środowisku wykonawczemu, kiedy może on przenieść pracę do skończonego zestawu rzeczywistych wątków systemu operacyjnego i poza nim. Dowiedz się o takich rzeczach, jak programowanie reaktywne obsługiwane przez Project Reactor (dość powszechne po stronie serwera) oraz Spring Webflux i RxJava (dość powszechne na Androidzie). Jeśli rozumiesz, jak działa programowanie reaktywne, naturalnym kolejnym krokiem jest przyjęcie rzeczy takich jak współprogramy Kotlina. Wielowątkowość kooperacyjna pozwala pomnożyć liczbę obsługiwanych użytkowników lub podzielić koszty infrastruktury.
Kluczem do sukcesu jest autonomia. : Mikrousługi umożliwiają małym, skupionym na jednym zespołom, zdolnym do samodzielnego wydawania oprogramowania.
Dziewięćdziesiąt procent twojej aplikacji jest przyziemne. : Wykorzystaj frameworki, takie jak Spring Boot, aby umożliwić Ci skupienie się na końcowych rezultatach produkcyjnych, a nie na wspieraniu kodu. Czy język programowania Java nie jest twoją filiżanką herbaty - ee - kawy? Ekosystem JVM jest bogaty w produktywne alternatywy, takie jak Kotlin.
Usuń tarcia związane z przejściem na produkcję. Unikaj tego, co Amazon CTO Erner Vogels nazywa "niezróżnicowanym podnoszeniem ciężarów". Utoruj sobie drogę do produkcji, a ludzie będą chcieli iść wcześnie i często. Będą tęsknić za tym, co Antoine de Saint-Exupéry nazwano "rozległymi i niekończącymi się morzami".

Program z PG

Więc piszesz testy jednostkowe? Świetny! Czy są dobre? Zapożyczając termin od Alistaira Cockburna, czy masz GUT? Dobre testy jednostkowe? A może wylądowałeś w swojej bazie testowej kogoś (przyszłego Ciebie?) z narastającym odsetkami długiem technicznym? Co rozumiem przez dobre? Dobre pytanie. Trudne pytanie. Warte odpowiedzi. Zacznijmy od imion. Zastanów się, co jest testowane w nazwie. Tak, nie chcesz, aby test1, test2 i test3 jako schemat nazewnictwa. W rzeczywistości nie chcesz testować w nazwach testów: @Test już to robi. Powiedz czytelnikowi, co testujesz, a nie, że testujesz. Ach, cóż, nie mam na myśli nazwy testowanej metody: powiedz czytelnikowi, jakie zachowanie, właściwość, możliwości itp. jest testowany. Jeśli masz metodę addItem, nie potrzebujesz odpowiedniego testu addItemIsOK. To powszechny zapach testowy. Zidentyfikuj przypadki zachowania i przetestuj tylko jeden przypadek na przypadek testowy. Aha, i nie, to nie oznacza addItemSuccess i addItemFailure. Pozwól, że zapytam, jaki jest cel twojego testu? Aby przetestować, że "to działa"? To tylko połowa historii. Największym wyzwaniem w kodzie nie jest określenie, czy "to działa", ale określenie, co oznacza "to działa". Masz szansę uchwycić to znaczenie, więc spróbuj addOfItemWithUniqueKeyIsRetained i addOfItemWithExistingKeyFails. Ponieważ te nazwy są długie, a także nie są kodem produkcyjnym, rozważ użycie podkreśleń, aby poprawić czytelność - wielkość liter w wielbłądzie nie jest skalowana - więc Addition_of_item_with_unique_key_is_retained. W JUnit 5 możesz użyć DisplayNameGenerator.ReplaceUnderscores z @DisplayName Generation, aby ładnie wydrukować jako "Zachowywane jest dodanie elementu z unikalnym kluczem". Możesz zobaczyć, że nazewnictwo jako propozycja ma przyjemną właściwość: jeśli test przejdzie pomyślnie, masz pewność, że propozycja może być prawdziwa; jeśli się nie powiedzie, propozycja jest fałszywa. Co jest dobrym punktem. Zaliczenie testów nie gwarantuje, że kod działa. Aby jednak test jednostkowy był dobry, znaczenie niepowodzenia powinno być jasne: powinno to oznaczać, że kod nie działa. Jak powiedział Dijkstra: "Testowanie programów może być używane do pokazywania obecności błędów, ale nigdy do pokazywania ich braku!" W praktyce oznacza to, że test jednostkowy nie powinien zależeć od rzeczy, których nie można kontrolować w ramach testu. System plików? sieć? Baza danych? Zamawianie asynchroniczne? Możesz mieć wpływ, ale nie kontrolować. Testowana jednostka nie powinna zależeć od rzeczy, które mogą spowodować awarię, gdy kod jest poprawny. Uważaj też na testy przepełnienia. Znasz te: kruche twierdzenia dotyczące szczegółów implementacji, a nie wymaganych funkcji. Aktualizujesz coś - pisownię, wartość magiczną, wynik jakości - i testy kończą się niepowodzeniem. Nie udają się, ponieważ testy zawiodły, a nie kod produkcyjny. Aha, i miej oczy otwarte również na testy niedopasowania. Są niejasne, przechodzą na pierwszy rzut oka, nawet z kodem, który jest szalenie i oczywiście błędny. Pomyślnie dodałeś swój pierwszy przedmiot. Nie tylko sprawdzaj, czy liczba przedmiotów jest większa od zera. Jest tylko jeden właściwy wynik: jeden przedmiot. Wiele liczb całkowitych jest większych od zera; miliardy się mylą. Mówiąc o wyniku, możesz zauważyć, że wiele testów odbywa się po prostej, trzyaktowej grze: zaaranżuj-działaj-zatwierdź, czyli dane-kiedy-to. Mając to na uwadze, możesz skupić się na historii, którą próbuje opowiedzieć test. Utrzymuje spójność, sugeruje inne testy i pomaga w nazwie. Aha, a kiedy wracamy do imion, może się okazać, że nazwy się powtarzają. Wyklucz powtórzenie. Użyj go, aby pogrupować testy w klasy wewnętrzne za pomocą @Nested. Możesz więc zagnieździć with_unique_key_is_retained i with_existing_key_fails wewnątrz Addition_of_item.

Czytaj codziennie OpenJDK

OpenJDK składa się z milionów linii kodu Java. Prawie każda klasa narusza pewne wytyczne dotyczące "czystego kodu". W prawdziwym świecie panuje bałagan. Nie ma czegoś takiego jak "czysty kod" i będziemy mieli trudności z określeniem, co to jest. Doświadczeni programiści Java potrafią czytać kod zgodny z różnymi stylami. OpenJDK ma ponad tysiąc autorów. Nawet jeśli istnieje pewna spójność w formatowaniu, kodują one na różne sposoby. Rozważmy na przykład metodę Vector.writeObject:

private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
final java.io.ObjectOutputStream.PutField fields = s.putFields();
final Object[] data;
synchronized (this) {
fields.put("capacityIncrement", capacityIncrement);
fields.put("elementCount", elementCount);
data = elementData.clone();
}
fields.put("elementData", data);
s.writeFields();
}

Dlaczego programista oznaczył pola i dane zmiennych lokalnych jako ostateczne? Nie ma powodu, dla którego było to konieczne. To jest styl kodowania decyzji. Dobrzy programiści potrafią równie dobrze czytać kod, niezależnie od tego, czy zmienne lokalne są ostateczne, czy nie. I tak im to nie przeszkadza. Dlaczego pola fields.put("elementData", data) znajdują się poza obszarem zsynchronizowanego bloku? Mogło to być spowodowane przedwczesną optymalizacją, która miała na celu zmniejszenie sekcji kodu szeregowego. A może programista był nieostrożny? Łatwo jest chcieć zoptymalizować wszystko, co widzimy, ale musimy oprzeć się tej potrzebie. Oto kolejna metoda z Spliteratora wewnątrz ArrayList:

public Spliterator trySplit() {
int hi = getFence(), lo = index, mid = (lo + hi) >>> 1;
return (lo >= mid) ? null : // divide range in half unless too small
new RandomAccessSpliterator<>(this, lo, index = mid);
}

Ta metoda z pewnością wywołałaby różnego rodzaju dzwonki ostrzegawcze "czystego kodu". Zakochani w finale narzekaliby, że hi, lo i mid mogą być finałem. Tak, mogli. Ale nie są. W OpenJDK generalnie nie oznaczają zmiennych lokalnych jako ostatecznych. Dlaczego mamy to niejasne (lo + hi) >>> 1? Czy nie moglibyśmy raczej powiedzieć (lo + hi) / 2? (Odpowiedź: to nie jest dokładnie to samo.) I dlaczego wszystkie trzy zmienne lokalne są zadeklarowane w jednym wierszu? Czy to nie narusza wszystkiego, co dobre i właściwe? Okazuje się, że według badań liczba błędów jest proporcjonalna do linii kodu. Rozłóż swoją metodę tak, jak poprosił cię profesor uniwersytecki, a otrzymasz więcej linijek kodu (LOC). A dzięki większej liczbie LOC, otrzymasz również więcej błędów dla tej samej funkcjonalności. Może się również zdarzyć, że początkujący programiści mają tendencję do rozpowszechniania swojego kodu na wielu stronach. Eksperci piszą zwięzły, zwarty kod. Musimy nauczyć się czytać wiele różnych stylów kodowania, a do tego polecam OpenJDK. Przeczytaj klasy java.util, java.io i wiele innych.

Naprawdę patrzę pod maskę

Java to kompletna platforma i tak należy ją traktować. W mojej karierze programistycznej Java spotkałem setki programistów, którzy dobrze znają składnię języka. Rozumieją lambdy i strumienie oraz znają każde API od String do nio z góry. Ale zrozumienie następujących rzeczy uczyniłoby ich bardziej kompletnymi profesjonalistami:

Algorytmy zbierania śmieci: JVM GC znacznie się poprawiła od czasu jego pierwszych wersji. Ergonomia JVM pozwala na automatyczne dostosowanie się do optymalnych parametrów dla wykrytego środowiska. Dobre zrozumienie tego, co się dzieje, może czasami jeszcze bardziej poprawić wydajność JVM.
Profilery JVM: strojenie JVM nie jest grą w zgadywanie. Przed wprowadzeniem jakichkolwiek zmian należy zrozumieć, jak zachowuje się aplikacja. Wiedza o tym, jak łączyć się i interpretować dane profilera, pomoże dostroić JVM pod kątem lepszej wydajności, znaleźć przecieki pamięci lub zrozumieć, dlaczego metoda trwa tak długo, aby wykonać.
Aplikacje natywne dla chmury jasno pokazują, że kod może być wykonywany na wielu komputerach w sieci w różnych systemach operacyjnych. Znajomość poniższych informacji może pomóc profesjonalistom Java w opracowaniu odpornej i przenośnej aplikacji
Kodowanie znaków: Różne systemy operacyjne mogą pracować z różnymi kodowaniami znaków. Zrozumienie, czym one są i jak je skonfigurować, może uniemożliwić aplikacji wyświetlanie dziwnych znaków.
Sieć TCP/IP: aplikacje natywne dla chmury to systemy rozproszone. W świecie chmury, Internetu i sieci zrozumienie sposobu routingu tabel, opóźnień, zapór i wszystkiego, co jest związane z siecią TCP/IP, jest ważne, zwłaszcza gdy coś nie działa zgodnie z oczekiwaniami.
Protokół HTTP: w świecie, w którym przeglądarka jest klientem, zrozumienie sposobu działania HTTP 1.1 i 2.0 może pomóc w lepszym zaprojektowaniu aplikacji. Wiedza o tym, co się dzieje, gdy przechowujesz dane w sesji HTTP, zwłaszcza w środowisku wieloklastrowym, może być bardzo pomocnym.

Warto nawet wiedzieć, co robią frameworki pod maską. Tutaj możemy wziąć przykładowe frameworki mapowania relacyjnego obiektów (ORM), takie jak JPA i Hibernate .Włącz dane wyjściowe SQL podczas programowania : po włączeniu danych wyjściowych SQL możesz zobaczyć, jakie polecenia są wysyłane do bazy danych, zanim stwierdzisz, że dziwne wywołanie SQL zachowuje się źle.

Rozmiar pobierania zapytania : większość implementacji JPA/hibernacji ma domyślny rozmiar pobierania równy jeden (1). Oznacza to, że jeśli zapytanie przyniesie 1000 jednostek z bazy danych, zostanie wykonanych 1000 poleceń SQL w celu wypełnienia tych jednostek. Możesz dostosować rozmiar pobierania, aby zmniejszyć liczbę wykonywanych instrukcji SQL. Możesz zidentyfikować ten problem, włączając dane wyjściowe SQL.
Relacje jeden-do-wielu i wiele-do-jednego : chociaż relacje jeden-do-wielu są domyślnie ładowane z opóźnieniem, niektórzy programiści popełniają błąd, zmieniając relację, aby chętnie załadować encje lub ręcznie zainicjować je przed zwróceniem kolekcji encji . Zachowaj ostrożność, ponieważ każda entuzjastycznie ładowana jednostka może również utworzyć relację wiele do jednego, powodując pobranie prawie każdej tabeli/jednostki z bazy danych. Włączenie danych wyjściowych SQL może również pomóc w zidentyfikowaniu tego problemu.

Krótko mówiąc, nie daj się kontrolować - miej kontrolę!

Odrodzenie Jawy

Wydaje się, że Java została uznana za martwą bardziej niż jakikolwiek inny język programowania. Być może nie jest zaskoczeniem, że doniesienia o jego śmierci są mocno przesadzone. Java ma ogromny wpływ na rozwój zaplecza, a większość przedsiębiorstw tworzy systemy w Javie. Jednak w każdej plotce jest ziarno prawdy - Java była powolnym językiem w dobie dynamicznych języków, takich jak Ruby i JavaScript. Tradycyjnie główne wydania Javy obejmowały od trzech do czterech lat rozwoju. W tym tempie ciężko nadążyć za innymi platformami. W 2017 roku wszystko się zmieniło. Oracle - zarządca Javy - zapowiedział, że platforma Java będzie wypuszczana dwa razy w roku. Java 9, wydana pod koniec 2017 roku, była ostatnim dużym i długo oczekiwanym wydaniem. Po Javie 9, co roku w marcu i wrześniu pojawia się nowe główne wydanie Javy. Jak w zegarku. Przejście na ten harmonogram wydań oparty na czasie ma wiele konsekwencji. Wydania nie mogą już czekać na funkcje, które nie są jeszcze ukończone. Ponadto, ponieważ między wydaniami jest mniej czasu, a zespół opracowujący Javę pozostaje ten sam rozmiar, mniej funkcji trafia do wydania. Ale to jest w porządku - mamy kolejne wydawnictwo już za sześć miesięcy. Możemy liczyć na stały dopływ nowych funkcji i ulepszeń. Co ciekawe, nowe funkcje językowe są teraz również dostarczane stopniowo. Język Java ewoluuje teraz w bardziej zwinny sposób. Na przykład Java 12 dostarczała Switch Expressions jako funkcję języka podglądu, z wyraźnym zamiarem późniejszego rozszerzenia tej funkcji o obsługę pełnego dopasowania wzorców. Jednym z powodów, dla których wydania Javy zajęły tyle czasu i wysiłku, jest to, że platforma stała się nieco skostniała w ciągu ponad 20 lat istnienia. W Javie 9 platforma jest w pełni zmodularyzowana. Każda część platformy jest teraz umieszczana w osobnym module, z wyraźnymi zależnościami od innych części. System modułów wprowadzony w Javie 9 zapewnia przestrzeganie tej architektury platformy od teraz. Wewnętrzne elementy platformy są teraz bezpiecznie zamknięte w modułach, zapobiegając (nad)używaniu przez aplikację i kod biblioteki. Wcześniej wiele aplikacji i bibliotek było zależnych od tych elementów wewnętrznych platformy, co utrudniało ewolucję Javy bez łamania dużej ilości istniejącego kodu. Możliwe jest również wykorzystanie systemu modułowego do własnych aplikacji. Może również sprawić, że baza kodu będzie łatwiejsza w utrzymaniu, elastyczna i przyszłościowa. Przejście od długiego i nieprzewidywalnego cyklu wydawniczego do regularnych wydań opartych na kalendarzu jest wielkim osiągnięciem zespołu Java. Przystosowanie się do tej nowej rzeczywistości zdecydowanie zajęło nam, jako społeczności programistów, czas. Na szczęście zmiany w Javie są teraz mniejsze i bardziej przyrostowe. Te częstsze i regularne wydania są łatwiejsze do przyjęcia i dostosowania. W przypadku wolniejszych zmian wersja Java jest oznaczana jako Long-Term Supported (LTS) co sześć wydań, zaczynając od Java 11. Oznacza to, że możesz przechodzić między wydaniami LTS co trzy lata, jeśli chcesz. Ważne jest, aby zrozumieć, że zobowiązanie LTS jest oferowane przez dostawców takich jak Oracle, Red Hat, a nawet Amazon i niekoniecznie jest bezpłatne. W każdym razie, niezależny od dostawcy projekt OpenJDK wciąż tworzy wspierane kompilacje dla najnowszej rozwijanej wersji Java. Jednak wiele rzeczy może i zmieni się w wydaniach między wydaniami LTS. Jeśli możesz, wskocz do pociągu z częstymi wydaniami i ciesz się stałym strumieniem lepszej Javy. To nie jest tak przerażające, jak mogłoby się wydawać.

Refaktoryzacja wartości logicznych do wyliczeń

Nie używałbyś "magicznych liczb" w swoim kodzie, więc nie używaj magii Boole′a też! Literały logiczne są gorsze niż liczby zakodowane na sztywno: 42 w kodzie może wyglądać znajomo, ale fałsz może być wszystkim i wszystko może być fałszem. Kiedy obie zmienne są prawdziwe, nie wiesz, czy to przypadek, czy obie są "prawdziwe" z tego samego powodu i powinny się zmieniać razem. To sprawia, że kod jest trudniejszy do odczytania i powoduje błędy, gdy czytasz go źle. Podobnie jak w przypadku liczb magicznych, powinieneś dokonać refaktoryzacji do nazwanych stałych. Refaktoryzacja 42 do stałej ASCII_ASTERISK lub EVERYTHING poprawia czytelność kodu, podobnie jak na przykład refaktoryzacja względem stałej logicznej o nazwie AVAILABLE w klasie Product. Jednak prawdopodobnie nie powinieneś mieć żadnych pól logicznych w swoim modelu domeny: niektóre wartości logiczne nie są tak naprawdę logiczne. Załóżmy, że encja Produkt ma dostępne pole logiczne, aby wskazać, czy produkt jest obecnie sprzedawany. To nie jest tak naprawdę wartość logiczna: jest to opcjonalna wartość "dostępna", co nie jest tym samym, ponieważ "niedostępny" tak naprawdę oznacza coś innego, na przykład "brak w magazynie". Gdy typ ma dwie możliwe wartości, jest to zbieg okoliczności i może się zmienić - np. przez dodanie opcji "przerwane". Istniejące pole logiczne nie może pomieścić dodatkowej wartości. Uwaga: używanie null do oznaczania czegoś jest najgorszym możliwym sposobem na zaimplementowanie trzeciej wartości. W końcu będziesz potrzebować komentarza do kodu, takiego jak "prawda, gdy produkt jest dostępny, fałszywy, gdy brak w magazynie, null, gdy wycofany". Proszę, nie rób tego. Najbardziej oczywistym modelem produktów, których już nie sprzedajesz, jest wycofane pole logiczne, oprócz pola dostępnego. To działa, ale jest trudniejsze do utrzymania, ponieważ nic nie wskazuje na to, że te pola są ze sobą powiązane. Na szczęście Java ma sposób na grupowanie nazwanych stałych. Refaktoryzuj powiązane pola logiczne, takie jak te, do typu wyliczenia Java:

enum ProductAvailability {
AVAILABLE, OUT_OF_STOCK, DISCONTINUED, BANNED
}

Typy wyliczeń są świetne, ponieważ wtedy dostajesz więcej rzeczy do nazwania. Ponadto wartości są bardziej czytelne niż prawda, co oznacza, że wartość jest w rzeczywistości inną wartością, np. DOSTĘPNY. Typy wyliczeń również okazują się wygodniejsze niż można by się spodziewać, co sprawia, że lenistwo jest słabą wymówką dla nie refaktoryzacji. Typ wyliczenia może nadal zawierać wygodne metody logiczne, które możesz chcieć, jeśli oryginalny kod zawierał wiele warunkowych kontroli dostępnych produktów. W rzeczywistości typy wyliczeniowe idą dalej niż zwykłe grupowanie stałych za pomocą pól, konstruktorów i metod. Mniej oczywistą, ale ważniejszą korzyścią jest to, że masz teraz miejsce docelowe dla innych refaktoryzacji, które przenoszą logikę związaną z dostępnością do typu ProductAvailability. Serializacja typu enum jest bardziej pracochłonna, niż np. użycie JSON lub bazy danych. Jednak to mniej niż można by się spodziewać. Prawdopodobnie już używasz biblioteki, która dobrze sobie z tym radzi i pozwala wybrać sposób serializacji do reprezentacji obiektu o pojedynczej wartości. Modele domen często cierpią z powodu prymitywnej obsesji - nadmiernego używania prymitywnych typów Java. Refaktoryzacja liczb i dat do klas domen pozwala, aby Twój kod stał się bardziej wyrazisty i czytelny, a nowe typy zapewniają lepszą bazę dla powiązanego kodu, takiego jak walidacje i porównania. W języku problematycznej domeny typy logiczne są fałszywe, a typy wyliczane są prawdziwe.

Refaktoryzacja w kierunku szybkiego czytania

Przypadkowy czytelnik zwykle osiąga 150-200 wpm (słów na minutę) z dobrym współczynnikiem rozumienia. Osoby, które lubią szybkie czytanie, mogą z łatwością osiągnąć nawet 700 wpm. Ale nie martw się, nie musimy ustanawiać nowego rekordu świata w szybkości czytania, aby nauczyć się podstawowych pojęć i zastosować je w naszym kodzie. Przyjrzymy się trzem obszarom, które są szczególnie pomocne, jeśli chodzi o czytanie kodu: skimming, metaguiding i fiksacja wizualna. Więc co sprawia, że szybkie czytanie jest tak szybkie? Jednym z pierwszych kroków jest stłumienie subwokalizacji. Subwokalizacja? Dokładnie. Ten głos w twojej głowie, który właśnie próbował poprawnie wyartykułować to słowo. I tak, teraz jesteś świadomy tego głosu. Ale nie martw się, wkrótce zniknie! Subwokalizacja może być nieuczona i jest niezbędnym pierwszym krokiem do znacznej poprawy szybkości czytania. Przyjrzyjmy się tej metodzie z trzema parametrami, z których wszystkie wymagają walidacji. Jednym ze sposobów odczytania kodu jest śledzenie, gdzie i w jaki sposób używane są parametry wejściowe:

public void printReport(Header header, Body body, Footer footer) {
checkNotNull(header, "header must not be null");
validate(body);
checkNotNull(footer, "footer must not be null");
}

Po zlokalizowaniu nagłówka musimy znaleźć kolejny parametr, body, który wymaga spojrzenia w dół i w lewo. Możemy zacząć od prostej refaktoryzacji, aby wyrównać pierwszą i trzecią kontrolę, aby przerwać przepływ poziomy tylko raz:

public void printReport(Header header, Body body, Footer footer) {
checkNotNull(header, "header must not be null");
checkNotNull(footer, "footer must not be null");
validate(body);
}

Alternatywnie, biorąc pod uwagę, że sprawdzanie wartości null jest również walidacją parametru, możemy wyodrębnić wywołania metody checkNotNull do ich własnych odpowiednio nazwanych metod, aby pomóc czytelnikowi. To, czy są to te same, czy przeciążone wersje metody, zależy od dostępnego kodu:

public void printReport(Header header, Body body, Footer footer) {
validateReportElement(header);
validateReportElement(body);
validateReportElement(footer);
}

Metaprzewodnik to kolejna technika szybkiego czytania. Zamiast próbować czytać słowo po słowie w książce, starasz się uchwycić całą linijkę na raz. Dzieci zwykle robią to za pomocą palca, aby śledzić słowo, które czytają. Korzystanie z jakiegoś rodzaju wskazówek pomaga nam iść naprzód i unikać cofania się o słowo lub dwa. Co zabawne, sam kod może działać jako takie urządzenie, ponieważ ma wrodzoną strukturę, którą możemy wykorzystać, aby kierować naszym okiem:

List< String > items = new ArrayList< >(zeros);
items.add("one");
items.add("two");
items.add("three");

Ile pozycji znajduje się na liście? Raz Dwa Trzy! Właściwie to cztery. Może więcej. Ups, przegapiłeś też argument z zerami? Struktura, która powinna nam pomóc, faktycznie staje nam na drodze. Chociaż pozwoliliśmy naszemu czytelnikowi kierować się wyrównaniem metod add, całkowicie pomyliliśmy wzrok i przegapiliśmy argument konstruktora. Przepisanie tego pozwala czytelnikowi na łatwe śledzenie przewodnika bez utraty ważnych informacji:

List< String > items = new ArrayList<>();
items.addAll(zeros);
items.add("one");
items.add("two");
items.add("three");

Następnym razem, gdy napiszesz fragment kodu, sprawdź, czy potrafisz go szybko przeczytać. Pamiętaj o podstawach wizualnej fiksacji i metaguidingu. Postaraj się znaleźć strukturę, która ma logiczny sens, jednocześnie prowadząc oko do odpowiednich informacji. Nie tylko pomoże ci szybciej czytać kod w przyszłości, ale także pomoże ci utrzymać płynność.

Proste obiekty wartości

Klasy, które reprezentują obiekty wartości, nie potrzebują metod pobierających ani ustawiających. Programiści Java są zwykle uczeni używania getterów do uzyskiwania dostępu do pól, na przykład:

public class Coordinate {
private Latitude latitude;
private Longitude longitude;
public Coordinate(Latitude latitude, Longitude longitude) {
this.latitude = latitude;
this.longitude = longitude;
}
/**
* @return the latitude of the Coordinate
*/
public Latitude getLatitude() {
return latitude;
}
/**
* @return the longitude of the Coordinate
*/
public Longitude getLongitude() {
return longitude;
}
}
System.out.println(thing.getLatitude());

Chodzi o to, że gettery hermetyzują sposób, w jaki wartości są reprezentowane w obiekcie, zapewniając spójne podejście w całej bazie kodu. Pozwala również na ochronę przed aliasowaniem, na przykład poprzez klonowanie kolekcji przed ich zwróceniem. Styl ma swoje korzenie w początkach JavaBeans, kiedy było duże zainteresowanie narzędziami graficznymi wykorzystującymi refleksję. Mógł być również pewien wpływ Smalltalk (klasyczny język zorientowany obiektowo), w którym wszystkie pola są prywatne, chyba że są udostępniane przez akcesor; Pola tylko do odczytu mają gettery, ale nie mają seterów. W praktyce nie wszystkie klasy pełnią tę samą rolę i z powodu braku alternatywnej struktury w języku wielu programistów pisze klasy Java, które w rzeczywistości są obiektami wartości: prostym zbiorem pól, które nigdy się nie zmieniają, gdzie równość jest oparte na wartości, a nie tożsamości. W naszym przykładzie dwie współrzędne obiekty o tej samej szerokości i długości geograficznej są praktycznie takie same. Mogę używać wystąpień Coordinate jako stałych w całym kodzie, ponieważ są niezmienne. Kilka lat temu, podobnie jak wielu moich kolegów, zacząłem męczyć się powtarzaniem schematów, których wymagają gettery, i uprościłem swój styl Obiektów Wartości. Upubliczniłem wszystkie pola, na przykład strukturę C:

public class Coordinate {
public final Latitude latitude;
public final Longitude longitude;
public Coordinate(Latitude latitude, Longitude longitude) {
this.latitude = latitude;
this.longitude = longitude;
}
}
System.out.println(coordinate.latitude);

Mogę to zrobić, ponieważ obiekt jest niezmienny (znowu trzeba uważać na aliasing, jeśli którakolwiek z wartości jest ustrukturyzowana) i staram się unikać dziedziczenia lub implementowania wielu zachowań. Stanowi to zmianę podejścia z wcześniejszych dni Javy. Na przykład java.awt.Point jest zmienny, a metoda move aktualizuje swoje pola x i y w miejscu. Obecnie, po dwudziestu latach ulepszeń w JVM i szerszym zastosowaniu programowania funkcyjnego, takie przejściowe obiekty są na tyle tanie, że spodziewalibyśmy się przenieść, aby zwrócić nową niezmienną kopię w nowej lokalizacji. Przykładem dla naszego Koordynatora będzie:

public class Coordinate {
public Coordinate atLatitude(Latitude latitude) {
return new Coordinate(latitude, this.longitude);
}
}

Odkryłem, że uproszczone obiekty wartości są użyteczną konwencją wyjaśniającą rolę typu, z mniej rozpraszającym szumem w kodzie. Są łatwe do refaktoryzacji i często stanowią użyteczny cel do gromadzenia metod, które lepiej wyrażają domenę kodu. Czasami cechy behawioralne obiektu wartości stają się bardziej znaczące i stwierdzam, że mogę wyrazić to, czego potrzebuję, za pomocą metod i uczynić pola prywatnymi. Okazuje się również, że zespół zajmujący się językiem Java również to zauważył i wprowadził strukturę rekordów w Javie 14. Dopóki nie będzie to powszechne, będziemy musieli polegać na konwencji.

Zadbaj o swoje deklaracje modułów

Jeśli tworzysz moduły Java, deklaracje modułów (pliki module-info.java) są z łatwością najważniejszymi plikami źródłowymi. Każdy z nich reprezentuje cały JAR i reguluje jego interakcję z innymi JAR-ami, więc dbaj o swoje deklaracje! Oto kilka rzeczy, na które należy zwrócić uwagę.

Utrzymuj deklaracje modułów w czystości

Deklaracje modułów są kodem i powinny być traktowane jako takie, więc upewnij się, że zastosowano Twój styl kodu. Poza tym, zamiast umieszczać dyrektywy losowo, ustrukturyzuj deklaracje modułów. Oto kolejność, której używa JDK:

1. Wymaga, w tym statyczne i przechodnie
2. Eksport
3. Eksportuj to
4. Otwiera
5. Otwórz to
6. Zastosowania
7. Postanowienia

Cokolwiek zdecydujesz, jeśli masz dokument definiujący Twój styl kodu, zapisz tam decyzję. Jeśli masz swoje IDE, narzędzie do budowania lub analizator kodu, sprawdź takie rzeczy jeszcze lepiej. Spróbuj przyspieszyć, aby automatycznie sprawdził, a nawet zastosował wybrany przez Ciebie styl.

Deklaracje modułu komentarzy

Opinie na temat dokumentacji kodu, takiej jak Javadoc lub komentarze inline, bardzo się różnią, ale niezależnie od stanowiska zespołu w sprawie komentarzy, rozszerz je na deklaracje modułów. Jeśli chcesz, aby abstrakcje zawierały jedno lub dwa zdania wyjaśniające ich znaczenie i wagę, dodaj taki komentarz Javadoc do każdego modułu. Nawet jeśli to nie w twoim stylu, większość ludzi zgadza się, że dobrze jest udokumentować, dlaczego podjęto konkretną decyzję. W deklaracji modułu może to oznaczać dodanie komentarza wbudowanego do:

•  Opcjonalna zależność wyjaśniająca, dlaczego moduł może być nieobecny
•  Kwalifikowany eksport wyjaśniający, dlaczego nie jest to publiczny interfejs API, ale jest częściowo dostępny
•  Otwarty pakiet wyjaśniający, które frameworki mają uzyskać dostęp do niego

Deklaracje modułów dają nowe możliwości: nigdy wcześniej dokumentowanie powiązań artefaktów projektu w kodzie nie było tak łatwe.

Deklaracje modułu przeglądu

Deklaracje modułów są centralną reprezentacją twojej struktury modułowej, a ich badanie powinno być integralną częścią każdego rodzaju przeglądu kodu, który robisz. Niezależnie od tego, czy przeglądasz swoje zmiany przed zatwierdzeniem, czy przed otwarciem pull requesta, zakończeniem sesji programowania w parach, czy podczas formalnego przeglądu kodu, za każdym razem, gdy sprawdzasz kod, zwróć szczególną uwagę na module-info.java:

•  Czy konieczne są nowe zależności modułów (rozważ wymianę z usługami) i zgodne z architekturą projektu?
•  Czy kod jest przygotowany do obsługi braku opcjonalnych zależności?
•  Czy konieczne jest eksportowanie nowych pakietów? Czy wszystkie klasy publiczne są gotowe do użycia? Czy możesz zmniejszyć powierzchnię API?
•  Czy ma sens, że eksport się kwalifikuje, czy jest to wykręt abyuzyskać dostęp do interfejsu API, który nie jest gotowy do upublicznienia?
•  Czy dokonano zmian, które mogą powodować problemy dla dalszych odbiorców, którzy nie są częścią procesu kompilacji?

Zainwestowanie czasu w rzetelne przeglądanie deskryptorów modułów może wydawać się marnotrawstwem, ale widzę to jako szansę: nigdy wcześniej nie było tak łatwo analizować i przeglądać relacje między artefaktami projektu i jego strukturą. I nie sfotografowany szkic tablicy, który został przesłany na twoją wiki kilka lat temu; cóż, prawdziwa sprawa, rzeczywiste relacje między twoimi artefaktami. Deklaracje modułów pokazują nagą rzeczywistość zamiast przestarzałych dobrych intencji.

Dbaj o swoje zależności

Współczesne programowanie Java jest w dużym stopniu uzależnione od bibliotek innych firm. Korzystając z Maven lub Gradle, mamy proste mechanizmy importowania i używania opublikowanych pakietów. Ponieważ programiści nie chcą tworzyć i utrzymywać funkcjonalności standardowych, ale raczej skupiać się na określonej logice biznesowej, korzystanie z frameworków i bibliotek może być mądrym wyborem. Patrząc na przeciętny projekt, ilość twojego kodu może wynosić zaledwie 1%, a resztę będą importowane biblioteki i frameworki. Wiele kodu, który jest wprowadzany do produkcji, po prostu nie należy do nas, ale w dużym stopniu od niego zależymy. Kiedy patrzymy na nasz kod i sposób, w jaki traktujemy wkład członków zespołu, często zwracamy się do procesów, takich jak przeglądy kodu, zanim połączymy nowy kod z naszym głównym oddziałem, jako pierwszy środek zapewnienia jakości. Alternatywnie, ten proces kontroli jakości może być również objęty ćwiczeniem programowania w parach. Jednak sposób, w jaki traktujemy nasze zależności, bardzo różni się od tego, jak traktujemy nasz własny kod. Zależności są często używane po prostu bez jakiejkolwiek formy walidacji. Co ważne, zależności najwyższego poziomu w wielu przypadkach z kolei pociągają za sobą zależności przechodnie, które mogą sięgać wielu poziomów. Na przykład 200-wierszowa aplikacja Spring z 5 bezpośrednimi zależnościami może w sumie użyć 60 zależności, co daje prawie pół miliona linii kodu dostarczanego do produkcji. Używając tylko tych zależności, ślepo ufamy kodowi innych ludzi, co jest dziwne w porównaniu z tym, jak radzimy sobie z własnym kodem.

Zależności podatne

Z punktu widzenia bezpieczeństwa powinieneś przeskanować swoje zależności w poszukiwaniu znanych luk w zabezpieczeniach. Jeśli luka w zależności zostanie znaleziona i ujawniona, powinieneś być tego świadomy i wymienić lub zaktualizować te zależności. Używanie przestarzałych zależności ze znanymi lukami może być katastrofalne, jeśli spojrzysz na niektóre przykłady z przeszłości. Skanując swoje zależności na każdym etapie procesu programowania, możesz zapobiec niespodziance zagrożonej zależności przed wysłaniem kodu do środowiska produkcyjnego. Powinieneś również skanować migawkę produkcyjną, ponieważ nowe luki mogą zostać ujawnione, gdy już używasz jej w swoim środowisku produkcyjnym.

Aktualizowanie zależności

Powinieneś mądrze wybierać swoje zależności. Sprawdź, jak dobrze utrzymywana jest biblioteka lub framework i ilu współpracowników nad nimi pracuje. Zależność od przestarzałych lub źle utrzymanych bibliotek stanowi duże ryzyko. Jeśli chcesz być na bieżąco, możesz użyć swojego menedżera pakietów, który pomoże Ci wykryć, czy są dostępne nowsze wersje. Korzystając z wtyczki wersji Maven lub Gradle, możesz użyć następujących poleceń, aby sprawdzić nowsze wersje:

•  Maven: wersje mvn: aktualizacje zależności wyświetlania
•  Gradle: Gradle dependencyUpdates - Drevision=release

Strategia dla twoich zależności

Podczas obsługi zależności w systemie powinieneś mieć przygotowaną strategię. Pytania o kondycję zależności i powód, dla którego dana zależność jest używana, powinny być jasno sprecyzowane. Następnie powinieneś również dokładnie przemyśleć, jaka powinna być strategia aktualizacji. Aktualizacja jest często ogólnie uważana za mniej bolesną. Wreszcie, ale nie mniej ważne, powinieneś mieć narzędzia, które skanują twoje biblioteki pod kątem znanych luk w zabezpieczeniach, aby zapobiec naruszeniu. W każdym razie powinieneś zadbać o swoje zależności i wybrać odpowiednią bibliotekę z odpowiednią wersją z właściwego powodu.

Poważnie traktuj "separację obaw"

Jeśli studiowałeś informatykę, być może dowiedziałeś się o idei zwanej separacją obaw. Najlepiej opisuje to bajt dźwiękowy "Jedna klasa jedna rzecz, jedna metoda jedna rzecz". Chodzi o to, aby Twoje klasy i metody (oraz funkcje) zawsze skupiały się na jednym wyniku. Zastanów się dokładnie nad obowiązkami swoich zajęć i metod. Czasami prowadzę zajęcia z projektowania sterowanego testami. Używam dodawania ułamków jako prostego kata kodowania do odkrywania TDD. Najczęstszy pierwszy test, który ludzie często piszą, wygląda mniej więcej tak:

attachEquals("2/3", Fractions.addFraction("1/3", "1/3"));

Dla mnie ten test krzyczy "zły projekt". Po pierwsze, gdzie jest ułamek? Istnieje tylko niejawnie, przypuszczalnie wewnątrz funkcji addFraction. Co gorsza, zastanówmy się, co się tutaj dzieje. Jak opisalibyśmy zachowanie funkcji addFraction? Może coś w stylu "Pobiera dwa ciągi, analizuje je i oblicza ich sumę". Gdy tylko zobaczysz lub pomyślisz o słowie "i" w opisie funkcji, metody lub klasy, powinieneś usłyszeć w swojej głowie dzwonki alarmowe. Istnieją dwa problemy: jednym jest parsowanie ciągów, a drugim dodawanie ułamków. Co by było, gdybyśmy zamiast tego napisali nasz test w ten sposób:

Fraction fraction = new Fraction(1, 3);
assertEquals(new Fraction(2,3), fraction.add(new Fraction(1, 3)));

Jak opisalibyśmy metodę add w tym drugim przykładzie? Być może "Zwraca sumę dwóch ułamków". To drugie rozwiązanie jest prostsze do wdrożenia, prostsze do przetestowania, a kod w środku będzie prostszy do zrozumienia. Jest również znacznie bardziej elastyczny, ponieważ jest bardziej modułowy, a przez to bardziej komponowany. Na przykład, gdybyśmy chcieli dodać trzy ułamki zamiast dwóch, jak to zadziała? W pierwszym przykładzie musielibyśmy dodać drugą metodę lub zrefaktoryzować pierwszą, abyśmy mogli wywołać coś takiego:

assertEquals("5/6", Fractions.addFraction("1/3", "1/3", "1/6"));

W drugim przypadku nie są konieczne zmiany kodu:

Fraction fraction1 = new Fraction(1, 3);
Fraction fraction2 = new Fraction(1, 3);
Fraction fraction3 = new Fraction(1, 6);
assertEquals(new Fraction(5,6),
fraction1.add(fraction2).add(fraction3));

Wyobraźmy sobie, że chcieliśmy zacząć od reprezentacji łańcuchowej. Moglibyśmy dodać nową, drugą klasę o nazwie FractionParser lub StringToFraction:

assertEquals(new Fraction(1, 3),
StringFractionTranslator.createFraction("1/3"));

StringFractionTranslator.createFraction konwertuje ciąg reprezentujący ułamek na ułamek. Moglibyśmy sobie wyobrazić inne metody w tej klasie, które pobierają Fraction i renderują String. Teraz możemy dokładniej przetestować ten kod, niezależnie od złożoności dodawania ułamków, mnożenia ich lub czegokolwiek innego. Programowanie sterowane testami jest bardzo pomocne w tym względzie, ponieważ wyraźnie podkreśla problemy słabego oddzielenia obaw. Często jest tak, że jeśli masz trudności z napisaniem testu, jest to wynikiem albo słabego sprzężenia w twoim projekcie, albo słabego rozgraniczenia obaw. Oddzielanie obaw to bardzo skuteczna strategia projektowania, którą można zastosować w dowolnym kodzie. Kod z dobrą separacją problemów jest z definicji bardziej modułowy i zazwyczaj jest znacznie bardziej komponowalny, elastyczny, testowalny i czytelny. Zawsze staraj się, aby każda metoda, klasa i funkcja skupiały się na jednym wyniku. Gdy tylko zauważysz, że twój kod próbuje zrobić dwie rzeczy, wyciągnij nową klasę lub metodę, aby było prostsze i jaśniejsze

Rozmowa techniczna to umiejętność, którą warto rozwijać

Wprowadzę Cię w tajemnicę: nasza branża jest okropna w przeprowadzaniu wywiadów z twórcami. Naprawdę głupie jest to, że prawie nigdy nie siadamy kandydata do pisania rzeczywistego kodu w rzeczywistym środowisku, w którym będzie się rozwijał. To tak, jakby testować muzyka z teorii, ale nigdy nie słuchać, jak gra. Dobrą wiadomością jest to, że rozmowa kwalifikacyjna to umiejętność jak każda inna, co oznacza, że można się jej nauczyć. Podobnie jak w przypadku zdobywania każdej innej umiejętności, możesz badać to, co jest w nią zaangażowane i ćwiczyć, ćwiczyć, ćwiczyć. Jeśli zostaniesz odrzucony podczas rozmowy kwalifikacyjnej, to nie oznacza, że nie jesteś dobrym programistą. To może po prostu oznaczać, że nie jesteś dobry w rozmowach kwalifikacyjnych. To coś, co możesz poprawić, a każda rozmowa kwalifikacyjna to kolejna okazja do zebrania większej ilości danych i przećwiczenia. Ankieterzy często zadają podobne pytania. Oto trzy, które są dość typowe:

Problemy z wielowątkowością : Nadal często pojawia się prośba o sprawdzenie kodu z synchronizacją rozproszoną wokół i znalezienie warunków wyścigu lub impasu. Organizacje z takim kodem mają większe problemy niż zatrudnianie programistów (chociaż jeśli pokażą ten kod w wywiadach, na pewno będą miały problem z zatrudnieniem programistów), więc może i tak nie chcesz tam pracować. Posiadanie praktycznego zrozumienia współbieżności w Javie pomoże w poruszaniu się po większości pytań podczas rozmowy kwalifikacyjnej. Jeśli nie znasz starej szkoły współbieżności Java, porozmawiaj o tym, jak współczesna Java odrzuciła te problemy i wyjaśnij, jak możesz zamiast tego użyć rozwidlenia/połączenia lub strumieni równoległych.

Kompilator ma problemy: "Czy ten kod się kompiluje?" Cóż, nie wiem, po to są komputery i IDE - narzędzia mogą odpowiedzieć na pytanie, podczas gdy ja martwię się o inne rzeczy. Jeśli podczas wywiadów zadają ci się takie pytania, skorzystaj z niektórych materiałów edukacyjnych Java Certification (na przykład z prawdziwych książek), aby dowiedzieć się, jak na nie odpowiadać.

Struktury danych: Struktury danych Java są dość proste: zrozumienie różnicy między listą, zbiorem i mapą jest dobrym punktem wyjścia. Wiedza o tym, do czego służy kod skrótu, pomaga, podobnie jak sposób użycia równości w kontekstach kolekcji.

Szybkie wyszukiwanie w Internecie typowych pytań do rozmowy kwalifikacyjnej w języku Java również da ci dobry zestaw tematów do zbadania. Czy to oszustwo? Jeśli nauczysz się wystarczająco, aby przejść przez rozmowę, czy naprawdę będziesz wiedział wystarczająco dużo, aby wykonać tę pracę? Pamiętaj: nasza branża jest okropna w przeprowadzaniu wywiadów z programistami. Doświadczenie na rozmowie kwalifikacyjnej jest często bardzo oddalone od doświadczenia zawodowego. Zadawaj wiele pytań, aby zobaczyć, jak naprawdę wygląda praca. Dość łatwo można nauczyć się nowych technologii - to właśnie robimy cały czas. To wszystko, co związane z ludźmi, często decyduje o tym, czy odniesiesz sukces. Ale to temat na inny artykuł.

Rozwój oparty na testach

Programowanie sterowane testami (TDD) jest szeroko rozumiane. Przed TDD nacisk na wysoką jakość oprogramowania wywierał jedynie wiedza, doświadczenie i zaangażowanie programisty. Po TDD było coś jeszcze. Powszechnie przyjmuje się, że wysoka jakość oprogramowania obejmuje następujące właściwości w kodzie:

•  Modułowość
•  Luźne powiązanie
•  Spójność
•  Dobra separacja obaw
•  Ukrywanie informacji

Testowalny kod ma te właściwości. TDD to rozwój (projekt) oparty na testach. W TDD piszemy test przed napisaniem kodu, który sprawi, że test przejdzie. TDD to znacznie więcej niż "dobre testy jednostkowe". Napisanie testu najpierw jest ważne; oznacza to, że zawsze otrzymujemy kod "testowalny". Oznacza to również, że zasięg nigdy nie stanowi problemu. Jeśli najpierw napiszemy test, zawsze mamy duży zasięg i nie musimy się martwić o to jako o metrykę - a jest to słaba metryka. TDD wzmacnia talent programisty. To nie jest złe jeśli programiści są świetni, ale to czyni każdego programistę lepszym. TDD jest bardzo proste - proces to Red, Green, Refactor:

•  Piszemy test i widzimy, że kończy się niepowodzeniem (czerwony).
•  Piszemy minimalny kod, aby przejść i zobaczyć, jak przechodzi (zielony).
•  Refaktoryzujemy kod i test, aby były czyste, wyrazisty, elegancki i prosty, jak tylko możemy (Refactor).

Te kroki reprezentują trzy różne fazy w projektowaniu naszego kodu. Na każdym z tych kroków powinniśmy myśleć inaczej.

Czerwony

Skoncentruj się na wyrażaniu behawioralnej intencji swojego kodu. Skoncentruj się tylko na publicznym interfejsie swojego kodu. To wszystko, co projektujemy w tym momencie - nic więcej.Pomyśl tylko o tym, jak napisać ładny, przejrzysty test, który przechwyci dokładnie to, co chciałbyś, aby robił twój kod. Skoncentruj się na projektowaniu interfejsu publicznego, ułatwiając napisanie testu. Jeśli twoje pomysły są łatwe do wyrażenia w teście, będą również łatwe do wyrażenia, gdy ktoś użyje twojego kodu.

Zielony

Zrób najprostszą rzecz, która sprawi, że test przejdzie. Nawet jeśli ta prosta rzecz wydaje się naiwna. Tak długo, jak test kończy się niepowodzeniem, twój kod jest zepsuty i jesteś w niestabilnym punkcie rozwoju. Wracaj w bezpieczne miejsce (zielony) tak szybko i prosto, jak tylko możesz. Twoje testy powinny rosnąć, tworząc "specyfikację behawioralną" dla Twojego kodu. Przyjęcie dyscypliny pisania kodu tylko wtedy, gdy test kończy się niepowodzeniem, pomaga w lepszym opracowaniu i rozwinięciu tej specyfikacji.

Refaktoryzacja

Po powrocie do Greena możesz bezpiecznie dokonać refaktoryzacji. Dzięki temu jesteś szczery i nie możesz wędrować w chwasty i gubić się! Wykonaj małe proste kroki, a następnie ponownie uruchom testy, aby potwierdzić, że wszystko nadal działa. Refaktoryzacja nie jest refleksją. To okazja do bardziej strategicznego myślenia o swoim projekcie. Jeśli konfiguracja twoich testów jest zbyt złożona, twój kod prawdopodobnie ma słabe rozdzielenie problemów i może być zbyt ściśle powiązany z innymi rzeczami. Jeśli potrzebujesz dołączyć zbyt wiele innych klas, aby przetestować swój kod, być może Twój kod nie jest zbyt spójny. Ćwicz przerwę na refaktoryzację za każdym razem, gdy zdasz test. Zawsze patrz i zastanawiaj się: "Czy mógłbym zrobić to lepiej?" Trzy fazy TDD są różne, a twoje skupienie mentalne również powinno być różne, aby zmaksymalizować korzyści z każdej fazy.

W Twoim koszu/katalogu są świetne narzędzia

Każdy programista Java zna javac do kompilowania, java do uruchamiania i prawdopodobnie jar do pakowania aplikacji Java. Jednak wraz z JDK jest instalowanych wiele innych przydatnych narzędzi. Znajdują się one już na twoim komputerze w katalogu bin/ twojego JDK i można je wywołać z twojej PATH. Warto zapoznać się z niektórymi z tych narzędzi, aby wiedzieć, co jest do Twojej dyspozycji:

jps

Jeśli kiedykolwiek zdarzyło Ci się, że używasz ps aux | grep java, aby znaleźć uruchomione maszyny JVM, prawdopodobnie chcesz po prostu uruchomić jps. To dedykowane narzędzie wyświetla listę wszystkich uruchomionych maszyn JVM, ale zamiast pokazywać długie polecenie ze ścieżkami CLASSPATH i argumentami, jps po prostu wyświetla identyfikator procesu i nazwę głównej klasy aplikacji, co znacznie ułatwia ustalenie, który proces jest który. jps -l wyświetli w pełni kwalifikowaną nazwę klasy głównej, jps -m pokaże argumenty przekazane do metody main, a jps -v pokaże wszystkie argumenty przekazane do samej JVM.

javap

JDK jest dostarczany z deasemblerem plików klasy Java. Uruchom javap , aby zobaczyć pola i metody tego pliku klasy, co często może być bardzo pouczające dla zrozumienia, w jaki kod napisany w językach opartych na JVM, takich jak Scala, Clojure lub Groovy, jest przekształcany pod maską. Uruchom javap -c , aby zobaczyć kompletny kod bajtowy tych metod.

jmap

Uruchomienie jmap -heap spowoduje wydrukowanie podsumowania przestrzeni pamięci procesu JVM, na przykład ilości pamięci używanej w każdej generacji pamięci JVM, a także konfiguracji sterty i używanego GC. jmap -histo wypisze histogram każdej klasy w stercie, ile jest wystąpień tej klasy i ile bajtów pamięci jest zużytych. Co najważniejsze, uruchomienie jmap - dump:format=b,file= zrzuci migawkę całej sterty do pliku.

jhat

Uruchomienie jhat spowoduje przejęcie pliku wygenerowanego przez jmap i uruchomienie lokalnego serwera WWW. Możesz połączyć się z tym serwerem w przeglądarce, aby interaktywnie eksplorować przestrzeń sterty pogrupowaną według nazwy pakietu. Link "Pokaż liczbę wystąpień dla wszystkich klas (z wyłączeniem platformy)" pokazuje tylko wystąpienia klas poza samą Javą. Możesz także uruchamiać zapytania "OQL", co pozwala na wysyłanie zapytań do przestrzeni sterty za pomocą składni SQLesque.

jinfo

Uruchom jinfo , aby wyświetlić wszystkie właściwości systemu, za pomocą których maszyna JVM została załadowana, oraz flagi wiersza polecenia maszyny JVM.

jstack

Uruchomienie jstack spowoduje wydrukowanie śladów stosu dla wszystkich bieżących wątków Java działających w JVM.

jconsole i jvisualvm

Są to narzędzia graficzne, które umożliwiają łączenie się z maszynami JVM i interaktywne monitorowanie uruchomionych maszyn JVM. Oferują one graficzne wykresy i histogramy różnych aspektów uruchomionego procesu i są przyjazną dla myszy alternatywą dla wielu narzędzi wymienionych powyżej.

jshell

Począwszy od wersji Java 9, Java ma REPL typu "uczciwość do dobra" - doskonałe narzędzie do sprawdzania składni, uruchamiania szybkich poleceń opartych na Javie lub testowania kodu i eksperymentowania bez tworzenia pełnych programów. Wiele z tych narzędzi może działać nie tylko lokalnie, ale także z procesami JVM uruchomionymi na zdalnych maszynach. To tylko niektóre z przydatnych programów, które już zainstalowałeś; poświęć trochę czasu, aby zobaczyć, co jeszcze znajduje się w katalogu plików wykonywalnych twojego JDK i przeczytaj ich strony podręcznika - zawsze warto wiedzieć, jakie narzędzia znajdują się w twoim pasku narzędzi.

Myśl poza piaskownicą Java

"Java to najlepszy język na świecie, do każdego celu". Jeśli w to wierzysz, musisz więcej wychodzić. Jasne, Java to świetny język, ale nie jest to jedyny dobry język ani najlepszy do każdego celu. W rzeczywistości od czasu do czasu powinieneś - jako profesjonalny programista - poświęcić czas na naukę i używanie nowego języka, zarówno w pracy, jak i samodzielnie. Zanurz się wystarczająco głęboko, aby rozpoznać, jak różni się w fundamentalny sposób od tego, do czego jesteś przyzwyczajony i czy może być przydatny w twoich projektach. Innymi słowy: spróbuj, może ci się spodoba. Oto kilka języków, których możesz chcieć się nauczyć:

•  JavaScript to język przeglądarki. Pomimo podobnych nazw i kilkanaście słów kluczowych, JavaScript i Java są bardzo różne. JavaScript jest dostarczany z setkami różnych frameworków internetowych, z których niektóre wykraczają poza frontend. Na przykład Node.js umożliwia uruchamianie JavaScript po stronie serwera, co otwiera wiele nowych możliwości.

•  Kotlin to język JVM, który, podobnie jak większość tych języków, ma bardziej zrelaksowana składnia niż Java, wraz z innymi funkcjami, które mogą dać jej przewagę nad Javą. Google używa Kotlina do większości swojej pracy w Androidzie i zachęca do korzystania z niego w aplikacjach na Androida. Nuff powiedział!

•  Dart and Flutter: Dart to skompilowany język skryptowy firmy Google. Pierwotnie do programowania internetowego, nie rozkwitł, dopóki Flutter nie zaczął używać Dart dla aplikacji na Androida i iOS (a pewnego dnia po stronie przeglądarki) z jednej bazy kodu.

•  Python, Ruby i Perl istnieją od dziesięcioleci i pozostają jednymi z najpopularniejszych języków. Pierwsze dwa mają implementacje JVM, Jython i JRuby, chociaż ta pierwsza nie jest aktywnie utrzymywana.

•  Scala, Clojure (http://clojure.org) i Frege (implementacja Haskella) to języki programowania funkcjonalnego (FP) JVM. FP ma długą, wąską historię, ale w ostatnich latach wkracza do głównego nurtu. W chwili pisania tego tekstu wiele języków FP, takich jak Idris i Agda, nie działa na JVM. Nauka FP może pomóc Ci w korzystaniu z funkcjonalnych udogodnień w Javie 8+, jeśli nie jesteś naprawdę wygodnie.
•  R jest językiem interpretowanym do manipulacji danymi. Sklonowany z S Bell Labs dla statystyków, R jest teraz popularny wśród naukowców zajmujących się danymi lub każdego, kto wychodzi "poza arkusz kalkulacyjny". Wiele wbudowanych i dodatkowych funkcji statystycznych, matematycznych i graficznych.
•  Rust to skompilowany język przeznaczony do tworzenia systemów z funkcjami współbieżności i silnego pisania.
•  Go ("Golang") to skompilowany język wymyślony w Google przez Roberta Griesemera, Roba Pike'a i Kena Thompsona (współtwórcę Unixa). Istnieje wiele kompilatorów, natywnie przeznaczonych dla różnych systemów operacyjnych i tworzenia stron internetowych poprzez kompilację do JavaScript i WebAssembly.
* C jest przodkiem C++, Objective-C i, do pewnego stopnia, Java, C# i Rust. (C dał tym językom podstawową składnię wbudowanych typów, składnię metod i nawiasy klamrowe dla bloków kodu i jest językiem winnym int i = 077; mający wartość 63 w Javie.) Jeśli nie naucz się języka asemblerowego, "poziom C" to miejsce, w którym możesz zacząć rozumieć modele pamięci, które da ci uznanie dla sposobu robienia rzeczy w Javie.
* JShell nie jest językiem, per se - jest to inny sposób robienia Javy.

Zamiast pisać public class Mumble { i public static void main(String[] args) { tylko po to, aby wypróbować wyrażenie lub jakieś nowe API, po prostu zapomnij o całej ceremonii i schemacie i użyj JShell.

Myślenie współprogramowe

Współprogramy to funkcje lub metody, które można zawiesić i wznowić. W Kotlinie można ich używać zamiast wątków do pracy asynchronicznej, ponieważ wiele współprogramów może działać wydajnie na jednym wątku. Aby zobaczyć, jak działają współprogramy, stworzymy przykładowy program, który odtwarza równolegle te sekwencje bębnów:

Sekwencja instrumentów
Tomy x-x-x-x-x-x-x-x-
High-hat x-x-x---x-x-x---
Talerz perkusyjny ----------------x----

Moglibyśmy do tego użyć wątków, ale w większości systemów dźwięk jest odtwarzany przez podsystem dźwiękowy, podczas gdy kod zatrzymuje się, aż będzie mógł odtworzyć następny dźwięk. Blokowanie w ten sposób cennego zasobu, takiego jak wątek, jest marnotrawstwem. Zamiast tego stworzymy zestaw współprogramów: po jednym dla każdego z instrumentów. Będziemy mieli metodę o nazwie playBeats, która pobiera sekwencję perkusyjną i nazwę pliku dźwiękowego. Pełny kod znajduje się pod adresem https://oreil.ly/6x0GK; uproszczona wersja wygląda tak:

suspend fun playBeats(beats: String, file: String) {
for (...) { // for each beat
&helli[;
playSound(file)

delay(