Java >> Java tutorial >  >> Tag >> class

Dataklasser, der anses for at være skadelige

Dette blogindlæg forklarer motivationen bag at fjerne Project Lombok fra et af de projekter, som jeg bidrager til. Det afspejler min personlige mening og afskrækker ikke bestemte teknologier.

For omkring tre år siden lærte jeg Project Lombok at kende, et bibliotek, der krydrer Java-kode. Jeg kunne godt lide det fra begyndelsen, da det bidrager med så meget nyttig funktionalitet. Jeg arbejder meget med entiteter (dataklasser) og værdiobjekter, så det kommer ikke som en overraskelse, at @Data eller Kotlins data class er meget bekvemme. Du får mere for pengene - bogstaveligt talt.
Jeg nævner Kotlin her, fordi det deler nogle af de ejendomme, som vi også får fra Lombok.

Adoption af sådanne (sprog|kodegenerering) funktioner i en kodebase starter typisk langsomt. Jo mere koden udvikler sig, jo flere komponenter bruger sådanne funktioner, fordi det er praktisk at bruge funktioner, du får gratis* og som du allerede er vant til. Med en enkelt annotation eller et enkelt søgeord tilvælger vi noget, der giver os ejendomsadgang, equals /hashCode , toString , genererede konstruktører og mere.

* :I virkeligheden er der ikke noget, der hedder en gratis frokost.

Nu kan man sige, brug kun det, du har brug for, og du har fuldstændig ret. Brug @Getters og @Setters hvis du kun ønsker ejendomstilbehør. Hvis du ønsker at få equals /hashCode , og tilføj derefter den relevante anmærkning. Rigtigt. I mange tilfælde mener vi, at vi har brug for mere funktionalitet, så hvorfor fylde koden med flere annoteringer, når vi får, hvad vi ønsker (og mere) med en enkelt @Data anmærkning. Handler det her ikke om kedelplade? Så det ser ud til at være en god ting at reducere antallet af annoteringer.

Nå:Nej.

Her er grunden:

Utilsigtet kompleksitet

Ved at introducere kodegenerering (det er hvad Lombok og Kotlin data classes gør), får vi en masse funktionalitet, men det egentlige spørgsmål burde være:Er det den funktionalitet, jeg ønsker skal være tilgængelig? Eller ønsker vi hellere at få eksplicit kontrol over funktionaliteten?
I flere tilfælde brugte vi dataklasser af bekvemmelighed. Med fjernelsen af ​​Lombok fandt vi ud af, at vi implicit brugte en masse funktioner, vi fik gratis* , såsom ligestillingskontrol. Med fjernelsen af ​​den genererede kode begyndte mange tests at mislykkes, fordi disse funktioner ikke længere var tilgængelige. De manglende funktioner rejste spørgsmålet:Er denne funktion påkrævet?

Dette spørgsmål kan så let overskues ved blot at tilmelde dig en dataklasse. I modsætning til det ville vi med en eksplicit tilgang have brugt mere tid på emnet. Sandsynligvis ville vores test se anderledes ud, eller vi ville have været mere eksplicitte om specifikke funktioner.

Eksplicit styring af din kode uden genereringsværktøjer tvinger dig til at tænke på, om funktionaliteten virkelig er påkrævet, eller om den ikke er det.

(Gentaget) PSA:„Kodegenerering, så du kan udføre forkerte ting hurtigere…“ #GeeCon— Oliver Drotbohm 🥁&👨‍💻 (@odrotbohm) 23. oktober 2014

Hvad er Boilerplate?

Boilerplate-kode er kode, som vi gentagne gange skal skrive for at afsløre en bestemt funktionalitet i stedet for at fortælle koden, at vi ønsker, at denne funktion skal fungere ud af boksen. Typiske eksempler er ejendomsadgang (Getters, Setters) og lighedstjek (equals /hashCode ). Nogle gange også konstruktører.
I modsætning til vores tidligere tro, er nedbrydning af en Lombok-annotation i dens egne komponenter ikke kedelplade. Det er ikke at være præcist, det er bekvemmelighed og at være ikke ansvarlig.

Arbejd omkring compileren

