Dizajniranje user-friendly Java knjižnice

1. Pregled

Java je jedan od stupova svijeta otvorenog koda. Gotovo svaki Java projekt koristi druge projekte otvorenog koda, jer nitko ne želi izmisliti kotač. Međutim, mnogo puta se dogodi da nam knjižnica treba za njezinu funkcionalnost, ali nemamo pojma kako je koristiti. Nailazimo na stvari poput:

  • Što je sa svim tim tečajevima "* Service"?
  • Kako to mogu napraviti, potrebno je previše ovisnosti. Što je "zasun“?
  • Oh, složio sam ga, ali sad počinje bacanje IllegalStateException. Što radim krivo?

Nevolja je u tome što ne misle svi dizajneri knjižnica o svojim korisnicima. Većina razmišlja samo o funkcionalnosti i značajkama, ali malo tko razmišlja o tome kako će se API koristiti u praksi i kako će izgledati i testirati se korisnički kod.

Ovaj članak donosi nekoliko savjeta o tome kako spasiti naše korisnike neke od tih borbi - i ne, to nije kroz pisanje dokumentacije. Naravno, na ovu bi temu mogla biti napisana cijela knjiga (a nekoliko ih je i bilo); ovo su neke od ključnih točaka koje sam naučio radeći u nekoliko knjižnica.

Primjere ću ovdje dati pomoću dvije knjižnice: charles i jcabi-github

2. Granice

To bi trebalo biti očito, ali puno puta nije. Prije početka pisanja bilo kojeg retka koda, moramo imati jasan odgovor na neka pitanja: koji su ulazi potrebni? koju će prvu klasu vidjeti moj korisnik? trebamo li implementacije od korisnika? koliki je izlaz? Jednom kada se na ova pitanja jasno odgovori, sve postaje lakše jer knjižnica već ima oblogu, oblik.

2.1. Ulazni

Ovo je možda najvažnija tema. Moramo biti sigurni da je jasno što korisnik knjižnici mora pružiti kako bi mogla obavljati svoj posao. U nekim je slučajevima ovo vrlo trivijalna stvar: to može biti samo String koji predstavlja oznaku identiteta za API, ali to može biti i implementacija sučelja ili apstraktne klase.

Vrlo dobra praksa je provesti sve ovisnosti kroz konstruktore i držati ih kratkim, s nekoliko parametara. Ako trebamo imati konstruktor s više od tri ili četiri parametra, tada bi se kod trebao očito prepraviti. A ako se metode koriste za ubrizgavanje obveznih ovisnosti, korisnici će najvjerojatnije završiti s trećom frustracijom opisanom u pregledu.

Također, uvijek bismo trebali ponuditi više od jednog konstruktora, korisnicima dati alternative. Neka rade s oboje Niz i Cijeli broj ili ih ne ograničavajte na a FileInputStream, radite s InputStream, tako da mogu poslati možda ByteArrayInputStream kod jediničnog ispitivanja itd.

Na primjer, evo nekoliko načina na koje možemo instancirati ulaznu točku API-ja Github pomoću jcabi-github:

Github noauth = novi RtGithub (); Github basicauth = novi RtGithub ("korisničko ime", "lozinka"); Github oauth = novi RtGithub ("token"); 

Jednostavno, bez gužve, bez sumnjivih konfiguracijskih objekata za inicijalizaciju. I ima smisla imati ova tri konstruktora, jer možete koristiti web mjesto Github dok ste odjavljeni, prijavljeni ili aplikacija može autentificirati u vaše ime. Prirodno, neke funkcije neće raditi ako niste provjereni, ali to znate od početka.

Kao drugi primjer, evo kako bismo radili s charlesom, knjižnicom za indeksiranje weba:

Upravljački program za WebDriver = novi FirefoxDriver (); Repozitorij repo = novo InMemoryRepository (); String indexPage = "//www.amihaiemil.com/index.html"; Grafikon WebCrawl = novi GraphCrawl (indexPage, pokretački program, novi IgnoredPatterns (), repo); graph.crawl (); 

