Design af et brugervenligt Java-bibliotek

1. Oversigt

Java er en af ​​søjlerne i open source-verdenen. Næsten hvert Java-projekt bruger andre open source-projekter, da ingen ønsker at genopfinde hjulet. Imidlertid sker det ofte, at vi har brug for et bibliotek til dets funktionalitet, men vi har ingen anelse om, hvordan vi bruger det. Vi løber ind i ting som:

  • Hvad er det med alle disse "* Service" klasser?
  • Hvordan instanterer jeg dette, det tager for mange afhængigheder. Hvad er en “låsen“?
  • Åh, jeg satte det sammen, men nu begynder det at smide IllegalStateException. Hvad laver jeg forkert?

Problemet er, at ikke alle biblioteksdesignere tænker på deres brugere. De fleste tænker kun på funktionalitet og funktioner, men få overvejer, hvordan API'en skal bruges i praksis, og hvordan brugernes kode vil se ud og blive testet.

Denne artikel leveres med et par råd om, hvordan vi kan gemme vores brugere nogle af disse kampe - og nej, det er ikke ved at skrive dokumentation. Naturligvis kunne der skrives en hel bog om dette emne (og nogle få har været); dette er nogle af de vigtigste punkter, jeg lærte, mens jeg selv arbejdede på flere biblioteker.

Jeg vil eksemplificere ideerne her ved hjælp af to biblioteker: charles og jcabi-github

2. Grænser

Dette skal være indlysende, men mange gange er det ikke. Før vi begynder at skrive en kodelinje, skal vi have et klart svar på nogle spørgsmål: hvilke input er nødvendige? hvad er den første klasse, som min bruger vil se? har vi brug for nogen implementeringer fra brugeren? hvad er output? Når disse spørgsmål er klart besvaret, bliver alt lettere, da biblioteket allerede har en foring, en form.

2.1. Indgang

Dette er måske det vigtigste emne. Vi er nødt til at sikre, at det er klart, hvad brugeren skal levere til biblioteket for at det kan udføre sit arbejde. I nogle tilfælde er dette en meget triviel sag: det kunne bare være en streng, der repræsenterer auth-token til en API, men det kan også være en implementering af en grænseflade eller en abstrakt klasse.

En meget god praksis er at tage alle afhængigheder gennem konstruktører og holde disse korte med et par parametre. Hvis vi har brug for en konstruktør med mere end tre eller fire parametre, skal koden klart omformuleres. Og hvis der bruges metoder til at injicere obligatoriske afhængigheder, vil brugerne sandsynligvis ende med den tredje frustration, der er beskrevet i oversigten.

Vi bør også altid tilbyde mere end en konstruktør, give brugerne alternativer. Lad dem arbejde begge med Snor og Heltal eller begræns dem ikke til en FileInputStream, arbejde med en InputStream, så de måske kan indsende ByteArrayInputStream når enhedstest osv.

For eksempel er her et par måder, hvorpå vi kan instantiere et Github API-indgangspunkt ved hjælp af jcabi-github:

Github noauth = ny RtGithub (); Github basicauth = ny RtGithub ("brugernavn", "adgangskode"); Github oauth = ny RtGithub ("token"); 

Enkel, ingen trængsel, ingen skyggefulde konfigurationsobjekter at initialisere. Og det giver mening at have disse tre konstruktører, fordi du kan bruge Github-webstedet, mens du er logget ud, logget ind eller en app kan godkende på dine vegne. Naturligvis fungerer nogle funktioner ikke, hvis du ikke er godkendt, men du ved det fra starten.

Som et andet eksempel er her, hvordan vi ville arbejde med charles, et webcrawlingbibliotek:

WebDriver-driver = ny FirefoxDriver (); Repository repo = nyt InMemoryRepository (); String indexPage = "//www.amihaiemil.com/index.html"; WebCrawl-graf = ny GraphCrawl (indexPage, driver, nye IgnoredPatterns (), repo); graph.crawl (); 

