Entwerfen einer benutzerfreundlichen Java-Bibliothek

1. Übersicht

Java ist eine der Säulen der Open-Source-Welt. Fast jedes Java-Projekt verwendet andere Open-Source-Projekte, da niemand das Rad neu erfinden möchte. Es kommt jedoch häufig vor, dass wir eine Bibliothek für ihre Funktionalität benötigen, aber wir haben keine Ahnung, wie wir sie verwenden sollen. Wir stoßen auf Dinge wie:

  • Was ist mit all diesen "* Service" -Klassen?
  • Wie instanziiere ich das, es braucht zu viele Abhängigkeiten. Was ist ein „ Riegel “?
  • Oh, ich habe es zusammengestellt, aber jetzt wird IllegalStateException ausgelöst . Was mache ich falsch?

Das Problem ist, dass nicht alle Bibliotheksdesigner an ihre Benutzer denken. Die meisten denken nur an Funktionen und Features, aber nur wenige überlegen, wie die API in der Praxis verwendet wird und wie der Code der Benutzer aussehen und getestet wird.

Dieser Artikel enthält einige Ratschläge, wie wir unseren Benutzern einige dieser Probleme ersparen können - und nein, es geht nicht darum, Dokumentation zu schreiben. Natürlich könnte ein ganzes Buch zu diesem Thema geschrieben werden (und einige waren es auch); Dies sind einige der wichtigsten Punkte, die ich bei der Arbeit an mehreren Bibliotheken selbst gelernt habe.

Ich werde die Ideen hier anhand von zwei Bibliotheken veranschaulichen: Charles und JCABI-Github

2. Grenzen

Dies sollte offensichtlich sein, ist es aber oft nicht. Bevor wir mit dem Schreiben einer Codezeile beginnen, müssen wir einige Fragen klar beantworten: Welche Eingaben werden benötigt? Was ist die erste Klasse, die mein Benutzer sehen wird? Benötigen wir Implementierungen vom Benutzer? Was ist die Ausgabe? Sobald diese Fragen klar beantwortet sind, wird alles einfacher, da die Bibliothek bereits ein Futter, eine Form hat.

2.1. Eingang

Dies ist vielleicht das wichtigste Thema. Wir müssen sicherstellen, dass klar ist, was der Benutzer der Bibliothek zur Verfügung stellen muss, damit sie ihre Arbeit erledigen kann. In einigen Fällen ist dies eine sehr triviale Angelegenheit: Es kann sich nur um eine Zeichenfolge handeln, die das Authentifizierungstoken für eine API darstellt, es kann sich jedoch auch um eine Implementierung einer Schnittstelle oder einer abstrakten Klasse handeln.

Eine sehr gute Praxis ist es, alle Abhängigkeiten durch Konstruktoren zu ziehen und diese mit wenigen Parametern kurz zu halten. Wenn wir einen Konstruktor mit mehr als drei oder vier Parametern benötigen, sollte der Code eindeutig überarbeitet werden. Und wenn Methoden verwendet werden, um obligatorische Abhängigkeiten einzufügen, werden die Benutzer höchstwahrscheinlich mit der dritten Frustration enden, die in der Übersicht beschrieben wird.

Außerdem sollten wir immer mehr als einen Konstruktor anbieten und den Benutzern Alternativen bieten. Lassen Sie sie sowohl mit String als auch mit Integer arbeiten oder beschränken Sie sie nicht auf einen FileInputStream , sondern mit einem InputStream , damit sie beim Unit-Test möglicherweise ByteArrayInputStream usw. senden können .

Im Folgenden finden Sie einige Möglichkeiten, wie Sie einen Github-API-Einstiegspunkt mithilfe von jcabi-github instanziieren können:

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

Einfach, keine Hektik, keine schattigen Konfigurationsobjekte zum Initialisieren. Diese drei Konstruktoren sind sinnvoll, da Sie die Github-Website verwenden können, während Sie abgemeldet oder angemeldet sind oder eine App sich in Ihrem Namen authentifizieren kann. Natürlich funktionieren einige Funktionen nicht, wenn Sie nicht authentifiziert sind, aber Sie wissen dies von Anfang an.

