Designe et brukervennlig Java-bibliotek

1. Oversikt

Java er en av pilarene i åpen kildekodeverden. Nesten hvert Java-prosjekt bruker andre open source-prosjekter siden ingen ønsker å finne opp hjulet på nytt. Imidlertid hender det mange ganger at vi trenger et bibliotek for funksjonaliteten, men vi har ingen anelse om hvordan vi skal bruke det. Vi støter på ting som:

  • Hva er det med alle disse "* Service" -klassene?
  • Hvordan instantierer jeg dette, det tar for mange avhengigheter. Hva er en “klinke“?
  • Å, jeg satte den sammen, men nå begynner det å kaste IllegalStateException. Hva gjør jeg galt?

Problemet er at ikke alle biblioteksdesignere tenker på brukerne sine. De fleste tenker bare på funksjonalitet og funksjoner, men få vurderer hvordan API-et skal brukes i praksis, og hvordan brukernes kode vil se ut og bli testet.

Denne artikkelen kommer med noen få råd om hvordan vi kan spare brukerne noen av disse kampene - og nei, det er ikke gjennom å skrive dokumentasjon. Selvfølgelig kan det skrives en hel bok om dette emnet (og noen få har vært); dette er noen av de viktigste punktene jeg lærte mens jeg jobbet med flere biblioteker selv.

Jeg vil eksemplifisere ideene her ved hjelp av to biblioteker: charles og jcabi-github

2. Grenser

Dette burde være åpenbart, men mange ganger er det ikke. Før vi begynner å skrive en kodelinje, må vi ha et klart svar på noen spørsmål: hvilke innganger er nødvendig? hva er den første klassen min bruker vil se? trenger vi noen implementeringer fra brukeren? hva er produksjonen? Når disse spørsmålene er klart besvart, blir alt lettere siden biblioteket allerede har en fôr, en form.

2.1. Inngang

Dette er kanskje det viktigste temaet. Vi må sørge for at det er klart hva brukeren trenger å gi biblioteket for å kunne utføre sitt arbeid. I noen tilfeller er dette en veldig triviell sak: det kan bare være en streng som representerer auth-token for et API, men det kan også være en implementering av et grensesnitt eller en abstrakt klasse.

En veldig god praksis er å ta alle avhengighetene gjennom konstruktører og holde disse korte, med noen få parametere. Hvis vi trenger å ha en konstruktør med mer enn tre eller fire parametere, bør koden tydelig omformuleres. Og hvis metoder brukes til å injisere obligatoriske avhengigheter, vil brukerne mest sannsynlig ende opp med den tredje frustrasjonen som er beskrevet i oversikten.

Vi bør også alltid tilby mer enn en konstruktør, gi brukerne alternativer. La dem jobbe begge med String og Heltall eller ikke begrense dem til a FileInputStream, jobbe med en InputStream, så de kan sende inn kanskje ByteArrayInputStream når enhetstesting osv.

Her er for eksempel noen måter vi kan starte et Github API-inngangspunkt ved hjelp av jcabi-github:

Github noauth = ny RtGithub (); Github basicauth = nye RtGithub ("brukernavn", "passord"); Github oauth = ny RtGithub ("token"); 

Enkelt, ikke noe mas, ingen skyggefulle konfigurasjonsobjekter å initialisere. Og det er fornuftig å ha disse tre konstruktørene, fordi du kan bruke Github-nettstedet mens du er logget ut, logget på eller en app kan autentisere på dine vegne. Naturligvis vil ikke noen funksjonalitet fungere hvis du ikke er autentisert, men du vet dette fra starten.

Som et andre eksempel, her er hvordan vi ville jobbe med charles, et nettgjennomgangsbibliotek:

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

Det er også ganske selvforklarende, tror jeg. Mens jeg skriver dette, innser jeg imidlertid at i den nåværende versjonen er det en feil: alle konstruktørene krever at brukeren leverer en forekomst av Ignorerte mønstre. Som standard skal ingen mønstre ignoreres, men brukeren trenger ikke å spesifisere dette. Jeg bestemte meg for å legge det slik her, så du ser et moteksempel. Jeg antar at du vil prøve å sette i gang en WebCrawl og lure på "Hva er det med det? Ignorerte mønstre?!”

Variabel indeksSide er URL-en derfra gjennomsøking skal starte, driveren er nettleseren du skal bruke (kan ikke være standard til noe siden vi ikke vet hvilken nettleser som er installert på maskinen som kjører). Repo-variabelen vil bli forklart nedenfor i neste avsnitt.

Så som du ser i eksemplene, prøv å holde det enkelt, intuitivt og selvforklarende. Innkapsl logikk og avhengigheter på en slik måte at brukeren ikke klør seg i hodet når han ser på konstruktørene dine.

Hvis du fortsatt er i tvil, kan du prøve å gjøre HTTP-forespørsler til AWS ved hjelp av aws-sdk-java: du må håndtere en såkalt AmazonHttpClient, som bruker en ClientConfiguration et sted, og må deretter ta en ExecutionContext et sted i mellom. Til slutt kan du utføre forespørselen din og få svar, men har fremdeles ingen anelse om hva en ExecutionContext er, for eksempel.

2.2. Produksjon

Dette er mest for biblioteker som kommuniserer med den ytre verden. Her skal vi svare på spørsmålet “hvordan vil produksjonen håndteres?”. Igjen, et ganske morsomt spørsmål, men det er lett å gå galt.

Se igjen på koden ovenfor. Hvorfor må vi tilby en repositorimplementering? Hvorfor returnerer ikke metoden WebCrawl.crawl () bare en liste over WebPage-elementer? Det er tydeligvis ikke bibliotekets jobb å håndtere de gjennomsøkte sidene. Hvordan skal det til og med vite hva vi vil gjøre med dem? Noe sånt som dette:

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

