Há muitas coisas que podem ser ditas sobre a cultura Java, mas acho que, no caso de você se deparar agora, há alguns aspectos significativos:
- O código da biblioteca é escrito uma vez, mas usado com muito mais frequência. Embora seja bom minimizar a sobrecarga de escrever a biblioteca, provavelmente vale mais a pena a longo prazo, de forma a minimizar a sobrecarga de uso da biblioteca.
- Isso significa que os tipos de auto-documentação são ótimos: os nomes dos métodos ajudam a esclarecer o que está acontecendo e o que você está obtendo de um objeto.
- A digitação estática é uma ferramenta muito útil para eliminar certas classes de erros. Certamente não corrige tudo (as pessoas gostam de brincar com Haskell que, uma vez que você aceita o código do sistema, provavelmente está correto), mas facilita muito a impossibilidade de certos tipos de coisas erradas.
- Escrever código de biblioteca é sobre a especificação de contratos. A definição de interfaces para seus tipos de argumento e resultado torna os limites de seus contratos mais claramente definidos. Se algo aceita ou produz uma tupla, não há como dizer se é o tipo de tupla que você realmente deve receber ou produzir, e há muito pouco em termos de restrições em um tipo tão genérico (ele tem o número certo de elementos? eles do tipo que você estava esperando?).
Classes "Struct" com campos
Como outras respostas mencionaram, você pode simplesmente usar uma classe com campos públicos. Se você finalizá-las, obtém uma classe imutável e as inicializa com o construtor:
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;
}
}
Obviamente, isso significa que você está vinculado a uma classe específica e qualquer coisa que precise produzir ou consumir um resultado de análise deve usar essa classe. Para algumas aplicações, tudo bem. Para outros, isso pode causar alguma dor. Grande parte do código Java é sobre a definição de contratos, e isso normalmente leva você a interfaces.
Outra armadilha é que, com uma abordagem baseada em classe, você expõe campos e todos esses campos devem ter valores. Por exemplo, isSeconds e millis sempre precisam ter algum valor, mesmo se isLessThanOneMilli for verdadeiro. Qual deve ser a interpretação do valor do campo millis quando isLessThanOneMilli for verdadeiro?
"Estruturas" como interfaces
Com os métodos estáticos permitidos nas interfaces, é realmente relativamente fácil criar tipos imutáveis sem muita sobrecarga sintática. Por exemplo, eu posso implementar o tipo de estrutura de resultados que você está falando como algo assim:
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;
}
};
}
}
Ainda é um monte de clichê, eu concordo absolutamente, mas há alguns benefícios também, e acho que eles começam a responder algumas de suas principais perguntas.
Com uma estrutura como esse resultado da análise, o contrato do seu analisador é definido com muita clareza. No Python, uma tupla não é realmente diferente de outra. Em Java, a digitação estática está disponível; portanto, já descartamos determinadas classes de erros. Por exemplo, se você está retornando uma tupla em Python e deseja retornar a tupla (millis, isSeconds, isLessThanOneMilli), você pode acidentalmente fazer:
return (true, 500, false)
quando você quis dizer:
return (500, true, false)
Com esse tipo de interface Java, você não pode compilar:
return ParseResult.from(true, 500, false);
em absoluto. Você tem que fazer:
return ParseResult.from(500, true, false);
Esse é um benefício das linguagens estaticamente tipadas em geral.
Essa abordagem também começa a lhe dar a capacidade de restringir quais valores você pode obter. Por exemplo, ao chamar getMillis (), você pode verificar se isLessThanOneMilli () é verdadeiro e, se for, lançar uma IllegalStateException (por exemplo), já que não há valor significativo de millis nesse caso.
Tornando difícil fazer a coisa errada
No exemplo de interface acima, você ainda tem o problema de trocar acidentalmente os argumentos isSeconds e isLessThanOneMilli, pois eles têm o mesmo tipo.
Na prática, você pode realmente usar o TimeUnit e a duração, para obter um 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();
}
};
}
}
Isso está ficando muito mais código, mas você só precisa escrevê-lo uma vez e (supondo que tenha documentado as coisas corretamente), as pessoas que acabam usando seu código não precisam adivinhar o que o resultado significa e acidentalmente não pode fazer coisas como result[0]
quando elas significam result[1]
. Você ainda pode criar instâncias de maneira bem sucinta, e obter dados delas também não é tão difícil:
ParseResult2 x = ParseResult2.from(TimeUnit.MILLISECONDS, 32);
ParseResult2 y = ParseResult2.lessThanOneMilli();
Observe que você também pode fazer algo assim com a abordagem baseada em classe. Basta especificar construtores para os diferentes casos. No entanto, você ainda tem o problema de inicializar os outros campos e não pode impedir o acesso a eles.
Outra resposta mencionou que a natureza corporativa do Java significa que, na maioria das vezes, você está compondo outras bibliotecas que já existem ou escrevendo bibliotecas para outras pessoas usarem. Sua API pública não deve exigir muito tempo consultando a documentação para decifrar os tipos de resultado, se for possível evitar.
Você escreve essas estruturas apenas uma vez, mas as cria muitas vezes, portanto ainda deseja a criação concisa (que obtém). A digitação estática garante que os dados que você obtém deles sejam o que você espera.
Agora, tudo o que foi dito, ainda existem lugares onde tuplas ou listas simples podem fazer muito sentido. Pode haver menos sobrecarga no retorno de uma matriz de algo, e se esse for o caso (e essa sobrecarga for significativa, o que você determinaria com a criação de perfil), usar um conjunto simples de valores internamente pode fazer muito sentido. Sua API pública provavelmente ainda deve ter tipos claramente definidos.