Dette er et Lombok-specifikt aspekt. Java compiler var aldrig beregnet til ting, som Lombok gør. Lombok-vedligeholdere gjorde et spektakulært stykke arbejde for at få det Lombok gør. Dette kommer til prisen for flere løsninger i compileren rettet mod specifikke compilere. De nødvendige ting til javac er til en vis grad forskellige fra, hvad der skal gøres for Eclipse's ecj .

I et statisk arrangement, hvor JDK'er og Eclipse IDE aldrig ændres, er alt fint. Men den virkelige verden er anderledes. Eclipse sender opdateringer, Java-udgivelseskadencehastigheden steg fra Java 9. Project Lombok er ikke drevet af en virksomhed, men af ​​et team af open source-bidragydere, hvis tid er begrænset.

Java-opgraderinger forårsagede tidligere, at Lombok var den komponent, der forhindrede os i at opgradere til nyere Java-versioner:Compiler internals havde ændret sig, og Lombok havde endnu ingen chance for at indhente det. Med Lombok-brug spredt over hele kodebasen, er den eneste mulighed ikke at opgradere.

Men:Ikke at opgradere er ikke en mulighed på lang sigt.
Til sidst indhentede Lombok det, hvilket åbner vejen for at opgradere til nyere versioner igen.

Plugin alle tingene!

Et aspekt af Lombok er, at det skal fortælle din IDE om genererede klassemedlemmer. Selvom der ikke er e. g. Sætter din kode, den er der i den kompilerede kode, og din IDE skal vide om det for ikke at give dig fejl. For IntelliJ og Netbeans er det ikke så meget et problem, fordi du kan aktivere annotationsbehandling og bruge det valgfri IntelliJ-plugin. Til Eclipse skal du bruge en agent, der ændrer Eclipse-adfærd. Uden korrekt IDE-opsætning vil enhver, der ønsker at arbejde med koden, få fejl/advarsler, der rejser spørgsmålet:Hvordan fungerer det overhovedet?

Kognitiv belastning

Hver ikke-indlysende adfærd bidrager til kompleksitet i den forstand, at den skal forstås. Hver ikke-standardadfærd fører også ned ad samme vej. Folk, der skal arbejde med sådan en kodebase for første gang, skal forstå, hvad der kommer til at forstå kodebasen. Selvom dette ikke er specifikt for Lombok, har alle hjælpeprogrammer, der bidrager med yderligere funktionalitet til din kode (kodegeneratorer, AOP, JVM-agenter, bytekodemanipulation generelt), et vist potentiale, der kan beskrives som magi. Hvorfor magi? For i første øjeblik er det ikke indlysende, hvad der sker. Det kan blive tydeligt, når nogen forklarer dig tricket.

En anden ændrer din (kompilerede) kode

Med brug af kodegenereringsfunktioner er vi afhængige af, at en anden gør det rigtige arbejde. Vi køber ind i dem, så deres værktøj giver os funktionalitet, der er nyttig for os. Vi skal ikke længere bøvle med den rigtige implementering til f.eks. equals /hashCode , bliver tilføjelse af en ejendom en let sag, fordi generationen opfanger forandringen for os. Udvider manuelt equals /hashCode er ikke trivielt. Nogle værktøjer kan gøre dette for os, men som du måske allerede har forudset, udveksler vi tool1 for tool2 uden at forbedre vores situation væsentligt.
En gang imellem ændrer værktøjer, hvordan de genererer kode, eller hvilke bits de genererer, og hvilke de stopper med at generere. At finde ud af disse ændringer er ikke sjovt, men vi har ikke en mulighed, hvis vi allerede har købt ind i deres programmeringsmodel. Den eneste mulighed er at trække sig tilbage, og det kommer på bekostning af manuel implementering.

Accidental Complexity 2:The Build

Afhængigt af konteksten er dette muligvis kun relevant for vores projekt. Vi sender et bibliotek med offentlig API-overflade ledsaget af en sources jar og Javadoc. Som standard fungerer Lombok med din .class Kun filer. Dette medfører, at kildekrukken ikke indeholder de genererede metoder, og Javadoc viser heller ikke de genererede medlemmer. Det, der startede med at eliminere boilerplate-kode, fortsætter med stigende kompleksitet. For at få ordentlige source jars og Javadoc skal vi tilføje plugins til buildet, der delomboker koden først og tillade source jar/Javadoc at køre oven på de delombokede kilder.

