To jest część 3 z 4-częściowej serii.
Operatory zapewniają wartość walidacji właściwości jako pojedynczej jednostki: proszę pomyśleć o wieloczęściowej kontroli warunkowej w instrukcji if. Najbardziej oczywistymi operatorami są logiczne AND (&&
) i OR (!!
), choć możliwe są również inne operatory.
Wdrażanie operatorów
Operatory są same w sobie walidatorami, implementującymi to samo fun validate(): Boolean
jako walidatory właściwości, ale zamiast tego oceniają jeden lub więcej walidatorów w celu określenia ogólnego sukcesu lub porażki, np. wszystkie przechodzą walidację dla AND lub co najmniej jeden przechodzi walidację dla OR.
AbstractOperator
Każdy zaimplementowany operator rozszerza AbstractOperator
aby zapewnić poprawność equals
oraz hashCode
istnieją metody podobne do AbstractPropertyValidator
. Zamiast getter
dostarczony podczas budowy, operator otrzymuje kolekcję PropertyOperatorValidator
przeciwko którym operator jest stosowany.
abstract class AbstractOperator<S> (
protected val conditionName: String,
protected val validators: List<PropertyOperatorValidator<S>>) :
AbstractValidator<S>() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AbstractOperatorValidator<*>
if (conditionName != other.conditionName) return false
return true
}
override fun hashCode(): Int {
return Objects.hash(conditionName)
}
}
Walidatory właściwości i operatory implementują PropertyOperatorValidator
poprzez AbstractValidator
który pozwala operatorowi na rekurencyjną ocenę operatorów, podobnie jak w przypadku używania nawiasów do definiowania warunków podrzędnych w ogólnym warunku.
AndOperator
/OrOperator
Operatory logiczne są konstruowane z następującymi parametrami
conditionName
: opisowa nazwa celu lub zamiaru operatora;validators
: zbiór walidatorów, które są oceniane przez operatora;errorMessage
: komunikat o błędzie podany wConstraintViolation
; w przeciwnym razie naruszenia ograniczeń są tworzone dla każdego ocenianego walidatora.
Komunikat o błędzie specyficzny dla operatora często zapewnia lepszy kontekst niż indywidualne komunikaty dla każdego nieudanego warunku, takie jak Wymagane zarówno imię, jak i nazwisko. zamiast firstName
wymagane; lastName
wymagane.
class AndOperator<S>(conditionName: String,
validators: List<PropertyValidator<S>>,
val errorMessage: String? = null) :
AbstractOperator<S>(conditionName, validators) {
override fun validate(
source: S,
errors: MutableSet<ConstraintViolation<S>>): Boolean {
val errorsToUse =
if (errorMessage.isNullOrBlank())
errors
else
mutableSetOf()
val success = validators.all {
it.validate(source, errorsToUse)
}
if (!success && !errorMessage.isNullOrBlank()) {
addViolation(
source,
errorMessage,
errorMessage,
conditionName,
null,
errors)
}
return success
}
}
Jedyna zmiana wymagana do wdrożenia OrOperator
jest to, że tylko jeden walidator musi przejść walidację.
val success = validators.any {
it.validate(source, errorsToUse)
}
Jak to wszystko połączyć?
Na przykład planowanie społeczne Invitee
jest osobą zapraszaną. Stan osoby zaproszonej to INVITED
, ACCEPTED
, lub DECLINED
.
data class Invitee {
val state: InviteeState,
val firstName: String?,
val lastName: String?,
val emailAddress: String,
val howMany: Int?
}
Akceptując lub odrzucając zaproszenie, użytkownik podaje swoje imię i nazwisko. W przypadku zaakceptowanych zaproszeń wskazuje, ile osób będzie w nich uczestniczyć (np. sama zaproszona osoba, partner lub małżonek, dzieci, przyjaciele itp.) W przeciwnym razie opcjonalne właściwości nie są wymagane.
Ogólna implementacja programowania
val invitee = getInvitee(...)
if (invitee.state == InviteeState.INVITED) return true
if (invite.firstName.isNullOrBlank() || invite.lastName.isNullOrBlank()) {
log.warn("First and last name required.")
return false
}
if (invitee.state == InviteeState.ACCEPTED && howMany :? 0 <= 0) {
log.warn("Must specify how many people expected to attend.")
return false
}
return true
Rozwiązanie języka specyficznego dla domeny
val invitee = getInvitee(...)
// howMany required when invitation accepted
val accepted = OrOperator(
conditionName = "acceptedHowMany",
errorMessage = "Must specify number of attendees when accepting.",
validators = setOf(
EnumNotEqualsValidator(
propertyName = "state",
getter = Invitee::stage,
value = InviteeState.ACCEPTED),
PositiveIntegerValidator(
propertyName = "howMany",
getter = Invitee::howMany)
)
)
// Invitee must provide first/last name when accepting/declining invite
val responded = OrOperator(
conditionName = "inviteReply",
errorMessage = "First and last name required when accepted/declined.",
validators = setOf(
EnumNotEqualsValidator(
propertyName = "state",
getter = Invitee::stage,
value = InviteeState.INVITED
),
AndOperator (
conditionName = "allPresent",
errorMessage = null,
validators = setOf(
StringNotBlankValidator("firstName", Invitee::firstName),
StringNotBlankValidator("lastName", Invitee::lastName),
)
)
)
)
// Both of the above must be true
val operator = AndOperator (
propertyName = "inviteeValidation",
validators = setOf (accepted, responded)
)
// Validate the complete operator
val violations = mutableSetOf<ConstraintViolation<T>>()
operator.validate(invitee, violations)
// empty collection means successful validation
val successfullyValidated = violations.isEmpty()
Porównanie
Chociaż ogólna implementacja jest krótsza, czy jest to lepsza implementacja? Niektóre zalety walidacji za pomocą DSL to:
- Poprawność: Nazwy walidatorów (powinny) wyraźnie identyfikować sposób walidacji właściwości. Rzeczywista walidacja jest zaimplementowana raz dla wszystkich przypadków użycia, a nie zaimplementowana ad-hoc, co zmniejsza pokrycie testami. Komunikaty o błędach operatora zapewniają kontekst dla wymagań biznesowych i przypadków użycia. Brak możliwości wprowadzenia efektów ubocznych do walidacji.
- Spójność: Implementacja jest implementacją, nie ma różnic wszędzie tam, gdzie potrzebna jest konkretna walidacja: jeśli Apache Common LangStringUtils jest używana raz, to Apache Common Lang StringUtils jest używana zawsze. Pozwala to uniknąć niespójności i błędów spowodowanych używaniem podobnych bibliotek w różnych częściach bazy kodu.
- Reużywalność: Zdefiniuj raz, użyj wielu: utwórz walidację we wspólnej bibliotece, do której można uzyskać dostęp w razie potrzeby. Preferowane zamiast wycinania i wklejania, powielania implementacji lub wymuszania niewygodnej hierarchii klas w celu zapewnienia wspólnego dostępu.
- Czytelność: Prawie niezależny od języka, walidacja jest zrozumiała dzięki zrozumieniu sposobu tworzenia DSL, a nie sposobu pisania kodu. Wymaga mniejszego zrozumienia jakiegokolwiek konkretnego języka programowania JVM, na granicy samodokumentowania.
- Ad-Hoc: Walidacje just-in-time tworzone programowo bez złożoności manipulacji kodem bajtowym poprzez ASM lub coś podobnego.
Uwagi końcowe
Operatory DSL pozwalają nam implementować bardziej złożone i użyteczne walidatory, znacznie wykraczające poza to, co jest możliwe w przypadku adnotacji/walidatorów na poziomie właściwości (np, @NotBlank
, @NotNull
, @Email
itp.). Ostatnim krokiem jest owinięcie tego prawdziwym Jakarta Bean Validator
który może być użyty do walidacji kompletnej fasoli.
Image © 1991 Scott C Sosna