Is this code optimal for checking amount is not null?
public record Money(BigDecimal amount) {
public static final Money ZERO = new Money(BigDecimal.ZERO);
public Money(BigDecimal amount) {
if (amount == null) {
throw new IllegalArgumentException();
}
this.amount = setScale(amount);
}
}
Cześć! To bardzo dobre pytanie, które dotyka sedna tego, czym są rekordy w Javie i jak zapewnić spójność danych.
Krótka odpowiedź: Tak, można i należy zdefiniować rekord tak, aby nie pozwalał na null. Sposób, który pokazałeś (walidacja w konstruktorze), jest jak najbardziej poprawnym i zalecanym mechanizmem w Javie.
Twój kod jest w 99% idealny. Rozumiem jednak, że "nie podoba Ci się" jego składnia. Na szczęście Java oferuje rozwiązanie, które sprawia, że jest to znacznie bardziej zwięzłe i eleganckie: konstruktor kompaktowy (compact constructor).
Ulepszenie Twojego kodu: Konstruktor Kompaktowy
Zamiast pisać pełny konstruktor kanoniczny, możesz użyć jego skróconej formy, która została stworzona właśnie do takich celów – walidacji i normalizacji danych.
Zobaczmy, jak można ulepszyć Twój kod:
Twoja wersja (poprawna, ale rozwlekła):
import java.math.BigDecimal;
public record Money(BigDecimal amount) {
public Money(BigDecimal amount) {
if (amount == null) {
throw new IllegalArgumentException("Amount cannot be null");
}
this.amount = setScale(amount); // Załóżmy, że setScale istnieje
}
// Prywatna metoda pomocnicza do normalizacji
private static BigDecimal setScale(BigDecimal input) {
return input.setScale(2, RoundingMode.HALF_UP);
}
}
Wersja z konstruktorem kompaktowym (zwięzła i idiomatyczna):
W konstruktorze kompaktowym nie musisz pisać this.amount = amount;. Java zrobi to za Ciebie. Skupiasz się tylko na logice walidacji i transformacji.
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Objects;
public record Money(BigDecimal amount) {
// Konstruktor kompaktowy
public Money {
// Używamy Objects.requireNonNull dla zwięzłości i standardowego komunikatu
Objects.requireNonNull(amount, "amount cannot be null");
// Możemy też od razu przypisać znormalizowaną wartość do pola `amount`
// Mimo że nie piszemy "this.amount =", ta operacja nadpisze parametr `amount`,
// a Java użyje tej nowej wartości do zainicjowania finalnego pola.
amount = amount.setScale(2, RoundingMode.HALF_UP);
}
// Możemy usunąć stałą ZERO, jeśli nie jest potrzebna publicznie
public static final Money ZERO = new Money(BigDecimal.ZERO);
}
Jak widzisz, kod w konstruktorze kompaktowym jest znacznie krótszy i czytelniejszy. Robi dokładnie to samo, co Twój kod, ale jego intencja (walidacja i normalizacja) jest bardziej wyeksponowana.
Dlaczego walidacja w konstruktorze to najlepsze podejście?
Rekordy w Javie są niezmienne (immutable). Oznacza to, że po utworzeniu obiektu jego stan (wartości pól) nie może być już zmieniony. Konstruktor jest jedynym "strażnikiem", który decyduje, czy obiekt w ogóle może powstać.
- Gwarancja spójności: Umieszczając walidację w konstruktorze, masz 100% pewności, że każdy istniejący obiekt typu
Moneyjest w poprawnym stanie (np. jego poleamountnigdy nie jestnull). - Zasada "Fail-Fast": Program wyrzuci wyjątek natychmiast przy próbie stworzenia niepoprawnego obiektu, a nie w losowym momencie w przyszłości, gdy ktoś spróbuje użyć
null-owej wartości, co znacznie ułatwia debugowanie.
A co z innymi podejściami? (np. adnotacje)
Możesz się zastanawiać, czy nie da się tego zrobić za pomocą adnotacji, np. @NonNull.
import org.jetbrains.annotations.NotNull; // lub z innej biblioteki
import java.math.BigDecimal;
public record Money(@NotNull BigDecimal amount) {
// ...
}
To również jest bardzo dobra praktyka, ale pełni inną rolę:
- Statyczna analiza kodu: Twoje IDE (np. IntelliJ, Eclipse) oraz narzędzia do budowania (np. SpotBugs, Sonar) wykorzystają tę adnotację, aby ostrzec Cię w czasie pisania kodu, jeśli spróbujesz przekazać
null. - Brak walidacji w czasie wykonania (runtime): Sama adnotacja
@NotNullnie dodaje logiki rzucającej wyjątek, gdy program jest uruchomiony. Jest to jedynie metadana dla narzędzi.
Wyjątkiem jest biblioteka Lombok, która potrafi przetworzyć adnotację @NonNull i automatycznie wygenerować w kodzie bajtowym sprawdzenie Objects.requireNonNull. Jednak w przypadku standardowych rekordów Javy, musisz to zrobić sam.
Złoty Standard: Połączenie obu podejść
Najlepszym i najbardziej kompletnym rozwiązaniem jest połączenie obu technik:
- Konstruktor kompaktowy z
Objects.requireNonNulldla gwarancji w czasie wykonania. - Adnotacja
@NonNulldla wsparcia od narzędzi i statycznej analizy.
import org.jetbrains.annotations.NotNull; // np. z zależności com.jetbrains:annotations
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Objects;
public record Money(@NotNull BigDecimal amount) {
public static final Money ZERO = new Money(BigDecimal.ZERO);
// Konstruktor kompaktowy do walidacji i normalizacji
public Money {
Objects.requireNonNull(amount, "amount cannot be null");
amount = amount.setScale(2, RoundingMode.HALF_UP);
}
}
Taka definicja rekordu jest "pancerna":
- Jest bezpieczna w czasie wykonania.
- Pomaga deweloperom unikać błędów już na etapie pisania kodu.
- Jest zwięzła i jasno komunikuje swoje intencje (niezmienniki).
Podsumowując
Twój instynkt był bardzo dobry! Walidacja w konstruktorze to klucz do tworzenia solidnych i niezawodnych rekordów. Użycie konstruktora kompaktowego oraz metody Objects.requireNonNull() sprawi, że Twój kod będzie bardziej elegancki i zgodny z nowoczesnymi praktykami w Javie.