Ci sono molte cose che si potrebbero dire sulla cultura Java, ma penso che nel caso in cui ti trovi di fronte in questo momento, ci sono alcuni aspetti significativi:
- Il codice della biblioteca viene scritto una volta ma usato molto più spesso. Mentre è bello ridurre al minimo il sovraccarico di scrivere la libreria, probabilmente è più utile nel lungo periodo scrivere in un modo che minimizzi il sovraccarico dell'uso della libreria.
- Ciò significa che i tipi di auto-documentazione sono fantastici: i nomi dei metodi aiutano a chiarire cosa sta succedendo e cosa stai uscendo da un oggetto.
- La digitazione statica è uno strumento molto utile per eliminare determinate classi di errori. Certamente non risolve tutto (alla gente piace scherzare su Haskell che una volta che il sistema dei tipi accetta il tuo codice, probabilmente è corretto), ma rende molto facile rendere certi tipi di cose sbagliate impossibili.
- Scrivere il codice della biblioteca significa specificare i contratti. La definizione di interfacce per argomenti e tipi di risultati rende più chiari i confini dei contratti. Se qualcosa accetta o produce una tupla, non si può dire se sia il tipo di tupla che dovresti effettivamente ricevere o produrre, e c'è ben poco in termini di vincoli su un tipo così generico (ha anche il giusto numero di elementi? quelli del tipo che ti aspettavi?).
Classi "Struct" con campi
Come hanno già detto altre risposte, puoi semplicemente usare una classe con campi pubblici. Se li rendi definitivi, otterrai una classe immutabile e li inizializzeresti con il costruttore:
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;
}
}
Ovviamente, questo significa che sei legato a una particolare classe e tutto ciò che deve mai produrre o consumare un risultato di analisi deve usare questa classe. Per alcune applicazioni, va bene. Per altri, ciò può causare dolore. Gran parte del codice Java riguarda la definizione dei contratti e questo ti porterà in genere alle interfacce.
Un altro inconveniente è che con un approccio basato sulla classe, stai esponendo campi e tutti questi campi devono avere valori. Ad esempio, isSeconds e millis devono sempre avere un certo valore, anche se isLessThanOneMilli è true. Quale dovrebbe essere l'interpretazione del valore del campo millis quando isLessThanOneMilli è vero?
"Strutture" come interfacce
Con i metodi statici consentiti nelle interfacce, in realtà è relativamente facile creare tipi immutabili senza un sacco di sovraccarico sintattico. Ad esempio, potrei implementare il tipo di struttura dei risultati di cui stai parlando come qualcosa del genere:
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;
}
};
}
}
È ancora un sacco di punti di forza, sono assolutamente d'accordo, ma ci sono anche un paio di vantaggi e penso che questi inizino a rispondere ad alcune delle tue domande principali.
Con una struttura come questo risultato di analisi, il contratto del tuo parser è definito in modo molto chiaro. In Python, una tupla non è veramente distinta da un'altra tupla. In Java, la tipizzazione statica è disponibile, quindi escludiamo già alcune classi di errori. Ad esempio, se stai restituendo una tupla in Python e vuoi restituire la tupla (millis, isSeconds, isLessThanOneMilli), puoi accidentalmente fare:
return (true, 500, false)
quando intendevi:
return (500, true, false)
Con questo tipo di interfaccia Java, non è possibile compilare:
return ParseResult.from(true, 500, false);
affatto. Devi fare:
return ParseResult.from(500, true, false);
Questo è un vantaggio delle lingue tipicamente statiche in generale.
Questo approccio inizia anche a darti la possibilità di limitare quali valori puoi ottenere. Ad esempio, quando si chiama getMillis (), è possibile verificare se isLessThanOneMilli () è true e, in caso affermativo, generare un IllegalStateException (ad esempio), poiché in questo caso non esiste un valore significativo di millis.
Rendendo difficile fare la cosa sbagliata
Nell'esempio di interfaccia sopra, hai ancora il problema che potresti scambiare accidentalmente gli argomenti isSeconds e isLessThanOneMilli, poiché hanno lo stesso tipo.
In pratica, potresti voler utilizzare TimeUnit e la durata, in modo da ottenere un risultato come:
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();
}
};
}
}
Sta diventando molto più codice, ma devi solo scriverlo una volta e (supponendo che tu abbia documentato correttamente le cose), le persone che finiscono per usare il tuo codice non devono indovinare il significato del risultato, e non può fare accidentalmente cose come result[0]
quando significano result[1]
. Puoi comunque creare le istanze in modo piuttosto succinto, e ottenere i dati da esse non è poi così difficile:
ParseResult2 x = ParseResult2.from(TimeUnit.MILLISECONDS, 32);
ParseResult2 y = ParseResult2.lessThanOneMilli();
Nota che potresti effettivamente fare qualcosa del genere anche con l'approccio basato sulla classe. Basta specificare i costruttori per i diversi casi. Tuttavia, hai ancora il problema su cosa inizializzare gli altri campi e non puoi impedirne l'accesso.
Un'altra risposta ha affermato che la natura di tipo enterprise di Java significa che per la maggior parte del tempo, stai componendo altre librerie già esistenti o scrivendo librerie che altre persone possono usare. L' API pubblica non dovrebbe richiedere molto tempo a consultare la documentazione per decifrare i tipi di risultati se può essere evitato.
Hai solo scrivere queste strutture una volta, ma si creano loro molte volte, quindi è ancora voglio che la creazione concisa (che si ottiene). La digitazione statica si assicura che i dati che stai ottenendo da loro siano quelli che ti aspetti.
Ora, tutto ciò detto, ci sono ancora posti in cui semplici tuple o liste possono avere molto senso. Potrebbero esserci meno costi generali nel restituire una matrice di qualcosa, e se questo è il caso (e tale sovraccarico è significativo, cosa che determineresti con la profilazione), allora usare una semplice matrice di valori internamente potrebbe avere molto senso. Probabilmente l'API pubblica dovrebbe avere tipi chiaramente definiti.