Vjerujem da je to i samo po sebi razumljivo. Međutim, dok pišem ovo, shvaćam da u trenutnoj verziji postoji pogreška: svi konstruktori zahtijevaju od korisnika da navede instancu IgnoredPatterns. Prema zadanim postavkama, nijedan obrazac ne smije se zanemariti, ali korisnik to ne bi trebao navesti. Odlučio sam ostaviti ovdje ovako, pa vidite kontra primjer. Pretpostavljam da biste pokušali pokrenuti WebCrawl i zapitati se „Što je s tim IgnoredPatterns?!”

Varijabla indexPage je URL odakle bi indeksiranje trebalo započeti, a pokretački program je preglednik koji se koristi (ne može se zadati ni za što jer ne znamo koji je preglednik instaliran na pokrenutom stroju). Repo varijabla bit će objašnjena u nastavku u sljedećem odjeljku.

Dakle, kao što vidite u primjerima, pokušajte biti jednostavnim, intuitivnim i samorazumljivim. Inkapsulirajte logiku i ovisnosti na takav način da se korisnik ne ogrebe po glavi dok gleda vaše konstruktore.

Ako i dalje sumnjate, pokušajte uputiti HTTP zahtjeve AWS-u pomoću aws-sdk-java: morat ćete se suočiti s takozvanim AmazonHttpClientom, koji negdje koristi ClientConfiguration, a zatim negdje između treba uzeti ExecutionContext. Konačno, možda ćete moći izvršiti svoj zahtjev i dobiti odgovor, ali još uvijek nemate pojma što je, na primjer, ExecutionContext.

2.2. Izlaz

To je uglavnom za knjižnice koje komuniciraju s vanjskim svijetom. Ovdje bismo trebali odgovoriti na pitanje "kako će se postupati s izlazom?". Opet, prilično smiješno pitanje, ali lako je pogriješiti.

Ponovno pogledajte kod gore. Zašto moramo osigurati implementaciju spremišta? Zašto metoda WebCrawl.crawl () ne vrati samo popis elemenata WebPagea? Očito nije posao knjižnice da obrađuje indeksirane stranice. Kako bi uopće mogao znati što bismo htjeli raditi s njima? Nešto kao ovo:

Grafikon WebCrawl = novi GraphCrawl (...); Stranice popisa = graph.crawl (); 

Ništa ne može biti gore. Iznimka OutOfMemory mogla bi se dogoditi niotkuda ako slučajno indeksirano mjesto ima, recimo, 1000 stranica - knjižnica ih sve učita u memoriju. Postoje dva rješenja za to:

  • Stalno vraćajte stranice, ali implementirajte neki mehanizam za pozivanje u kojem bi korisnik trebao unijeti početni i završni broj. Ili
  • Zamolite korisnika da implementira sučelje s metodom koja se naziva izvoz (Popis), kako bi algoritam pozivao svaki put kad bi se postigao maksimalan broj stranica

Druga je opcija daleko najbolja; olakšava stvari s obje strane i provjerljivije je. Razmislite koliko bi logike trebalo implementirati na strani korisnika ako bismo pošli s prvim. Kao ovaj, specificirano je Spremište za stranice (da biste ih poslali u DB ili ih možda zapisali na disk) i ništa drugo ne treba učiniti nakon poziva metode crawl ().

Inače, kod iz gornjeg odjeljka Unos je sve što moramo napisati da bismo dobili sadržaj web stranice (još uvijek u memoriji, kako kaže repo implementacija, ali to je naš izbor - osigurali smo da je implementacija tako riskiramo).

Da rezimiramo ovaj odjeljak: nikada ne bismo trebali potpuno odvojiti svoj posao od posla klijenta. Uvijek bismo trebali razmišljati što se događa s izlazom koji stvaramo. Slično kao što bi vozač kamiona trebao pomoći pri raspakiranju robe, umjesto da je jednostavno baci po dolasku na odredište.

3. Sučelja

