Een gebruiksvriendelijke Java-bibliotheek ontwerpen

1. Overzicht

Java is een van de pijlers van de open-sourcewereld. Bijna elk Java-project maakt gebruik van andere open-sourceprojecten, aangezien niemand het wiel opnieuw wil uitvinden. Het komt echter vaak voor dat we een bibliotheek nodig hebben voor zijn functionaliteit, maar we hebben geen idee hoe we deze moeten gebruiken. We komen dingen tegen als:

  • Wat is er met al deze "* Service" -lessen?
  • Hoe kan ik dit instantiëren, er zijn te veel afhankelijkheden voor nodig. Wat is een "klink“?
  • Oh, ik heb het in elkaar gezet, maar nu begint het te gooien IllegalStateException. Wat doe ik verkeerd?

Het probleem is dat niet alle bibliotheekontwerpers aan hun gebruikers denken. De meesten denken alleen aan functionaliteit en functies, maar weinigen denken na over hoe de API in de praktijk zal worden gebruikt en hoe de code van de gebruikers eruit zal zien en getest zal worden.

Dit artikel bevat een paar adviezen over hoe we onze gebruikers een aantal van deze problemen kunnen besparen - en nee, het is niet door documentatie te schrijven. Natuurlijk zou er een heel boek over dit onderwerp kunnen worden geschreven (en een paar zijn dat ook geweest); dit zijn enkele van de belangrijkste punten die ik heb geleerd toen ik zelf aan verschillende bibliotheken werkte.

Ik zal de ideeën hier illustreren met behulp van twee bibliotheken: charles en jcabi-github

2. Grenzen

Dit zou duidelijk moeten zijn, maar vaak is het dat niet. Voordat we beginnen met het schrijven van een regel code, moeten we een duidelijk antwoord hebben op enkele vragen: welke invoer is nodig? wat is de eerste klas die mijn gebruiker zal zien? hebben we implementaties van de gebruiker nodig? wat is de output? Zodra deze vragen duidelijk zijn beantwoord, wordt alles gemakkelijker omdat de bibliotheek al een voering heeft, een vorm.

2.1. Invoer

Dit is misschien wel het belangrijkste onderwerp. We moeten ervoor zorgen dat het duidelijk is wat de gebruiker aan de bibliotheek moet verstrekken om zijn werk te kunnen doen. In sommige gevallen is dit een heel triviale zaak: het kan gewoon een string zijn die het auth-token voor een API vertegenwoordigt, maar het kan ook een implementatie van een interface zijn, of een abstracte klasse.

Een zeer goede gewoonte is om alle afhankelijkheden door constructors te halen en deze kort te houden, met een paar parameters. Als we een constructor nodig hebben met meer dan drie of vier parameters, dan moet de code duidelijk geherstructureerd worden. En als methoden worden gebruikt om verplichte afhankelijkheden te injecteren, zullen de gebruikers hoogstwaarschijnlijk eindigen met de derde frustratie die in het overzicht wordt beschreven.

We moeten ook altijd meer dan één constructor aanbieden, gebruikers alternatieven geven. Laat ze er allebei mee werken Draad en Geheel getal of beperk ze niet tot een FileInputStream, werk met een InputStream, zodat ze misschien kunnen indienen ByteArrayInputStream wanneer unit testen etc.

Hier zijn bijvoorbeeld een paar manieren waarop we een Github API-toegangspunt kunnen instantiëren met jcabi-github:

Github noauth = nieuwe RtGithub (); Github basicauth = nieuwe RtGithub ("gebruikersnaam", "wachtwoord"); Github oauth = nieuwe RtGithub ("token"); 

Eenvoudig, geen gedoe, geen louche configuratieobjecten om te initialiseren. En het is logisch om deze drie constructeurs te hebben, omdat u de Github-website kunt gebruiken terwijl u bent uitgelogd, ingelogd of een app zich namens u kan authenticeren. Natuurlijk werken sommige functies niet als u niet geauthenticeerd bent, maar dat weet u vanaf het begin.

Als tweede voorbeeld, hier is hoe we zouden werken met Charles, een webcrawling-bibliotheek:

WebDriver-stuurprogramma = nieuwe FirefoxDriver (); Repository repo = nieuwe InMemoryRepository (); String indexPage = "//www.amihaiemil.com/index.html"; WebCrawl-grafiek = nieuwe GraphCrawl (indexPage, driver, nieuwe IgnoredPatterns (), repo); graph.crawl (); 

