Є багато речей, які можна сказати про культуру Java, але я думаю, що у випадку, з яким ви зараз стикаєтесь, є кілька важливих аспектів:
- Код бібліотеки пишеться один раз, але використовується набагато частіше. Хоча приємно мінімізувати накладні витрати на написання бібліотеки, мабуть, доцільніше в довгостроковій перспективі писати таким чином, що мінімізує накладні витрати на використання бібліотеки.
- Це означає, що типи самодокументування чудові: назви методів допомагають зрозуміти, що відбувається і що ви отримуєте з об’єкта.
- Статичне введення тексту - дуже корисний інструмент для усунення певних класів помилок. Це, звичайно, не виправляє все (люди люблять жартувати з приводу Haskell, що як тільки ви отримаєте систему типу прийняти ваш код, це, мабуть, правильно), але це робить дуже легко зробити певні види неправильних речей неможливими.
- Написання коду бібліотеки стосується конкретизації договорів. Визначення інтерфейсів для аргументів та типів результатів дозволяє чіткіше визначати межі ваших контрактів. Якщо щось приймає або виробляє кортеж, там не сказано, чи це такий кортеж, який ви насправді повинні отримувати чи виготовляти, і існує дуже мало способів обмеження такого загального типу (чи має він навіть потрібну кількість елементів? вони того типу, якого ви очікували?).
"Структуруйте" класи з полями
Як згадували інші відповіді, ви можете просто використовувати клас із загальнодоступними полями. Якщо ви зробите ці остаточні, то ви отримаєте незмінний клас, і ви ініціалізуєте їх з конструктором:
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;
}
}
Звичайно, це означає, що ви прив'язані до певного класу, і все, що коли-небудь потрібно для отримання або споживання результату аналізу, має використовувати цей клас. Для деяких програм це добре. Для інших це може спричинити біль. Багато Java-кодів полягає у визначенні контрактів, і це, як правило, залучає вас до інтерфейсів.
Інша проблема полягає в тому, що при підході до класу ви відкриваєте поля, і всі ці поля повинні мати значення. Наприклад, isSeconds і milis завжди мають мати деяке значення, навіть якщо isLessThanOneMilli є правдою. Якою має бути інтерпретація значення поля мілісу, коли isLessThanOneMilli є правдивим?
"Структури" як інтерфейси
За допомогою статичних методів, дозволених в інтерфейсах, створити непорушні типи фактично порівняно просто без усієї синтаксичної накладних витрат. Наприклад, я можу реалізувати таку структуру результатів, про яку ви говорите, як щось подібне:
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;
}
};
}
}
Я все ще багато котла, я абсолютно згоден, але є і кілька переваг, і я думаю, що вони починають відповідати на деякі ваші основні питання.
З такою структурою, як результат цього розбору, контракт вашого аналізатора дуже чітко визначений. У Python один кортеж насправді не відрізняється від іншого кортежу. У Java є статична типізація, тому ми вже виключаємо певні класи помилок. Наприклад, якщо ви повертаєте кортеж в Python, і хочете повернути кортеж (millis, isSeconds, isLessThanOneMilli), ви можете випадково зробити:
return (true, 500, false)
коли ви мали на увазі:
return (500, true, false)
З таким інтерфейсом Java ви не можете компілювати:
return ParseResult.from(true, 500, false);
зовсім. Ви повинні зробити:
return ParseResult.from(500, true, false);
Це користь загалом статичних типів мов.
Цей підхід також починає давати вам можливість обмежувати, які значення ви можете отримати. Наприклад, під час виклику getMillis (), ви можете перевірити, чи правда isLessThanOneMilli (), і якщо це так, киньте IllegalStateException (наприклад), оскільки в цьому випадку немає значущого значення міліс.
Зробити це важко зробити неправильну річ
У наведеному вище прикладі інтерфейсу у вас все ще виникає проблема, що ви можете випадково поміняти аргументи isSeconds і isLessThanOneMilli, оскільки вони мають один і той же тип.
На практиці ви дійсно можете скористатися TimeUnit та тривалістю, щоб у вас виникли такі результати, як:
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();
}
};
}
}
Це стане набагато більшим кодом, але його потрібно написати лише один раз, і (припускаючи, що ви правильно задокументовані речі), люди, які в кінцевому підсумку використовують ваш код, не повинні здогадуватися, що означає результат, і не можу випадково робити такі речі, як result[0]
коли вони означають result[1]
. Ви все одно можете створювати екземпляри досить лаконічно, і витяг з них даних теж не так важкий:
ParseResult2 x = ParseResult2.from(TimeUnit.MILLISECONDS, 32);
ParseResult2 y = ParseResult2.lessThanOneMilli();
Зауважте, що ви насправді можете зробити щось подібне і підходом до класу. Просто вкажіть конструктори для різних випадків. У вас все ще виникає питання про те, до чого ініціалізувати інші поля, і ви не можете перешкодити доступу до них.
Ще одна відповідь зазначала, що характер Java для корпоративного типу означає, що більшу частину часу ви створюєте інші бібліотеки, які вже існують, або пишете бібліотеки для використання іншими людьми. Ваш публічний API не повинен вимагати багато часу, звертаючись до документації, щоб розшифрувати типи результатів, якщо цього можна уникнути.
Ви пишете ці структури лише один раз, але ви створюєте їх багато разів, тому ви все одно хочете цього стислого створення (яке ви отримуєте). Статичне введення гарантує, що дані, які ви отримуєте з них, - це те, що ви очікуєте.
Тепер, все, що сказано, все ще є місця, де прості кортежі чи списки можуть мати багато сенсу. Можливо, буде менше витрат на повернення масиву чогось, і якщо це так (і цей наклад є значущим, що ви визначите при профілюванні), то використання простого масиву значень всередині може мати багато сенсу. У вашому загальнодоступному API все-таки повинні бути чітко визначені типи.