Ingenting kan være verre. Et OutOfMemory-unntak kan skje fra ingenting hvis det gjennomsøkte nettstedet tilfeldigvis har 1000 sider - biblioteket laster dem alle i minnet. Det er to løsninger på dette:

  • Fortsett å returnere sidene, men implementer en personsøkemekanisme der brukeren må oppgi start- og sluttnummer. Eller
  • Be brukeren om å implementere et grensesnitt med en metode som kalles eksport (Liste), som algoritmen vil ringe hver gang et maksimalt antall sider blir nådd

Det andre alternativet er uten tvil det beste; det holder ting enklere på begge sider og er mer testbart. Tenk hvor mye logikk som må implementeres på brukerens side hvis vi gikk med den første. På denne måten er det angitt et arkiv for sider (for å sende dem i en DB eller skrive dem på disken kanskje), og ingenting annet må gjøres etter at metoden er gjennomsøkt ().

Forresten, koden fra Inndataseksjonen ovenfor er alt vi må skrive for å få innholdet på nettstedet hentet (fremdeles i minnet, som repo-implementeringen sier, men det er vårt valg - vi ga at implementeringen så vi tar risikoen).

For å oppsummere denne delen: Vi skal aldri skille jobben vår helt fra klientens jobb. Vi bør alltid tenke på hva som skjer med produksjonen vi lager. I likhet med at en lastebilsjåfør skal hjelpe til med å pakke ut varene i stedet for bare å kaste dem ut ved ankomst til destinasjonen.

3. Grensesnitt

Bruk alltid grensesnitt. Brukeren skal kun samhandle med koden vår gjennom strenge kontrakter.

For eksempel i jcabi-github biblioteket klassen RtGithub si den eneste brukeren faktisk ser:

Repo repo = new RtGithub ("oauth_token"). Repos (). Get (new Coordinates.Simple ("eugenp / tutorials")); Issue issue = repo.issues () .create ("Eksempelutgave", "Laget med jcabi-github");

Ovennevnte utdrag oppretter en billett i eugenp / tutorials repo. Forekomster av repo og utgave brukes, men de faktiske typene blir aldri avslørt. Vi kan ikke gjøre noe slikt:

Repo repo = ny RtRepo (...)

Ovennevnte er ikke mulig av en logisk grunn: vi kan ikke direkte lage et problem i en Github-repo, kan vi? Først må vi logge inn, deretter søke i repoen, og bare da kan vi opprette et problem. Selvfølgelig kan scenariet ovenfor være tillatt, men da vil brukerens kode bli forurenset med mye kokeplatekode: at RtRepo vil sannsynligvis måtte ta en slags autorisasjonsobjekt gjennom konstruktøren, autorisere klienten og komme til riktig repo etc.

Grensesnitt gir også enkel utvidbarhet og bakoverkompatibilitet. På den ene siden er vi som utviklere nødt til å respektere allerede utgitte kontrakter, og på den andre siden kan brukeren utvide grensesnittene vi tilbyr - han kan dekorere dem eller skrive alternative implementeringer.

Med andre ord, abstrakt og innkapsling så mye som mulig. Ved å bruke grensesnitt kan vi gjøre dette på en elegant og ikke-restriktiv måte - vi håndhever arkitektoniske regler mens vi gir programmereren frihet til å forbedre eller endre oppførselen vi utsetter.

For å avslutte denne delen, bare husk: biblioteket vårt, reglene våre. Vi burde vite nøyaktig hvordan klientens kode kommer til å se ut og hvordan han skal enhetstest den. Hvis vi ikke vet det, vil ingen og biblioteket vårt bare bidra til å lage kode som er vanskelig å forstå og vedlikeholde.

4. Tredjeparter

Husk at et godt bibliotek er et lett bibliotek. Koden din kan løse et problem og være funksjonell, men hvis glasset legger til 10 MB til byggingen min, er det klart at du mistet tegningene av prosjektet for lenge siden. Hvis du trenger mange avhengigheter, prøver du sannsynligvis å dekke for mye funksjonalitet og bør dele prosjektet opp i flere mindre prosjekter.

Vær så gjennomsiktig som mulig, ikke bind deg til faktiske implementeringer når det er mulig. Det beste eksemplet som kommer opp i tankene dine er: bruk SLF4J, som bare er et API for logging - ikke bruk log4j direkte, kanskje brukeren vil bruke andre loggere.

Dokumentbiblioteker som kommer gjennom prosjektet transitt og sørg for at du ikke inkluderer farlige avhengigheter som xalan eller xml-apis (hvorfor de er farlige er ikke for denne artikkelen å utdype).

Hovedpoenget her er: Hold bygningen lys, gjennomsiktig og vet alltid hva du jobber med. Det kan spare brukerne mer mas enn du kunne forestille deg.

5. Konklusjon

Artikkelen skisserer noen få enkle ideer som kan hjelpe et prosjekt å holde seg oppdatert når det gjelder brukervennlighet. Et bibliotek, som er en komponent som skal finne sin plass i en større sammenheng, bør være kraftig i funksjonalitet, men tilby et jevnt og godt utformet grensesnitt.

Det er et enkelt skritt over linjen og gjør et rot ut av designet. Bidragsyterne vil alltid vite hvordan de skal bruke den, men noen nye som først ser på det, kan ikke. Produktivitet er det viktigste av alt, og etter dette prinsippet bør brukerne kunne begynne å bruke et bibliotek i løpet av få minutter.


$config[zx-auto] not found$config[zx-overlay] not found