Uvijek koristite sučelja. Korisnik bi trebao komunicirati s našim kodom samo putem strogih ugovora.

Na primjer, u jcabi-github knjižnica klasa RtGithub je jedina koju korisnik zapravo vidi:

Repo repo = novi RtGithub ("oauth_token"). Repos (). Get (nove Koordinate.Simple ("eugenp / tutoriali")); Issue issue = repo.issues () .create ("Primjer izdanja", "Izrađeno pomoću jcabi-github");

Gornji isječak stvara ulaznicu u repou eugenp / tutorials. Koriste se slučajevi Repo i Issue, ali stvarni tipovi se nikad ne otkrivaju. Ne možemo učiniti nešto poput ovoga:

Repo repo = novi RtRepo (...)

Navedeno nije moguće iz logičnog razloga: ne možemo izravno stvoriti problem u Github repo-u, zar ne? Prvo se moramo prijaviti, zatim pretražiti repo i tek tada možemo stvoriti problem. Naravno, gornji scenarij bi mogao biti dopušten, ali tada bi korisnikov kod postao zagađen s puno šifri: RtRepo vjerojatno bi trebao uzeti neku vrstu autorizacijskog objekta kroz njegov konstruktor, autorizirati klijenta i doći do pravog repo-a itd.

Sučelja također pružaju lakoću proširivosti i kompatibilnost s unatrag. S jedne strane, mi kao programeri dužni smo poštovati već objavljene ugovore, a s druge strane, korisnik može proširiti sučelja koja nudimo - mogao bi ih ukrasiti ili napisati alternativne implementacije.

Drugim riječima, apstrahirajte i inkapsulirajte što je više moguće. Korištenjem sučelja to možemo učiniti na elegantan i ne restriktivan način - provodimo arhitektonska pravila, dok programeru dajemo slobodu da poboljša ili promijeni ponašanje koje izlažemo.

Da biste završili ovaj odjeljak, samo imajte na umu: naša knjižnica, naša pravila. Trebali bismo točno znati kako će izgledati klijentov kôd i kako će ga jedinica testirati. Ako to ne znamo, nitko neće, a naša će knjižnica jednostavno doprinijeti stvaranju koda koji je teško razumjeti i održavati.

4. Treće strane

Imajte na umu da je dobra knjižnica lagana knjižnica. Vaš kôd može riješiti problem i biti funkcionalan, ali ako staklenka doda 10 MB mojoj gradnji, onda je jasno da ste davno izgubili nacrte svog projekta. Ako vam treba puno ovisnosti, vjerojatno pokušavate pokriti previše funkcionalnosti i trebali biste projekt razbiti na više manjih projekata.

Budite što transparentniji, kad god je to moguće, ne vežite se za stvarne implementacije. Najbolji primjer koji vam padne na pamet je: koristite SLF4J, koji je samo API za bilježenje - ne koristite log4j izravno, možda bi korisnik želio koristiti druge zapisnike.

Biblioteke dokumenata koje prolaze kroz vaš projekt tranzitivno i paze da ne uključite opasne ovisnosti poput xalan ili xml-apis (zašto su opasni, ovaj članak ne razrađuje).

Dno crta ovdje je: neka vaša građa bude lagana, prozirna i uvijek znajte s čime radite. Korisnicima bi to moglo uštedjeti više gužve nego što ste mogli zamisliti.

5. Zaključak

Članak iznosi nekoliko jednostavnih ideja koje mogu pomoći projektu da ostane na liniji s obzirom na iskoristivost. Knjižnica, kao komponenta koja bi svoje mjesto trebala naći u širem kontekstu, trebala bi biti moćna u funkcionalnosti, a istovremeno ponuditi glatko i dobro izrađeno sučelje.

To je lagan korak preko crte i stvara nered u dizajnu. Suradnici će ga uvijek znati koristiti, ali netko novi tko prvi pogleda na njega možda neće. Produktivnost je najvažnija od svih i slijedeći ovo načelo, korisnici bi trebali moći početi koristiti knjižnicu u nekoliko minuta.