Det er også ret selvforklarende, tror jeg. Mens jeg skriver dette, indser jeg dog, at der i den aktuelle version er en fejltagelse: alle konstruktører kræver, at brugeren leverer en instans af Ignorerede mønstre. Som standard skal ingen mønstre ignoreres, men brugeren skal ikke angive dette. Jeg besluttede at lade det være her her, så du ser et modeksempel. Jeg antager, at du ville prøve at sætte gang i en WebCrawl og spekulere på ”Hvad er det med det? Ignorerede mønstre?!”

Variabel indexPage er den URL, hvorfra gennemgangen skal starte, driveren er den browser, der skal bruges (kan ikke som standard være noget, da vi ikke ved, hvilken browser der er installeret på den kørende maskine). Repo-variablen forklares nedenfor i det næste afsnit.

Så som du ser i eksemplerne, så prøv at holde det simpelt, intuitivt og selvforklarende. Indkapsl logik og afhængigheder på en sådan måde, at brugeren ikke klør sig i hovedet, når han ser på dine konstruktører.

Hvis du stadig er i tvivl, så prøv at lave HTTP-anmodninger til AWS ved hjælp af aws-sdk-java: du bliver nødt til at håndtere en såkaldt AmazonHttpClient, der bruger en ClientConfiguration et eller andet sted og derefter skal tage en ExecutionContext et sted imellem. Endelig kan du muligvis udføre din anmodning og få et svar, men har stadig ingen anelse om, hvad en ExecutionContext er, for eksempel.

2.2. Produktion

Dette er for det meste til biblioteker, der kommunikerer med den ydre verden. Her skal vi svare på spørgsmålet "hvordan håndteres output?". Igen et ret sjovt spørgsmål, men det er let at træde forkert.

Se igen på koden ovenfor. Hvorfor skal vi levere en implementering af lageret? Hvorfor returnerer metoden WebCrawl.crawl () ikke bare en liste over WebPage-elementer? Det er tydeligvis ikke bibliotekets opgave at håndtere de gennemsøgte sider. Hvordan skal det overhovedet vide, hvad vi gerne vil gøre med dem? Noget som dette:

WebCrawl-graf = ny GraphCrawl (...); Listsider = graph.crawl (); 

Intet kunne være værre. En OutOfMemory-undtagelse kan ske ud af ingenting, hvis det crawlede websted tilfældigvis har, lad os sige, 1000 sider - biblioteket indlæser dem alle i hukommelsen. Der er to løsninger på dette:

  • Bliv ved med at returnere siderne, men implementer en personsøgningsmekanisme, hvor brugeren skal angive start- og slutnumre. Eller
  • Bed brugeren om at implementere en grænseflade med en metode kaldet eksport (liste), som algoritmen vil ringe til, hver gang der nås et maksimalt antal sider

Den anden mulighed er langt den bedste; det holder tingene enklere på begge sider og er mere testbare. Tænk på, hvor meget logik der skal implementeres på brugerens side, hvis vi gik med den første. På denne måde er der angivet et lager til sider (at sende dem i en DB eller skrive dem muligvis på en disk), og intet andet skal gøres efter opkaldsmetodesøgning ().

Forresten er koden fra afsnittet Input ovenfor alt, hvad vi skal skrive for at få indholdet af hjemmesiden hentet (stadig i hukommelsen, som repo-implementeringen siger, men det er vores valg - vi forudsatte, at implementeringen så vi tager risikoen).

For at opsummere dette afsnit: vi bør aldrig adskille vores job helt fra klientens job. Vi skal altid tænke på, hvad der sker med den output, vi skaber. Meget som en lastbilchauffør skal hjælpe med at pakke varerne ud snarere end blot at smide dem ud ved ankomsten til destinationen.

3. Grænseflader

Brug altid grænseflader. Brugeren skal kun interagere med vores kode gennem strenge kontrakter.

