Praktiline näide
Sissejuhatus
Eelmised peatükid tutvustasid reflektsiooni ja annotatsioone samm-sammult. Selles peatükis paneme need kokku väikeseks, kuid kasulikuks raamistikuks. Eesmärk on luua väljade valideerija, mis loeb objekti väljade annotatsioone ja annab teada, millised neist rikuvad ette määratud reegleid.
See on lihtsustatud versioon sellest, mida päris teegid teevad. Näiteks Hibernate Validator järgib sama üldist ülesehitust, kuid sisaldab palju rohkem reegleid ja seadistusvõimalusi. Peaasi on aru saada reflektsiooni põhiidee olemusest.
Eesmärk
Eesmärk on saada kood tööle sedasi, et annotatsioonidega märgitud elemendid kontrollitakse üle ja vajaduse korral visatakse erind
public class RegistrationForm {
@NotNull
@MaxLength(50)
private String username;
@NotNull
@Email
private String email;
@Min(18)
private int age;
}
RegistrationForm form = new RegistrationForm();
form.setUsername("ada");
form.setEmail("not-an-email");
form.setAge(15);
Validator validator = new Validator();
List<String> errors = validator.validate(form);
for (String error : errors) {
System.out.println(error);
}
// email: not a valid email address
// age: must be at least 18
Need annotatsioonid peaksid töötama ükskõik millise klassi ja väljade peal.
1. samm - annotatsioonide loomine
Iga reegel on omaette annotatsioon. Kõik loodavad annotatsioonid on mõeldud ainult väljadel kasutamiseks ning käitusajal lugemiseks:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {
String message() default "must not be null";
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MaxLength {
int value();
String message() default "is too long";
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Min {
int value();
String message() default "is too small";
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Email {
String message() default "not a valid email address";
}
Igal annotatsioonil on parameeter, mis on asjakohane rakendatavate reeglite puhul, ning valikuline message väli, millega on võimalik veateadet muuta.
2. samm - valideerija
Valideerija ülesanne on läbi uurida kõik väljad, mis ette antud objektil on ning kontrollida millistel nendest on annotatsioonid küljes. Samuti käivitatakse kõik valideerimismeetodid siin.
public class Validator {
public List<String> validate(Object target) {
List<String> errors = new ArrayList<>();
for (Field field : target.getClass().getDeclaredFields()) {
field.setAccessible(true);
Object value = readField(field, target);
checkNotNull(field, value, errors);
checkMaxLength(field, value, errors);
checkMin(field, value, errors);
checkEmail(field, value, errors);
}
return errors;
}
private Object readField(Field field, Object target) {
try {
return field.get(target);
} catch (IllegalAccessException e) {
throw new RuntimeException("Could not read field " + field.getName(), e);
}
}
}
Iga reegli jaoks loome eraldi meetodi, mis uurib üht kindlat annotatsiooni.
Selle tulemusena on validate meetod loetav ning uute reeglite lisamine kerge:
vaja lihtsalt luua uus annotatsioon, luua meetod ning lisada viide sellele meetodile validate meetodisse.
3. samm - reeglite teostamine
Iga reegel järgib ühist mustrit: annotatsiooni otsimine, olemasolu puhul välja väärtuse kontrollimine ning rikkumise puhul erindi viskamine.
private void checkNotNull(Field field, Object value, List<String> errors) {
NotNull annotation = field.getAnnotation(NotNull.class);
if (annotation != null && value == null) {
errors.add(field.getName() + ": " + annotation.message());
}
}
private void checkMaxLength(Field field, Object value, List<String> errors) {
MaxLength annotation = field.getAnnotation(MaxLength.class);
if (annotation != null && value instanceof String s && s.length() > annotation.value()) {
errors.add(field.getName() + ": " + annotation.message());
}
}
private void checkMin(Field field, Object value, List<String> errors) {
Min annotation = field.getAnnotation(Min.class);
if (annotation != null && value instanceof Number n && n.intValue() < annotation.value()) {
errors.add(field.getName() + ": " + annotation.message());
}
}
private void checkEmail(Field field, Object value, List<String> errors) {
Email annotation = field.getAnnotation(Email.class);
if (annotation != null && value instanceof String s && !s.matches(".+@.+\\..+")) {
errors.add(field.getName() + ": " + annotation.message());
}
}
Nüüd peaks eesmärk täidetud olema ning valideerija töötama.
Mis on selle disaini juures huvitav
Mõned asjad väärivad eraldi esiletoomist.
Annotatsioonide kuhjamine Ühel väljal võib korraga olla mitu reeglit ning iga reegli kontrollimeetod ignoreerib välja, kui vastav annotatsioon puudub. Ei ole olemas keskset nimekirja sellest, millised kombinatsioonid on lubatud - valideerija käivitab iga kontrolli iga välja peal.
Tüübi kontroll iga reegli sees on oluline.
checkMaxLength rakendub ainult String tüüpi väljadele tänu instanceof String s mustrile.
Kui lisada @MaxLength annotatsiooni int väljale, siis reegel lihtsalt ei tee midagi, mitte ei kuku veaga läbi.
See muudab raamistiku väärkasutuse suhtes leebeks - disainiotsus, millel on omad kompromissid.
Rangem raamistik keelduks sellise konfiguratsiooniga üldse käivitumast.
Uue reegli lisamine on suletud muudatus.
Uus reegel tähendab uut annotatsiooniklassi ja uut checkXxx meetodit.
Olemasolevaid reegleid ega kasutajaklasse ei pea muutma.
See on SOLID avatud/suletud printsiip praktikas - laiendamine ilma muutmiseta.
Reflection esineb ainult ühes meetodis.
validate on ainus koht, kus reflektsiooni kasutatakse.
Iga reegel näeb ainult annotatsiooni ja väärtust.
Reflektsiooni koondamine ühte kohta on hea praktika: see hoiab ebaturvalise, ainult käitusajal toimiva käitumise piiratuna ja lihtsasti testitavana.
Kokkuvõtteks
See muster - leia annotatsioonidega elemendid, loe parameetrid, vii läbi mingi tegevus - on aluseks kõikidele populaarsetele teekidelen näiteks:
- Jackson skaneerib väljade annotatsioone, et otsustada JSON-väljade nimed.
- JUnit skaneerib meetodite annotatsioone, et otsustada, millised meetodid on testid.
- Spring skaneerib klassi- ja väljaannotatsioone, et siduda sõltuvused omavahel kokku.
Taustamehaanika on täpselt sama nagu selles valideerijas. Nende peatükkidega sai läbitud kõik reflektsiooniga seotud sõnavara, mida on vaja nendest teekidest arusaamiseks, lähtekoodi lugemiseks ja muudeks tegevusteks.