Het is ook vrij duidelijk, geloof ik. Terwijl ik dit schrijf, realiseer ik me echter dat er in de huidige versie een fout is: alle constructors vereisen dat de gebruiker een exemplaar van Genegeerde patronen. Standaard mogen geen patronen worden genegeerd, maar de gebruiker hoeft dit niet op te geven. Ik besloot het hier zo te laten, dus je ziet een tegenvoorbeeld. Ik neem aan dat je zou proberen een WebCrawl te instantiëren en je afvraagt: “Wat is daarmee Genegeerde patronen?!”

Variabele indexPage is de URL van waar de crawl zou moeten beginnen, driver is de browser die moet worden gebruikt (kan niets standaard instellen omdat we niet weten welke browser op de draaiende machine is geïnstalleerd). De repo-variabele wordt hieronder in de volgende sectie uitgelegd.

Dus, zoals je in de voorbeelden ziet, probeer het simpel, intuïtief en vanzelfsprekend te houden. Kaps logica en afhankelijkheden zo in dat de gebruiker zijn hoofd niet krabt als hij naar je constructeurs kijkt.

Twijfel je nog steeds, probeer dan HTTP-verzoeken aan AWS te doen met aws-sdk-java: je krijgt dan te maken met een zogenaamde AmazonHttpClient, die ergens een ClientConfiguration gebruikt, en dan ergens tussenin een ExecutionContext moet nemen. Ten slotte kunt u uw verzoek uitvoeren en een antwoord krijgen, maar u hebt nog steeds geen idee wat bijvoorbeeld een ExecutionContext is.

2.2. Uitvoer

Dit is meestal voor bibliotheken die communiceren met de buitenwereld. Hier moeten we de vraag beantwoorden "hoe wordt de output behandeld?". Nogmaals, een nogal grappige vraag, maar het is gemakkelijk om een ​​verkeerde keuze te maken.

Kijk nogmaals naar de bovenstaande code. Waarom moeten we een Repository-implementatie bieden? Waarom retourneert de methode WebCrawl.crawl () niet alleen een lijst met webpagina-elementen? Het is duidelijk niet de taak van de bibliotheek om met de gecrawlde pagina's om te gaan. Hoe moet het überhaupt weten wat we ermee zouden willen doen? Iets zoals dit:

WebCrawl-grafiek = nieuwe GraphCrawl (...); Lijstpagina's = graph.crawl (); 

Niets is erger. Een OutOfMemory-uitzondering kan zomaar gebeuren als de gecrawlde site bijvoorbeeld 1000 pagina's heeft - de bibliotheek laadt ze allemaal in het geheugen. Hier zijn twee oplossingen voor:

  • Blijf de pagina's terugsturen, maar implementeer een of ander semafoonmechanisme waarbij de gebruiker het begin- en eindnummer moet opgeven. Of
  • Vraag de gebruiker om een ​​interface te implementeren met een methode genaamd export (List), die het algoritme elke keer aanroept als een maximum aantal pagina's wordt bereikt

De tweede optie is verreweg de beste; het houdt de zaken aan beide kanten eenvoudiger en is beter testbaar. Bedenk hoeveel logica er aan de kant van de gebruiker zou moeten worden geïmplementeerd als we voor de eerste zouden gaan. Op deze manier wordt een repository voor pagina's gespecificeerd (om ze in een database te sturen of ze misschien op schijf te schrijven) en hoeft er niets anders te worden gedaan na het aanroepen van methode crawl ().

Trouwens, de code uit de sectie Input hierboven is alles wat we moeten schrijven om de inhoud van de website opgehaald te krijgen (nog steeds in het geheugen, zoals de repo-implementatie zegt, maar het is onze keuze - we hebben die implementatie dus wij nemen het risico).

Om deze paragraaf samen te vatten: we mogen onze baan nooit volledig scheiden van die van de klant. We moeten altijd nadenken over wat er gebeurt met de output die we creëren. Net zoals een vrachtwagenchauffeur zou moeten helpen bij het uitpakken van de goederen in plaats van ze bij aankomst op de bestemming gewoon weg te gooien.

3. Interfaces

Gebruik altijd interfaces. De gebruiker mag alleen met onze code communiceren via strikte contracten.

