Entwerfen einer benutzerfreundlichen Java-Bibliothek

Entwerfen einer benutzerfreundlichen Java-Bibliothek

1. Überblick

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 dauert zu viele Abhängigkeiten. Was ist ein "latch"?

  • Oh, ich habe es zusammengestellt, aber jetzt wirft esIllegalStateException. 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 undjcabi-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 sind erforderlich? 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 eine Auskleidung, 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 lediglich um einen String handeln, der das Authentifizierungstoken für eine API darstellt, es kann sich jedoch auch um eine Implementierung einer Schnittstelle oder einer abstrakten Klasse handeln.

Es ist eine sehr gute Praxis, alle Abhängigkeiten durch Konstruktoren zu erfassen 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 die dritte Frustration haben, die in der Übersicht beschrieben wird.

Außerdem sollten wir immer mehr als einen Konstruktor anbieten, dem Benutzer Alternativen geben. Lassen Sie sie sowohl mitString als auch mitInteger arbeiten oder beschränken Sie sie nicht aufFileInputStream, arbeiten Sie mitInputStream, damit sie möglicherweiseByteArrayInputStream einreichen können, wenn Unit Testing etc.

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. Und es ist sinnvoll, diese drei Konstruktoren zu haben, 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 sehen Sie hier, wie wir mit Charles, einer Web-Crawler-Bibliothek, arbeiten:

WebDriver driver = new FirefoxDriver();
Repository repo = new InMemoryRepository();
String indexPage = "http://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 vonIgnoredPatterns angeben. Standardmäßig sollten keine Muster ignoriert werden, der Benutzer sollte dies jedoch nicht angeben müssen. Ich habe beschlossen, es hier so zu belassen, also siehst du ein Gegenbeispiel. Ich gehe davon aus, dass Sie versuchen würden, ein WebCrawl zu instanziieren und sich fragen: "Was ist mit diesenIgnoredPatterns?!"

Die Variable indexPage ist die URL, von der aus der Crawl gestartet werden soll, der Treiber ist der zu verwendende Browser (es kann kein Standardwert verwendet werden, da nicht bekannt ist, welcher Browser auf dem laufenden Computer installiert ist). Die Repo-Variable wird weiter unten 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 mitaws-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 Anforderung ausführen und eine Antwort erhalten, haben jedoch 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.

Schauen Sie sich den obigen Code noch einmal an. Warum müssen wir eine Repository-Implementierung bereitstellen? Warum gibt die Methode WebCrawl.crawl () nicht einfach eine Liste von WebPage-Elementen zurück? Es ist eindeutig nicht die Aufgabe der Bibliothek, die gecrawlten Seiten zu bearbeiten. Wie soll es überhaupt wissen, was wir mit ihnen machen möchten? Etwas wie das:

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

Nichts könnte schlimmer sein. Eine OutOfMemory-Ausnahme kann aus dem Nichts auftreten, wenn die gecrawlte Site beispielsweise 1000 Seiten umfasst - die Bibliothek lädt sie alle in den Speicher. Hierfür gibt es zwei Lösungen:

  • Geben Sie die Seiten weiterhin zurück, implementieren Sie jedoch einen Paging-Mechanismus, bei dem der Benutzer die Start- und Endnummern eingeben müsste. Or

  • Bitten Sie den Benutzer, eine Schnittstelle mit einer Methode namens export (List ) zu implementieren, die der Algorithmus jedes Mal aufruft, wenn eine maximale Anzahl von Seiten erreicht wird

Die zweite Option ist bei weitem die beste; es hält die Sache auf beiden Seiten einfacher und ist überprüfbarer. Überlegen Sie, wie viel Logik auf der Benutzerseite implementiert werden müsste, wenn wir uns für die erste entscheiden würden. Auf diese Weise wird ein Repository für Seiten angegeben (um sie möglicherweise in einer Datenbank zu senden oder auf die Festplatte zu schreiben), und nach dem Aufruf von method crawl () müssen keine weiteren Schritte ausgeführt werden.

Übrigens ist der Code aus dem obigen Abschnitt Input alles, was wir schreiben müssen, um den Inhalt der Website abzurufen (immer noch im Speicher, wie die Repo-Implementierung sagt, aber es ist unsere Wahl - wir haben diese Implementierung bereitgestellt wir gehen das Risiko ein).

Um diesen Abschnitt zusammenzufassen: Wir sollten unseren Job niemals vollständig vom Job des Kunden trennen. Wir sollten uns immer überlegen, was mit den von uns erstellten Ergebnissen passiert. Ähnlich wie ein Lkw-Fahrer sollte er beim Auspacken der Waren helfen, anstatt sie einfach bei der Ankunft am Bestimmungsort wegzuwerfen.

