Istnieje wiele rzeczy, które można powiedzieć o kulturze Java, ale myślę, że w przypadku, gdy masz teraz do czynienia, istnieje kilka istotnych aspektów:
- Kod biblioteki jest zapisywany raz, ale jest używany znacznie częściej. Chociaż miło jest zminimalizować narzut związany z pisaniem biblioteki, prawdopodobnie na dłuższą metę bardziej opłacalne jest pisanie w sposób, który minimalizuje narzut związany z korzystaniem z biblioteki.
- Oznacza to, że typy samodokumentujące są świetne: nazwy metod pomagają wyjaśnić, co się dzieje i co wydostajesz się z obiektu.
- Wpisywanie statyczne jest bardzo przydatnym narzędziem do eliminowania niektórych klas błędów. Z pewnością nie naprawia wszystkiego (ludzie lubią żartować z Haskella, że kiedy system typów zaakceptuje Twój kod, prawdopodobnie jest poprawny), ale bardzo łatwo uniemożliwia pewne niewłaściwe rzeczy.
- Pisanie kodu bibliotecznego polega na określaniu umów. Definiowanie interfejsów dla argumentów i typów wyników sprawia, że granice twoich umów są jaśniej zdefiniowane. Jeśli coś akceptuje lub produkuje krotkę, nie wiadomo, czy jest to rodzaj krotki, którą powinieneś otrzymać lub wyprodukować, i jest bardzo mało przeszkód dla takiego rodzaju ogólnego (czy ma nawet odpowiednią liczbę elementów? są tego typu, którego się spodziewałeś?).
Klasy „Struct” z polami
Jak wspomniano w innych odpowiedziach, możesz po prostu użyć klasy z polami publicznymi. Jeśli dokonasz tych ostatecznych, otrzymasz niezmienną klasę i zainicjujesz je za pomocą konstruktora:
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;
}
}
Oczywiście oznacza to, że jesteś przywiązany do określonej klasy i wszystko, co kiedykolwiek musi wytworzyć lub skonsumować wynik analizy, musi korzystać z tej klasy. W przypadku niektórych aplikacji nie ma problemu. Dla innych może to powodować ból. Znaczna część kodu Java dotyczy definiowania umów, a to zazwyczaj prowadzi do interfejsów.
Inną pułapką jest to, że przy podejściu klasowym ujawniasz pola i wszystkie te pola muszą mieć wartości. Np. IsSeconds i millis zawsze muszą mieć jakąś wartość, nawet jeśli isLessThanOneMilli jest prawdą. Jaka powinna być interpretacja wartości pola millis, gdy jest prawdą isLessThanOneMilli?
„Struktury” jako interfejsy
Dzięki metodom statycznym dozwolonym w interfejsach tworzenie niezmiennych typów jest stosunkowo łatwe bez dużego nakładu syntaktycznego. Mogę na przykład zaimplementować strukturę wyników, o której mówisz, w następujący sposób:
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;
}
};
}
}
Zgadzam się z tym, to wciąż bardzo dużo rzeczy do zrobienia, ale jest też kilka korzyści i myślę, że zaczną odpowiadać na niektóre z waszych głównych pytań.
O strukturze jak tego wyniku parsowania The kontrakt Twojego parsera jest bardzo jasno określone. W Pythonie jedna krotka tak naprawdę nie różni się od innej krotki. W Javie dostępne jest pisanie statyczne, więc już wykluczamy pewne klasy błędów. Na przykład, jeśli zwracasz krotkę w Pythonie i chcesz zwrócić krotkę (millis, isSeconds, isLessThanOneMilli), możesz przypadkowo zrobić:
return (true, 500, false)
kiedy miałeś na myśli:
return (500, true, false)
Dzięki tego rodzaju interfejsowi Java nie można kompilować:
return ParseResult.from(true, 500, false);
w ogóle. Musisz zrobić:
return ParseResult.from(500, true, false);
Jest to ogólna zaleta języków statycznych.
To podejście zaczyna także dawać Ci możliwość ograniczenia, jakie wartości możesz uzyskać. Na przykład, wywołując getMillis (), możesz sprawdzić, czy isLessThanOneMilli () jest prawdą, a jeśli tak, wyrzuć IllegalStateException (na przykład), ponieważ w tym przypadku nie ma znaczącej wartości millis.
Utrudnianie robienia złych rzeczy
W powyższym przykładzie interfejsu nadal występuje problem, że można przypadkowo zamienić argumenty isSeconds i isLessThanOneMilli, ponieważ mają one ten sam typ.
W praktyce naprawdę warto skorzystać z TimeUnit i czasu trwania, aby uzyskać wynik taki jak:
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();
}
};
}
}
To będzie o wiele więcej kodu, ale musisz go napisać tylko raz, i (zakładając, że właściwie udokumentowałeś rzeczy), ludzie, którzy ostatecznie używają twojego kodu, nie muszą zgadywać, co oznacza wynik, i nie mogę przypadkowo zrobić czegoś takiego, jak result[0]
mają na myśli result[1]
. Nadal możesz tworzyć instancje dość zwięźle, a uzyskiwanie z nich danych nie jest wcale takie trudne:
ParseResult2 x = ParseResult2.from(TimeUnit.MILLISECONDS, 32);
ParseResult2 y = ParseResult2.lessThanOneMilli();
Zauważ, że możesz zrobić coś takiego również z podejściem klasowym. Po prostu określ konstruktory dla różnych przypadków. Nadal masz problem z tym, co zainicjować inne pola i nie możesz uniemożliwić dostępu do nich.
Inna odpowiedź wspomniała, że Java typu korporacyjnego oznacza, że przez większość czasu tworzysz inne biblioteki, które już istnieją, lub piszesz biblioteki dla innych osób. Twój publiczny interfejs API nie powinien wymagać dużo czasu od zapoznania się z dokumentacją, aby odszyfrować typy wyników, jeśli można tego uniknąć.
Te struktury piszesz tylko raz, ale tworzysz je wiele razy, więc nadal chcesz tego zwięzłego tworzenia (które otrzymujesz). Wpisywanie statyczne zapewnia, że dane, które z nich pobierasz, są zgodne z oczekiwaniami.
Teraz jednak powiedziano, że wciąż istnieją miejsca, w których proste krotki lub listy mogą mieć sens. Zwracanie tablicy czegoś może być mniejsze, a jeśli tak jest (a narzut jest znaczny, co można określić za pomocą profilowania), użycie wewnętrznej tablicy prostych wartości może mieć sens. Twój publiczny interfejs API powinien prawdopodobnie mieć jasno zdefiniowane typy.