Afhængigt af din opsætning bruges de delombokede kilder kun til kildekrukken og Javadoc. Det betyder, at du bruger én version af din kode til dokumentationsformål. Denne kode er forskellig fra den, du bruger til at kompilere. Lombok fører i det væsentlige til den samme outcode. At gøre dette aspekt indlysende efterlader os med en dårlig følelse.

Øget kompleksitet kommer typisk med en længere byggetid, og vi kan spørge os selv, om det er det værd, vi får.

En god udvikler er som en varulv:bange for sølvkugler.— 🖖 Jochen Mader 🇪🇺 (@codepitbull) 8. oktober 2016

Lombok polariserer fællesskabet

Selvom de foregående afsnit lyder, som om vi har at gøre med alvorlige problemer, er mange af dem sandsynligvis specifikke for vores projektkontekst. Lombok lover at reducere boilerplate-koden. Den gør sit arbejde godt. At arbejde i et dataorienteret miljø, hvor vi har brug for forskellige konstellationer af objekter til test eller endda i produktionskoden, kræver meget kode for et ordentligt dataobjekt/værdiobjekt.
Giver en god implementering til hashCode er ikke-triviel. Der er et par CVE'er på grund af ukorrekt hashCode implementeringer. Glemte at tilføje et felt i equals /hashCode er en anden almindelig kilde til fejl.
Vi eliminerer disse kilder til fejl, når vi bruger kodegenerering. Kode, der ikke er der, påvirker heller ikke vores testdækningsstatistikker. Dette betyder ikke, at det ikke behøver at blive testet.

Ser vi på statistikken for Lombok-fjernelsesforpligtelsen, ser vi:

Fjernet:300 linjer
Tilføjet:1200 linjer

Dette er en ret god fremstilling af, hvilken fordel vi får ud af at bruge Lombok. Når først Lombok er brugt et enkelt sted, fortsætter vi typisk med at bruge det andre steder - fordi det allerede er på klassestien. Ser vi på de fjernede 300 linjer, bør vi i stedet se dem som 150 linjer fjernet, fordi det typisk er en import sætning og en annotation, der groft efterlader os med et forhold på 1:8 mellem bekvemmelighedskode og manuelt vedligeholdt kode.

Vi bliver ikke betalt af kodelinjer, men at have mere kode resulterer i en større overflade at vedligeholde.

Når man ser på mit tweet, er der meget modsatrettede meninger. Disse reaktioner er grunden til, at der ikke er et enkelt svar, hvornår du bør/bør ikke bruge Project Lombok eller Kotlin dataklasser, da det altid afhænger af dit team, konteksten og hvilken type kode du skriver.

Jeg har for nylig fjernet @project_lombok fra et projekt. Et tweet er for kort til at opsummere resultaterne. Følger op med et blogindlæg. https://t.co/wpS33nKScA— Mark Paluch 👨‍💻&🎹 (@mp911de) 2. juli 2019

Dobbelt smerte

Ikke at bruge kodegenereringsfunktioner gør koden eksplicit. Eksplicit kode afslører altid, hvad den gør. Eksplicit kode kræver design. At komme ind i kodegenereringsfunktioner er fristende på grund af øjeblikkelige resultater og indledende enkelhed. Når vi først har brugt disse funktioner, gennemgår vi forskellige situationer og lærer om aspekter, der ikke umiddelbart var indlysende. Det er svært at komme til et punkt for at fjerne en ganske fordelagtig funktion på grund af de tilknyttede omkostninger. Husker du 1:8 LoC-forholdet?

Bare fordi vi ønsker at slippe af med kodegenerering, betyder det ikke, at vi kan fjerne funktioner, som vi har modtaget af værktøjet gratis* . Det betyder snarere, at vi skal levere denne funktionalitet på egen hånd.

Jeg ville sige det sådan:Du har et hus, du lejer det ud til en lejer, fordi leje løfter profiterer. Til sidst finder du ud af, at din lejer er rodet, og du begynder at slippe af med din lejer. Når din lejer er ude, indser du omfanget af rod, og du begynder at rydde op for ikke at miste dit hus.

Nettoeffekten er den samme:Du har lagt mange kræfter (og sandsynligvis penge) i den læring.

Hvis din lejer opfører sig ordentligt, er der ingen grund til at ændre på, hvordan tingene er.

Java tag