On pourrait dire beaucoup de choses sur la culture Java, mais je pense que dans le cas auquel vous êtes confronté actuellement, il y a quelques aspects importants:
- Le code de bibliothèque est écrit une fois mais est utilisé beaucoup plus souvent. Bien qu'il soit agréable de minimiser le temps système nécessaire à l'écriture de la bibliothèque, il est probablement plus intéressant à long terme d'écrire d'une manière qui minimise le temps système d'utilisation de la bibliothèque.
- Cela signifie que les types auto-documentés sont excellents: les noms de méthodes permettent de clarifier ce qui se passe et ce que vous obtenez d'un objet.
- Le typage statique est un outil très utile pour éliminer certaines classes d’erreurs. Cela ne résout certainement pas tout (les gens aiment blaguer à propos de Haskell, une fois que le système de typage accepte votre code, c'est probablement correct), mais il est très facile de rendre certains types de mauvaises choses impossibles.
- L'écriture de code de bibliothèque consiste à spécifier des contrats. La définition d'interfaces pour vos types d'argument et de résultat permet de définir plus clairement les limites de vos contrats. Si quelque chose accepte ou produit un tuple, il est impossible de dire si c'est le genre de tuple que vous devriez réellement recevoir ou produire, et il y a très peu de contraintes sur un type aussi générique (a-t-il même le bon nombre d'éléments? Sont ils du type que vous attendiez?).
"Struct" classes avec des champs
Comme d'autres réponses l'ont mentionné, vous pouvez simplement utiliser une classe avec des champs publics. Si vous faites ces derniers, alors vous obtenez une classe immuable, et vous les initialiserez avec le constructeur:
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;
}
}
Bien entendu, cela signifie que vous êtes lié à une classe particulière et que tout ce qui doit générer ou utiliser un résultat d'analyse doit utiliser cette classe. Pour certaines applications, ça va. Pour d'autres, cela peut causer de la douleur. Une grande partie du code Java consiste à définir des contrats, ce qui vous mènera généralement vers des interfaces.
Un autre piège est qu'avec une approche basée sur les classes, vous exposez des champs et tous ces champs doivent avoir des valeurs. Par exemple, isSeconds et millis doivent toujours avoir une valeur, même si isLessThanOneMilli est true. Quelle doit être l'interprétation de la valeur du champ millis lorsque isLessThanOneMilli est true?
"Structures" comme interfaces
Avec les méthodes statiques autorisées dans les interfaces, il est en fait relativement facile de créer des types immuables sans trop de surcharge syntaxique. Par exemple, je pourrais implémenter le type de structure de résultats dont vous parlez comme ceci:
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;
}
};
}
}
Je suis tout à fait d’accord, c’est toujours beaucoup, mais il ya aussi quelques avantages, et je pense que ceux-ci commencent à répondre à certaines de vos principales questions.
Avec une structure comme celle-ci, le contrat de votre analyseur est très clairement défini. En Python, un tuple n'est pas vraiment distinct d'un autre tuple. En Java, le typage statique est disponible, nous avons donc déjà exclu certaines classes d’erreurs. Par exemple, si vous renvoyez un tuple en Python et que vous souhaitez renvoyer le tuple (millis, isSeconds, isLessThanOneMilli), vous pouvez accidentellement effectuer les opérations suivantes:
return (true, 500, false)
quand vous vouliez dire:
return (500, true, false)
Avec ce type d’interface Java, vous ne pouvez pas compiler:
return ParseResult.from(true, 500, false);
du tout. Tu dois faire:
return ParseResult.from(500, true, false);
C'est un avantage des langages statiquement typés en général.
Cette approche commence également à vous donner la possibilité de restreindre les valeurs que vous pouvez obtenir. Par exemple, lorsque vous appelez getMillis (), vous pouvez vérifier si isLessThanOneMilli () est true et, le cas échéant, déclenchez IllegalStateException (par exemple), car il n'y a pas de valeur significative de millis dans ce cas.
Faire en sorte qu'il soit difficile de faire le mauvais choix
Dans l'exemple d'interface ci-dessus, vous avez toujours le problème de pouvoir échanger accidentellement les arguments isSeconds et isLessThanOneMilli, car ils ont le même type.
En pratique, vous voudrez peut-être utiliser TimeUnit et la durée pour obtenir le résultat suivant:
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();
}
};
}
}
Ça commence à être beaucoup plus de code, mais vous n'avez besoin de l'écrire qu'une seule fois et (en supposant que vous ayez bien documenté les choses), les personnes qui finissent par utiliser votre code n'ont pas à deviner ce que le résultat signifie, et ne peut pas faire accidentellement des choses comme result[0]
quand ils veulent dire result[1]
. Vous devez toujours créer des instances assez succinctement, et en extraire des données n'est pas si difficile non plus:
ParseResult2 x = ParseResult2.from(TimeUnit.MILLISECONDS, 32);
ParseResult2 y = ParseResult2.lessThanOneMilli();
Notez que vous pouvez également faire quelque chose comme ceci avec l'approche basée sur les classes. Il suffit de spécifier les constructeurs pour les différents cas. Vous avez toujours la question de savoir à quoi initialiser les autres champs et vous ne pouvez pas empêcher leur accès.
Une autre réponse a indiqué que la nature de type entreprise de Java signifiait que la plupart du temps, vous composiez d'autres bibliothèques déjà existantes ou rédigiez des bibliothèques que d'autres personnes pourraient utiliser. Votre API publique ne devrait pas nécessiter beaucoup de temps pour consulter la documentation afin de déchiffrer les types de résultats si elle peut être évitée.
Vous n'écrivez ces structures qu'une seule fois, mais vous les créez plusieurs fois, de sorte que vous souhaitez toujours cette création concise (que vous obtenez). Le typage statique garantit que les données que vous en retirez sont conformes à vos attentes.
Cela dit, il reste encore des endroits où de simples nuplets ou des listes peuvent avoir beaucoup de sens. Il peut y avoir moins de temps système à renvoyer un tableau de quelque chose, et si c'est le cas (et que le temps système est significatif, ce que vous détermineriez avec le profilage), alors utilisez un simple tableau de valeurs en interne. peut sembler très judicieux. Votre API publique devrait toujours avoir des types clairement définis.