Es gibt eine Menge Dinge, die über die Java-Kultur gesagt werden könnten, aber ich denke, dass es in dem Fall, in dem Sie gerade konfrontiert sind, ein paar wichtige Aspekte gibt:
- Bibliothekscode wird einmal geschrieben, aber viel häufiger verwendet. Während es nett ist, den Aufwand für das Schreiben der Bibliothek zu minimieren, ist es wahrscheinlich auf lange Sicht sinnvoller, so zu schreiben, dass der Aufwand für die Verwendung der Bibliothek minimiert wird.
- Das bedeutet, dass selbstdokumentierende Typen großartig sind: Methodennamen helfen dabei, klar zu machen, was passiert und was Sie aus einem Objekt herausholen.
- Die statische Typisierung ist ein sehr nützliches Werkzeug, um bestimmte Fehlerklassen zu beseitigen. Es behebt sicherlich nicht alles (die Leute witzeln gern über Haskell, dass es wahrscheinlich richtig ist, wenn Sie das Typensystem erst einmal dazu bringen, Ihren Code zu akzeptieren), aber es macht es sehr einfach, bestimmte Arten von falschen Dingen unmöglich.
- Beim Schreiben von Bibliothekscode geht es um die Angabe von Verträgen. Durch das Definieren von Schnittstellen für Ihre Argumente und Ergebnistypen werden die Grenzen Ihrer Verträge klarer definiert. Wenn etwas ein Tupel akzeptiert oder erzeugt, gibt es keine Aussage darüber, ob es sich um die Art von Tupel handelt, die Sie tatsächlich erhalten oder erzeugen sollten, und es gibt kaum Einschränkungen für einen solchen generischen Typ (hat er überhaupt die richtige Anzahl von Elementen?) sie von der Art, die Sie erwartet hatten?).
"Struct" -Klassen mit Feldern
Wie andere Antworten bereits erwähnt haben, können Sie eine Klasse nur mit öffentlichen Feldern verwenden. Wenn Sie diese final machen, erhalten Sie eine unveränderliche Klasse und Sie initialisieren sie mit dem Konstruktor:
class ParseResult0 {
public final long millis;
public final boolean isSeconds;
public final boolean isLessThanOneMilli;
public ParseResult0(long millis, boolean isSeconds, boolean isLessThanOneMilli) {
this.millis = millis;
this.isSeconds = isSeconds;
this.isLessThanOneMilli = isLessThanOneMilli;
}
}
Dies bedeutet natürlich, dass Sie an eine bestimmte Klasse gebunden sind, und alles, was jemals ein Analyseergebnis erzeugen oder verarbeiten muss, muss diese Klasse verwenden. Für einige Anwendungen ist das in Ordnung. Bei anderen kann dies Schmerzen verursachen. Bei viel Java-Code geht es um die Definition von Verträgen, und das führt Sie in der Regel zu Schnittstellen.
Ein weiterer Fallstrick ist , dass mit einer Klasse basierten Ansatz, Sie Felder sind Belichtungs- und alle diese Felder müssen Werte haben. ZB müssen isSeconds und millis immer einen Wert haben, auch wenn isLessThanOneMilli wahr ist. Wie sollte der Wert des Millis-Feldes interpretiert werden, wenn isLessThanOneMilli wahr ist?
"Strukturen" als Schnittstellen
Mit den statischen Methoden, die in Interfaces erlaubt sind, ist es relativ einfach, unveränderliche Typen ohne großen syntaktischen Aufwand zu erstellen. Zum Beispiel könnte ich die Art der Ergebnisstruktur, von der Sie sprechen, folgendermaßen implementieren:
interface ParseResult {
long getMillis();
boolean isSeconds();
boolean isLessThanOneMilli();
static ParseResult from(long millis, boolean isSeconds, boolean isLessThanOneMill) {
return new ParseResult() {
@Override
public boolean isSeconds() {
return isSeconds;
}
@Override
public boolean isLessThanOneMilli() {
return isLessThanOneMill;
}
@Override
public long getMillis() {
return millis;
}
};
}
}
Das ist immer noch eine Menge Boilerplate, da bin ich mir absolut einig, aber es gibt auch ein paar Vorteile, und ich denke, dass diese anfangen, einige Ihrer Hauptfragen zu beantworten.
Mit einer Struktur wie diesem Analyseergebnis ist der Vertrag Ihres Parsers sehr klar definiert. In Python unterscheidet sich ein Tupel nicht wirklich von einem anderen Tupel. In Java ist die statische Typisierung verfügbar, sodass bestimmte Fehlerklassen bereits ausgeschlossen sind. Wenn Sie beispielsweise ein Tupel in Python zurückgeben und das Tupel zurückgeben möchten (millis, isSeconds, isLessThanOneMilli), können Sie versehentlich Folgendes tun:
return (true, 500, false)
als du meintest:
return (500, true, false)
Mit dieser Art von Java-Schnittstelle können Sie nicht kompilieren:
return ParseResult.from(true, 500, false);
überhaupt. Du musst:
return ParseResult.from(500, true, false);
Das ist ein Vorteil von statisch typisierten Sprachen im Allgemeinen.
Mit diesem Ansatz können Sie auch einschränken, welche Werte Sie erhalten können. Wenn Sie zum Beispiel getMillis () aufrufen, können Sie überprüfen, ob isLessThanOneMilli () wahr ist, und wenn dies der Fall ist, lösen Sie (zum Beispiel) eine IllegalStateException aus, da in diesem Fall kein sinnvoller Wert für millis vorliegt.
Es schwierig machen, das Falsche zu tun
Im obigen Schnittstellenbeispiel besteht jedoch weiterhin das Problem, dass Sie die Argumente isSeconds und isLessThanOneMilli versehentlich vertauschen könnten, da sie denselben Typ haben.
In der Praxis sollten Sie TimeUnit und duration wirklich verwenden, damit Sie das folgende Ergebnis erzielen:
interface Duration {
TimeUnit getTimeUnit();
long getDuration();
static Duration from(TimeUnit unit, long duration) {
return new Duration() {
@Override
public TimeUnit getTimeUnit() {
return unit;
}
@Override
public long getDuration() {
return duration;
}
};
}
}
interface ParseResult2 {
boolean isLessThanOneMilli();
Duration getDuration();
static ParseResult2 from(TimeUnit unit, long duration) {
Duration d = Duration.from(unit, duration);
return new ParseResult2() {
@Override
public boolean isLessThanOneMilli() {
return false;
}
@Override
public Duration getDuration() {
return d;
}
};
}
static ParseResult2 lessThanOneMilli() {
return new ParseResult2() {
@Override
public boolean isLessThanOneMilli() {
return true;
}
@Override
public Duration getDuration() {
throw new IllegalStateException();
}
};
}
}
Das ist immer ein zu viel mehr Code, aber Sie müssen es nur einmal schreiben, und (vorausgesetzt , Sie richtig Dinge dokumentiert), die Menschen , die am Ende mit Ihrem Code müssen nicht erraten , was das Ergebnis bedeutet, und kann nicht versehentlich Dinge tun, wie result[0]
wenn sie bedeuten result[1]
. Sie können Instanzen immer noch ziemlich prägnant erstellen , und es ist auch nicht allzu schwierig, Daten aus ihnen herauszuholen:
ParseResult2 x = ParseResult2.from(TimeUnit.MILLISECONDS, 32);
ParseResult2 y = ParseResult2.lessThanOneMilli();
Beachten Sie, dass Sie mit dem klassenbasierten Ansatz auch so etwas tun können. Geben Sie einfach die Konstruktoren für die verschiedenen Fälle an. Sie haben jedoch immer noch die Frage, wie die anderen Felder initialisiert werden sollen, und Sie können den Zugriff darauf nicht verhindern.
In einer anderen Antwort wurde erwähnt, dass aufgrund des Enterprise-Charakters von Java häufig andere Bibliotheken erstellt werden, die bereits vorhanden sind, oder Bibliotheken für andere Benutzer geschrieben werden. Ihre öffentliche API sollte nicht viel Zeit benötigen, um die Dokumentation zu konsultieren und die Ergebnistypen zu entschlüsseln, wenn dies vermieden werden kann.
Sie schreiben diese Strukturen nur einmal, aber Sie erstellen sie viele Male, sodass Sie immer noch diese prägnante Erstellung (die Sie erhalten) möchten. Die statische Typisierung stellt sicher, dass die Daten, die Sie aus ihnen herausholen, Ihren Erwartungen entsprechen.
Trotzdem gibt es immer noch Orte, an denen einfache Tupel oder Listen viel Sinn ergeben können. Die Rückgabe eines Arrays von etwas ist möglicherweise mit weniger Aufwand verbunden. Wenn dies der Fall ist (und dieser Aufwand erheblich ist, den Sie mit der Profilerstellung ermitteln würden), kann die interne Verwendung eines einfachen Arrays von Werten sehr sinnvoll sein. Ihre öffentliche API sollte wahrscheinlich immer noch klar definierte Typen haben.