Als zweites Beispiel würden wir wie folgt mit Charles arbeiten, einer Web-Crawler-Bibliothek:

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(); 

Es ist auch ziemlich selbsterklärend, glaube ich. Beim Schreiben stelle ich jedoch fest, dass in der aktuellen Version ein Fehler vorliegt : Bei allen Konstruktoren muss der Benutzer eine Instanz von IgnoredPatterns angeben . Standardmäßig sollten keine Muster ignoriert werden, aber der Benutzer sollte dies nicht angeben müssen. Ich habe beschlossen, es hier so zu lassen, also sehen Sie ein Gegenbeispiel. Ich gehe davon aus, dass Sie versuchen würden, ein WebCrawl zu instanziieren und sich fragen: "Was ist mit diesen IgnoredPatterns ?!"

Die Variable indexPage ist die URL, unter der der Crawl beginnen soll. Der Treiber ist der zu verwendende Browser (kann standardmäßig nichts verwenden, da wir nicht wissen, welcher Browser auf dem laufenden Computer installiert ist). Die Repo-Variable wird im nächsten Abschnitt erläutert.

Versuchen Sie also, wie Sie in den Beispielen sehen, es einfach, intuitiv und selbsterklärend zu halten. Kapselung von Logik und Abhängigkeiten so, dass sich der Benutzer beim Betrachten Ihrer Konstruktoren nicht am Kopf kratzt.

Wenn Sie immer noch Zweifel haben, versuchen Sie, HTTP-Anforderungen mit aws-sdk-java an AWS zu senden: Sie müssen sich mit einem sogenannten AmazonHttpClient befassen, der irgendwo eine ClientConfiguration verwendet, und dann irgendwo dazwischen einen ExecutionContext verwenden. Schließlich können Sie möglicherweise Ihre Anfrage ausführen und eine Antwort erhalten, haben aber immer noch keine Ahnung, was beispielsweise ein ExecutionContext ist.

2.2. Ausgabe

Dies gilt hauptsächlich für Bibliotheken, die mit der Außenwelt kommunizieren. Hier sollten wir die Frage beantworten, wie die Ausgabe behandelt wird. Wieder eine ziemlich lustige Frage, aber es ist leicht, einen Fehler zu machen.

Look again at the code above. Why do we have to provide a Repository implementation? Why doesn't the method WebCrawl.crawl() just return a list of WebPage elements? It's clearly not the library's job to handle the crawled pages. How should it even know what we would like to do with them? Something like this:

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

Nothing could be worse. An OutOfMemory exception could happen out of nowhere if the crawled site happens to have, let's say, 1000 pages – the library loads them all in memory. There are two solutions to this:

  • Keep returning the pages, but implement some paging mechanism in which the user would have to supply the start and end numbers. Or
  • Ask the user to implement an interface with a method called export(List), that the algorithm would call every time a max number of pages would be reached

The second option is by far the best; it keeps things simpler on both sides and is more testable. Think how much logic would have to be implemented on the user's side if we went with the first. Like this, a Repository for pages is specified (to send them in a DB or write them on disk maybe) and nothing else has to be done after calling method crawl().

By the way, the code from the Input section above is everything that we have to write in order to get the contents of the website fetched (still in memory, as the repo implementation says, but it is our choice – we provided that implementation so we take the risk).

To summarize this section: we should never completely separate our job from the client's job. We should always think what happens with the output we create. Much like a truck driver should help with unpacking the goods rather than simply throwing them out upon arrival at the destination.

3. Interfaces

Always use interfaces. The user should interact with our code only through strict contracts.

For example, in the jcabi-github library the class RtGithub si the only one the user actually sees:

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

The above snippet creates a ticket in the eugenp/tutorials repo. Instances of Repo and Issue are used, but the actual types are never revealed. We cannot do something like this:

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.

Es ist ein einfacher Schritt über die Linie und macht das Design durcheinander. Die Mitwirkenden werden immer wissen, wie man es benutzt, aber jemand, der es zuerst sieht, könnte es nicht. Produktivität ist das Wichtigste von allen. Nach diesem Prinzip sollten die Benutzer in wenigen Minuten in der Lage sein, eine Bibliothek zu verwenden.