Bijvoorbeeld in de jcabi-github bibliotheek de klasse RtGithub is de enige die de gebruiker daadwerkelijk ziet:

Repo repo = nieuwe RtGithub ("oauth_token"). Repos (). Get (nieuwe Coordinates.Simple ("eugenp / tutorials")); Probleemprobleem = repo.issues () .create ("Voorbeeldprobleem", "Gemaakt met jcabi-github");

Het bovenstaande fragment maakt een ticket aan in de eugenp / tutorials-repo. Instanties van Repo en Issue worden gebruikt, maar de daadwerkelijke typen worden nooit onthuld. We kunnen zoiets niet doen:

Repo repo = nieuwe RtRepo (...)

Het bovenstaande is om een ​​logische reden niet mogelijk: we kunnen niet direct een probleem in een Github-repo creëren, toch? Eerst moeten we inloggen, dan zoeken in de opslagplaats en alleen dan kunnen we een probleem creëren. Natuurlijk zou het bovenstaande scenario kunnen worden toegestaan, maar dan zou de gebruikerscode vervuild raken met veel standaardcode: dat RtRepo zou waarschijnlijk een soort autorisatie-object via de constructor moeten nemen, de client autoriseren en naar de juiste repo moeten gaan, enz.

Interfaces bieden ook gemak van uitbreidbaarheid en achterwaartse compatibiliteit. Aan de ene kant moeten wij als ontwikkelaars de reeds vrijgegeven contracten respecteren en aan de andere kant kan de gebruiker de interfaces die we aanbieden uitbreiden - hij kan ze decoreren of alternatieve implementaties schrijven.

Met andere woorden, zoveel mogelijk abstract en ingekapseld. Door interfaces te gebruiken, kunnen we dit op een elegante en niet-beperkende manier doen - we dwingen architecturale regels af terwijl we de programmeur de vrijheid geven om het gedrag dat we blootleggen te verbeteren of te veranderen.

Om dit gedeelte te beëindigen, moet u in gedachten houden: onze bibliotheek, onze regels. We moeten precies weten hoe de code van de klant eruit zal zien en hoe hij deze gaat testen. Als we dat niet weten, zal niemand dat doen en zal onze bibliotheek eenvoudigweg bijdragen aan het creëren van code die moeilijk te begrijpen en te onderhouden is.

4. Derden

Houd er rekening mee dat een goede bibliotheek een lichtgewicht bibliotheek is. Uw code lost misschien een probleem op en is functioneel, maar als de jar 10 MB aan mijn build toevoegt, is het duidelijk dat u de blauwdrukken van uw project lang geleden bent kwijtgeraakt. Als je veel afhankelijkheden nodig hebt, probeer je waarschijnlijk te veel functionaliteit af te dekken en zou je het project in meerdere kleinere projecten moeten opsplitsen.

Wees zo transparant mogelijk, bindt waar mogelijk niet aan daadwerkelijke implementaties. Het beste voorbeeld dat in je opkomt is: gebruik SLF4J, wat alleen een API is voor logboekregistratie - gebruik log4j niet rechtstreeks, misschien wil de gebruiker andere loggers gebruiken.

Documentbibliotheken die tijdelijk door uw project komen en ervoor zorgen dat u geen gevaarlijke afhankelijkheden opneemt, zoals xalan of xml-apis (waarom ze gevaarlijk zijn, is niet aan dit artikel om uit te werken).

Waar het op neerkomt is: houd uw build licht, transparant en weet altijd waar u mee werkt. Het kan uw gebruikers meer drukte besparen dan u zich kunt voorstellen.

5. Conclusie

Het artikel schetst een paar eenvoudige ideeën die een project kunnen helpen om op het gebied van bruikbaarheid op het spel te blijven. Een bibliotheek, die een onderdeel is dat zijn plaats in een grotere context zou moeten vinden, moet krachtig zijn in functionaliteit en toch een soepele en goed ontworpen interface bieden.

Het is een makkelijke stap over de lijn en maakt een zooitje van het ontwerp. De bijdragers zullen altijd weten hoe ze het moeten gebruiken, maar iemand die het voor het eerst ziet, zal het misschien niet doen. Productiviteit is het allerbelangrijkste en volgens dit principe moeten de gebruikers binnen enkele minuten een bibliotheek kunnen gebruiken.