Progettazione di una libreria Java intuitiva

1. Panoramica

Java è uno dei pilastri del mondo open source. Quasi tutti i progetti Java utilizzano altri progetti open-source poiché nessuno vuole reinventare la ruota. Tuttavia, molte volte capita di aver bisogno di una libreria per le sue funzionalità ma non abbiamo la più pallida idea di come usarla. Ci imbattiamo in cose come:

  • Cos'è con tutte queste classi "* Servizio"?
  • Come creo un'istanza, ci vogliono troppe dipendenze. Cos'è un " latch "?
  • Oh, l'ho messo insieme, ma ora inizia a lanciare IllegalStateException . Che cosa sto facendo di sbagliato?

Il problema è che non tutti i progettisti di biblioteche pensano ai propri utenti. La maggior parte pensa solo alla funzionalità e alle caratteristiche, ma pochi considerano come l'API verrà utilizzata nella pratica e come apparirà e verrà testato il codice degli utenti.

Questo articolo viene fornito con alcuni consigli su come salvare i nostri utenti alcune di queste difficoltà e no, non è attraverso la scrittura di documentazione. Naturalmente, un intero libro potrebbe essere scritto su questo argomento (e alcuni lo sono stati); questi sono alcuni dei punti chiave che ho imparato lavorando personalmente su diverse biblioteche.

Esemplificherò le idee qui usando due librerie: charles e jcabi-github

2. Confini

Dovrebbe essere ovvio ma molte volte non lo è. Prima di iniziare a scrivere qualsiasi riga di codice, dobbiamo avere una risposta chiara ad alcune domande: quali input sono necessari? qual è la prima classe che vedrà il mio utente? abbiamo bisogno di implementazioni da parte dell'utente? qual è l'output? Una volta che a queste domande viene data una risposta chiara, tutto diventa più facile poiché la libreria ha già un rivestimento, una forma.

2.1. Ingresso

Questo è forse l'argomento più importante. Dobbiamo assicurarci che sia chiaro ciò che l'utente deve fornire alla libreria affinché possa svolgere il suo lavoro. In alcuni casi questa è una questione molto banale: potrebbe essere solo una stringa che rappresenta il token di autenticazione per un'API, ma potrebbe anche essere un'implementazione di un'interfaccia o una classe astratta.

Una buona pratica è prendere tutte le dipendenze tramite i costruttori e mantenerle brevi, con pochi parametri. Se abbiamo bisogno di un costruttore con più di tre o quattro parametri, il codice dovrebbe essere chiaramente rifattorizzato. E se vengono utilizzati metodi per iniettare dipendenze obbligatorie, molto probabilmente gli utenti finiranno con la terza frustrazione descritta nella panoramica.

Inoltre, dovremmo sempre offrire più di un costruttore, dare alternative agli utenti. Lascia che funzionino sia con String che Integer o non limitarli a un FileInputStream , lavorare con un InputStream , in modo che possano inviare forse ByteArrayInputStream durante unit test ecc.

Ad esempio, ecco alcuni modi in cui possiamo istanziare un punto di ingresso dell'API Github utilizzando jcabi-github:

Github noauth = new RtGithub(); Github basicauth = new RtGithub("username", "password"); Github oauth = new RtGithub("token"); 

Semplice, senza fretta, senza oggetti di configurazione ombreggiati da inizializzare. E ha senso avere questi tre costruttori, perché puoi utilizzare il sito Web Github mentre sei disconnesso, connesso o un'app può autenticarsi per tuo conto. Naturalmente, alcune funzionalità non funzioneranno se non sei autenticato, ma lo sai fin dall'inizio.

Come secondo esempio, ecco come lavoreremmo con charles, una libreria di scansione web:

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

È anche abbastanza autoesplicativo, credo. Tuttavia, mentre scrivo questo, mi rendo conto che nella versione corrente c'è un errore: tutti i costruttori richiedono all'utente di fornire un'istanza di IgnoredPatterns . Per impostazione predefinita, nessun modello dovrebbe essere ignorato, ma l'utente non dovrebbe doverlo specificare. Ho deciso di lasciarlo così qui, quindi vedi un controesempio. Presumo che proveresti a creare un'istanza di WebCrawl e ti chiedi "Che cos'è questo IgnoredPatterns ?!"

La variabile indexPage è l'URL da cui deve iniziare la scansione, il driver è il browser da utilizzare (non può essere impostato come predefinito poiché non sappiamo quale browser è installato sulla macchina in esecuzione). La variabile repo verrà spiegata di seguito nella sezione successiva.

Quindi, come vedi negli esempi, cerca di mantenerlo semplice, intuitivo e autoesplicativo. Incapsula la logica e le dipendenze in modo tale che l'utente non si gratti la testa quando guarda i tuoi costruttori.

Se hai ancora dubbi, prova a effettuare richieste HTTP ad AWS utilizzando aws-sdk-java: dovrai gestire un cosiddetto AmazonHttpClient, che utilizza una ClientConfiguration da qualche parte, quindi deve prendere un ExecutionContext da qualche parte nel mezzo. Infine, potresti eseguire la tua richiesta e ottenere una risposta, ma non hai ancora idea di cosa sia un ExecutionContext, ad esempio.

2.2. Produzione

