Hay muchas cosas que se podrían decir sobre la cultura Java, pero creo que en el caso de que te enfrentes ahora, hay algunos aspectos importantes:
- El código de la biblioteca se escribe una vez, pero se usa con mucha más frecuencia. Si bien es bueno minimizar la sobrecarga de escribir la biblioteca, probablemente valga la pena a la larga escribir de una manera que minimice la sobrecarga de usar la biblioteca.
- Eso significa que los tipos de autodocumentación son geniales: los nombres de los métodos ayudan a aclarar lo que sucede y lo que se obtiene de un objeto.
- La escritura estática es una herramienta muy útil para eliminar ciertas clases de errores. Ciertamente no soluciona todo (a la gente le gusta bromear sobre Haskell que una vez que obtienes el sistema de tipos para aceptar tu código, probablemente sea correcto), pero hace que sea muy fácil hacer que ciertas cosas incorrectas sean imposibles.
- Escribir código de biblioteca se trata de especificar contratos. La definición de interfaces para sus argumentos y tipos de resultados hace que los límites de sus contratos estén más claramente definidos. Si algo acepta o produce una tupla, no hay forma de decir si es el tipo de tupla que realmente debería recibir o producir, y hay muy pocas restricciones en el tipo genérico (¿tiene incluso el número correcto de elementos? ellos del tipo que estabas esperando?).
Clases de "estructura" con campos
Como han mencionado otras respuestas, puede usar una clase con campos públicos. Si haces estos finales, obtienes una clase inmutable y los inicializas con el constructor:
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;
}
}
Por supuesto, esto significa que está atado a una clase en particular, y cualquier cosa que necesite producir o consumir un resultado de análisis debe usar esta clase. Para algunas aplicaciones, está bien. Para otros, eso puede causar algo de dolor. Gran parte del código Java se trata de definir contratos, y eso generalmente lo llevará a las interfaces.
Otro escollo es que con un enfoque basado en clases, está exponiendo campos y todos esos campos deben tener valores. Por ejemplo, isSeconds y millis siempre tienen que tener algún valor, incluso si isLessThanOneMilli es verdadero. ¿Cuál debería ser la interpretación del valor del campo millis cuando isLessThanOneMilli es verdadero?
"Estructuras" como interfaces
Con los métodos estáticos permitidos en las interfaces, en realidad es relativamente fácil crear tipos inmutables sin una gran sobrecarga sintáctica. Por ejemplo, podría implementar el tipo de estructura de resultados del que estás hablando como algo así:
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;
}
};
}
}
Eso sigue siendo un montón de repeticiones, estoy totalmente de acuerdo, pero también hay un par de beneficios, y creo que esos comienzan a responder algunas de sus preguntas principales.
Con una estructura como este resultado de análisis, el contrato de su analizador está muy claramente definido. En Python, una tupla no es realmente distinta de otra tupla. En Java, la escritura estática está disponible, por lo que ya descartamos ciertas clases de errores. Por ejemplo, si devuelve una tupla en Python y desea devolver la tupla (millis, isSeconds, isLessThanOneMilli), puede hacer accidentalmente:
return (true, 500, false)
cuando quisiste decir:
return (500, true, false)
Con este tipo de interfaz Java, no puede compilar:
return ParseResult.from(true, 500, false);
en absoluto. Tu tienes que hacer:
return ParseResult.from(500, true, false);
Eso es un beneficio de los idiomas tipados estáticamente en general.
Este enfoque también comienza a darle la capacidad de restringir los valores que puede obtener. Por ejemplo, al llamar a getMillis (), puede verificar si isLessThanOneMilli () es verdadero y, si lo es, lanzar una IllegalStateException (por ejemplo), ya que no hay un valor significativo de millis en ese caso.
Haciendo difícil hacer lo incorrecto
Sin embargo, en el ejemplo de interfaz anterior, aún tiene el problema de que podría intercambiar accidentalmente los argumentos isSeconds e isLessThanOneMilli, ya que tienen el mismo tipo.
En la práctica, es posible que desee utilizar TimeUnit y la duración, para obtener un resultado como:
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();
}
};
}
}
Eso va a ser mucho más código, pero solo necesita escribirlo una vez, y (suponiendo que haya documentado correctamente las cosas), las personas que terminan usando su código no tienen que adivinar qué significa el resultado, y no puede hacer accidentalmente cosas como result[0]
cuando quieren decir result[1]
. Todavía puede crear instancias de manera bastante sucinta, y obtener datos de ellas tampoco es tan difícil:
ParseResult2 x = ParseResult2.from(TimeUnit.MILLISECONDS, 32);
ParseResult2 y = ParseResult2.lessThanOneMilli();
Tenga en cuenta que también podría hacer algo como esto con el enfoque basado en la clase. Simplemente especifique constructores para los diferentes casos. Sin embargo, todavía tiene el problema de qué inicializar los otros campos y no puede evitar el acceso a ellos.
Otra respuesta mencionó que la naturaleza de tipo empresarial de Java significa que la mayor parte del tiempo, está componiendo otras bibliotecas que ya existen o escribiendo bibliotecas para que otras personas las usen. Su API pública no debería requerir mucho tiempo consultando la documentación para descifrar los tipos de resultados si se puede evitar.
Solo escribe estas estructuras una vez, pero las crea muchas veces, por lo que aún desea esa creación concisa (que obtiene). La escritura estática asegura que los datos que está obteniendo de ellos sean lo que espera.
Ahora, dicho todo esto, todavía hay lugares donde las tuplas o listas simples pueden tener mucho sentido. Puede haber menos sobrecarga al devolver una matriz de algo, y si ese es el caso (y esa sobrecarga es significativa, lo que determinaría con la creación de perfiles), entonces usar una simple matriz de valores internamente puede tener mucho sentido. Su API pública aún debería tener tipos claramente definidos.