3. Schnittstellen

Verwenden Sie immer Schnittstellen. Der Benutzer sollte mit unserem Code nur durch strenge Verträge interagieren.

In der Bibliothekjcabi-github ist beispielsweise die Klasse RtGithub die einzige, die der Benutzer tatsächlich sieht:

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

Das obige Snippet erstellt ein Ticket ineugenp/tutorials repo. Es werden Instanzen von Repo und Issue verwendet, die tatsächlichen Typen werden jedoch nie bekannt gegeben. Wir können so etwas nicht machen:

Repo repo = new RtRepo(...)

Dies ist aus einem logischen Grund nicht möglich: Wir können ein Problem nicht direkt in einem Github-Repo erstellen, oder? Zuerst müssen wir uns einloggen, dann das Repo durchsuchen und erst dann können wir ein Problem erstellen. Natürlich könnte das obige Szenario erlaubt sein, aber dann würde der Code des Benutzers mit viel Boilerplate-Code verschmutzt:RtRepo müssten wahrscheinlich eine Art Autorisierungsobjekt durch seinen Konstruktor führen, den Client autorisieren und abrufen zum richtigen Repo etc.

Schnittstellen bieten auch einfache Erweiterbarkeit und Abwärtskompatibilität. Einerseits sind wir als Entwickler verpflichtet, die bereits freigegebenen Verträge zu respektieren, andererseits kann der Benutzer die von uns angebotenen Schnittstellen erweitern - er kann sie dekorieren oder alternative Implementierungen schreiben.

Mit anderen Worten, abstrahieren und kapseln Sie so viel wie möglich. Durch die Verwendung von Schnittstellen können wir dies auf elegante und nicht einschränkende Weise tun - wir setzen Architekturregeln durch und geben dem Programmierer die Freiheit, das von uns offen gelegte Verhalten zu verbessern oder zu ändern.

Denken Sie zum Beenden dieses Abschnitts daran: Unsere Bibliothek, unsere Regeln. Wir sollten genau wissen, wie der Code des Kunden aussehen wird und wie er ihn einem Unit-Test unterziehen wird. Wenn wir das nicht wissen, wird niemand und unsere Bibliothek einfach dazu beitragen, Code zu erstellen, der schwer zu verstehen und zu warten ist.

4. Dritte

Denken Sie daran, dass eine gute Bibliothek eine leichte Bibliothek ist. Ihr Code könnte ein Problem lösen und funktionsfähig sein. Wenn das JAR meinem Build jedoch 10 MB hinzufügt, ist klar, dass Sie die Blaupausen Ihres Projekts vor langer Zeit verloren haben. Wenn Sie viele Abhängigkeiten benötigen, versuchen Sie wahrscheinlich, zu viele Funktionen abzudecken, und sollten das Projekt in mehrere kleinere Projekte aufteilen.

Seien Sie so transparent wie möglich und binden Sie sich nach Möglichkeit nicht an tatsächliche Implementierungen. Das beste Beispiel, das mir in den Sinn kommt, ist: Verwenden Sie SLF4J, das nur eine API für die Protokollierung ist. Verwenden Sie log4j nicht direkt. Vielleicht möchte der Benutzer andere Protokollierer verwenden.

Dokumentbibliotheken, die Ihr Projekt transitiv durchlaufen und sicherstellen, dass Sie keine gefährlichen Abhängigkeiten wiexalan oderxml-apis einschließen (warum sie gefährlich sind, wird in diesem Artikel nicht näher erläutert).

Das Fazit hier ist: Behalten Sie Ihr Build leicht, transparent und wissen Sie immer, mit was Sie arbeiten. Es könnte Ihren Benutzern mehr Stress ersparen, als Sie sich vorstellen können.

5. Fazit

Der Artikel skizziert einige einfache Ideen, die dazu beitragen können, dass ein Projekt in Bezug auf Benutzerfreundlichkeit auf dem neuesten Stand bleibt. Eine Bibliothek, die eine Komponente ist, die ihren Platz in einem größeren Kontext finden sollte, sollte leistungsfähig sein und dennoch eine reibungslose und gut gestaltete Benutzeroberfläche bieten.

Es ist ein einfacher Schritt über die Linie und bringt das Design durcheinander. Die Mitwirkenden werden immer wissen, wie man es benutzt, aber jemand, der es zuerst betrachtet, wird es vielleicht nicht tun. Produktivität ist das Wichtigste. Nach diesem Prinzip sollten die Benutzer in wenigen Minuten in der Lage sein, eine Bibliothek zu verwenden.