Questo è principalmente per le biblioteche che comunicano con il mondo esterno. Qui dovremmo rispondere alla domanda "come verrà gestito l'output?". Di nuovo, una domanda piuttosto divertente, ma è facile sbagliare.

Guarda di nuovo il codice sopra. Perché dobbiamo fornire un'implementazione del repository? Perché il metodo WebCrawl.crawl () non restituisce semplicemente un elenco di elementi della pagina Web? Chiaramente non è compito della biblioteca gestire le pagine sottoposte a scansione. Come dovrebbe anche sapere cosa vorremmo fare con loro? Qualcosa come questo:

WebCrawl graph = new GraphCrawl(...); List pages = graph.crawl(); 

Niente potrebbe essere peggio. Un'eccezione OutOfMemory potrebbe verificarsi dal nulla se il sito sottoposto a scansione ha, diciamo, 1000 pagine: la libreria le carica tutte in memoria. Ci sono due soluzioni a questo:

  • Continua a restituire le pagine, ma implementa un meccanismo di paginazione in cui l'utente dovrebbe fornire i numeri di inizio e di fine. O
  • Chiedere all'utente di implementare un'interfaccia con un metodo chiamato export (List), che l'algoritmo chiamerebbe ogni volta che si raggiunge un numero massimo di pagine

La seconda opzione è di gran lunga la migliore; mantiene le cose più semplici su entrambi i lati ed è più testabile. Pensa quanta logica dovrebbe essere implementata dal lato dell'utente se andassimo con il primo. In questo modo, viene specificato un repository per le pagine (per inviarle in un DB o scriverle su disco forse) e nient'altro deve essere fatto dopo aver chiamato il metodo crawl ().

A proposito, il codice dalla sezione Input sopra è tutto ciò che dobbiamo scrivere per ottenere il contenuto del sito web recuperato (ancora in memoria, come dice l'implementazione del repo, ma è una nostra scelta - abbiamo fornito quell'implementazione così ci assumiamo il rischio).

Per riassumere questa sezione: non dovremmo mai separare completamente il nostro lavoro da quello del cliente. Dobbiamo sempre pensare a cosa succede con l'output che creiamo. Proprio come un camionista dovrebbe aiutare a disimballare le merci piuttosto che semplicemente buttarle fuori all'arrivo a destinazione.

3. Interfacce

Usa sempre le interfacce. L'utente dovrebbe interagire con il nostro codice solo attraverso contratti rigidi.

Ad esempio, nella libreria jcabi-github la classe RtGithub è l'unica che l'utente vede effettivamente:

Repo repo = new RtGithub("oauth_token").repos().get( new Coordinates.Simple("eugenp/tutorials")); Issue issue = repo.issues() .create("Example issue", "Created with jcabi-github");

Lo snippet sopra crea un ticket nel repository eugenp / tutorials. Vengono utilizzate istanze di Repo e Issue, ma i tipi effettivi non vengono mai rivelati. Non possiamo fare qualcosa del genere:

Repo repo = new RtRepo(...)

The above is not possible for a logical reason: we cannot directly create an issue in a Github repo, can we? First, we have to login, then search the repo and only then we can create an issue. Of course, the scenario above could be allowed, but then the user's code would become polluted with a lot of boilerplate code: that RtRepo would probably have to take some kind of authorization object through its constructor, authorize the client and get to the right repo etc.

Interfaces also provide ease of extensibility and backward-compatibility. On one hand, we as developers are bound to respect the already released contracts and on the other, the user can extend the interfaces we offer – he might decorate them or write alternative implementations.

In other words, abstract and encapsulate as much as possible. By using interfaces we can do this in an elegant and non-restrictive manner – we enforce architectural rules while giving the programmer freedom to enhance or change the behaviour we expose.

To end this section, just keep in mind: our library, our rules. We should know exactly how the client's code is going to look like and how he's going to unit test it. If we do not know that, no one will and our library will simply contribute in creating code that is hard to understand and maintain.

4. Third Parties

Keep in mind that a good library is a light-weight library. Your code might solve an issue and be functional, but if the jar adds 10 MB to my build, then it's clear that you lost the blueprints of your project a long time ago. If you need a lot of dependencies you are probably trying to cover too much functionality and should break the project into multiple smaller projects.

Be as transparent as possible, whenever possible do not bind to actual implementations. The best example that comes to mind is: use SLF4J, which is only an API for logging – do not use log4j directly, maybe the user would like to use other loggers.

Document libraries that come through your project transitively and make sure you don't include dangerous dependencies such as xalan or xml-apis (why they are dangerous is not for this article to elaborate).

Bottom line here is: keep your build light, transparent and always know what you are working with. It could save your users more hustle than you could imagine.

5. Conclusion

The article outlines a few simple ideas that can help a project stay on the line with regards to usability. A library, being a component that should find its place in a bigger context, should be powerful in functionality yet offer a smooth and well-crafted interface.

È un facile passaggio oltre la linea e crea un disastro nel design. I contributori sapranno sempre come usarlo, ma qualcuno di nuovo che lo vede per primo potrebbe non farlo. La produttività è la più importante di tutte e seguendo questo principio, gli utenti dovrebbero essere in grado di iniziare a utilizzare una libreria in pochi minuti.