For eksempel i jcabi-github bibliotek klassen RtGithub si den eneste, som brugeren faktisk ser:

Repo repo = ny RtGithub ("oauth_token"). Repos (). Get (nye Coordinates.Simple ("eugenp / tutorials")); Issue issue = repo.issues () .create ("Eksempel nummer", "Oprettet med jcabi-github");

Ovenstående uddrag opretter en billet i eugenp / tutorials repo. Forekomster af repo og udgave bruges, men de faktiske typer afsløres aldrig. Vi kan ikke gøre noget som dette:

Repo repo = ny RtRepo (...)

Ovenstående er ikke mulig af en logisk grund: vi kan ikke direkte oprette et problem i en Github repo, kan vi? Først skal vi logge ind, derefter søge i repoen, og først derefter kan vi oprette et problem. Naturligvis kunne scenariet ovenfor tillades, men derefter ville brugerens kode blive forurenet med en masse kedelpladekode: at RtRepo bliver sandsynligvis nødt til at tage en form for autorisationsobjekt gennem sin konstruktør, autorisere klienten og komme til den rigtige repo osv.

Grænseflader giver også let udvidelse og bagudkompatibilitet. På den ene side er vi som udviklere forpligtet til at respektere de allerede frigivne kontrakter, og på den anden side kan brugeren udvide de grænseflader, vi tilbyder - han kan dekorere dem eller skrive alternative implementeringer.

Med andre ord, abstrakt og indkapsles så meget som muligt. Ved at bruge grænseflader kan vi gøre dette på en elegant og ikke-begrænsende måde - vi håndhæver arkitektoniske regler, samtidig med at vi giver programmøren frihed til at forbedre eller ændre den adfærd, vi udsætter.

For at afslutte dette afsnit skal du bare huske på: vores bibliotek, vores regler. Vi skal vide nøjagtigt, hvordan klientens kode vil se ud, og hvordan han vil enhedsteste den. Hvis vi ikke ved det, vil ingen og vores bibliotek blot bidrage til at skabe kode, der er vanskelig at forstå og vedligeholde.

4. Tredjeparter

Husk, at et godt bibliotek er et letvægtsbibliotek. Din kode kan muligvis løse et problem og være funktionel, men hvis krukken tilføjer 10 MB til min build, er det klart, at du mistede tegningerne af dit projekt for længe siden. Hvis du har brug for mange afhængigheder, prøver du sandsynligvis at dække for meget funktionalitet og bør opdele projektet i flere mindre projekter.

Være så gennemsigtig som muligt, når det er muligt ikke binde til faktiske implementeringer. Det bedste eksempel, der kommer til at tænke på, er: brug SLF4J, som kun er en API til logning - brug ikke log4j direkte, måske vil brugeren gerne bruge andre loggere.

Dokumentbiblioteker, der gennemgår dit projekt transitivt, og sørg for at du ikke inkluderer farlige afhængigheder som f.eks xalan eller xml-apis (hvorfor de er farlige er ikke for denne artikel at uddybe).

Bundlinjen her er: Hold din bygning lys, gennemsigtig og ved altid hvad du arbejder med. Det kan spare dine brugere mere trængsel end du kunne forestille dig.

5. Konklusion

Artiklen skitserer et par enkle ideer, der kan hjælpe et projekt med at holde sig på linjen med hensyn til brugervenlighed. Et bibliotek, der er en komponent, der skal finde sin plads i en større sammenhæng, skal have en stærk funktionalitet, men alligevel tilbyde en glat og veludformet grænseflade.

Det er et let skridt over linjen og gør et rod ud af designet. Bidragsydere ved altid, hvordan de skal bruges, men en ny, der først ser øjnene på det, gør det måske ikke. Produktivitet er det vigtigste af alle, og efter dette princip skal brugerne kunne begynde at bruge et bibliotek i løbet af få minutter.