Java c'est verbeux, mais on peut réduire le bruit et s'approcher du langage naturel avec un Fluent API
// JUnit API
assertEquals(9, fellowshipOfTheRing.size());
assertTrue(fellowshipOfTheRing.contains(frodo, sam));
assertFalse(fellowshipOfTheRing.contains(sauron));
// AssertJ API (fluent)
assertThat(fellowshipOfTheRing).hasSize(9)
.contains(frodo, sam)
.doesNotContain(sauron);
Plusieurs nouveaux idiomes depuis Java 8 nous facilitent la tâche
// java.time API
Instant.ofEpochMilli(milli)
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
// java stream API
list.stream().filter(Objects::nonNull)
.collect(Collectors.joining(" ");
Plusieurs librairies populaires fournissent des API fluent
jOOQ, AssertJ, Apache Spark, etc.
Dataset<Row> averagePrice = prices
.filter(value.<String>getAs("insurer").equals("COOL insurer"))
.groupBy("product")
.agg(avg("price").as("average"))
.orderBy(desc("average"));
"A domain-specific language (DSL) is a computer language specialized to a particular application domain. This is in contrast to a general-purpose language (GPL), which is broadly applicable across domains"
Exclusions assureurs sur le modèle objet (code historique endetté)
public void check(FieldContext context, FormuleMoto formule, Conducteur conducteur,
Vehicule vehicule, Void unused, Besoins besoins,
Set<EAbTestingScenario> scenarios)
throws ExclusionException {
if (besoins == null) {
return;
}
if (besoins.getDateDebutContrat() == null) {
return;
}
if (!DateHelper.isAfter(besoins.getDateDebutContrat(),
DateHelper.ajouteJoursADate(DateHelper.getToday(), NBR_JOURS),
DateHelper.EPrecision.jour)) {
throw new ExclusionException(DATE_EFFET_PLUS_60_JOURS);
}
}
public ExclusionRule exclusionRule() {
return DOOV.when(dateContrat().after(todayPlusDays(60)))
.excludeFormules()
.withMessage(DATE_EFFET_PLUS_60_JOURS)
.exclusionRule();
}
Hiérarchie de 492 classes endettées, aucune gouvernance ni auditabilité
model-map : Mapping bijectif typé de clef / valeur / modèle. Utilisé chez LesFurets comme dictionnaire front, pour notre modèle de donnée C*, etc.
public class Account extends Identity {
@SamplePath(field = SampleFieldId.LOGIN,
readable = "account login")
private String login;
@SamplePath(field = SampleFieldId.PASSWD,
readable = "account password")
private String password;
}
Grâce aux annotations sur le domain model, la génération de code crée une classe FieldIdInfo
contenant les clefs typées
public static final EnumFieldInfo<Country> COUNTRY = FieldInfoProvider
.<Country> enumField()
.fieldId(SampleFieldId.COUNTRY)
.readable("account country")
.type(Country.class)
.build(ALL);
SampleModel model = new SampleModel();
model.setAccount(new Account());
model.getAccount().setEmail("softshake@geneva.ch");
System.out.println(model.getAccount().getEmail());
FieldModel fieldModel = new SampleModelWrapper(model);
System.out.println(fieldModel.<String> get(EMAIL));
fieldModel.set(EMAIL, "lesfurets@gmail.com");
System.out.println(fieldModel.<String> get(EMAIL));
Utilisation de jOOQ (Java Object Oriented Query) dans notre code base
jOOQ generates Java code from your database and lets you build type safe SQL queries through its fluent API
DSL.using(conn)
.selectFrom(BO_INSURANCE)
.where(BO_INSURANCE.STATUS.equal(CONFIRMED.getCode()))
.stream()
.map(InsuranceDAO::InsuranceDto)
.collect(toList());
On s'inspire de jOOQ qui introspecte la base de données et génère des classes d'info typées (équivalent aux FieldInfo
)
public final TableField<Record, Integer> ID = createField(
"id", org.jooq.impl.SQLDataType.INTEGER.nullable(false), this, "");
compliance : les règles correspondent aux cahiers des charges
auditabilité : validation sans regarder le contenu du code
gouvernance : maintenance du catalogue de règles
expressivité : efficacité pour les développeurs
Le point d'entré est DOOV#when(StepCondition)
et l'opération StepWhen#validate
permet de retourner la règle
DOOV.when(accountEmail().matches("\\w+[@]\\w+\\.com")
.or(accountEmail().matches("\\w+[@]\\w+\\.fr")))
.validate()
.withMessage("email finishes with .com or .fr");
Une règle en langage naturel est disponible avec ValidationRule#readable
System.out.println(EMAIL_VALID.readable());
> When (email matches '\w+[@]\w+\.com' or email matches '\w+[@]\w+\.fr')
> validate with message "email finishes with .com or .fr"
Il est possible d'ajouter une règle dans un ou plusieurs registry ValidationRule#registerOn(Registry)
DOOV.when(accountEmail().matches("\\w+[@]\\w+\\.com")
.or(accountEmail().matches("\\w+[@]\\w+\\.fr")))
.validate()
.registerOn(REGISTRY_ACCOUNT);
L'opération terminale ValidationRule#executeOn(FieldModel)
exécute la règle
REGISTRY_ACCOUNT.stream()
.map(rule -> rule.executeOn(model))
.filter(Result::isInvalid)
.map(Result::message)
.collect(toList());
Les conditions sont disponibles par types, et les arguments sont "type safe" et validés par le compilateur
DOOV.when(userAccountCreation().after(LocalDate.of(2000, 01, 01))).validate();
// ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
// date field is type safe here
"In computer science, an abstract syntax tree (AST), or just syntax tree, is a tree representation of the abstract syntactic structure of source code written in a programming language"
L'arbre de syntaxe commence toujours par un token when
, puis on compose un prédicat :
matchAll
, matchAny
, ou matchOne
L'AST reprend les opérations classiques sur les prédicats numériques:
lesserThan
, greaterThan
, etc. qui peuvent être appliquées
à une valeur numérique ou un autre champ numérique
On retrouve les opérations de base sur les String
:
matches
et contains
qui prennent en paramètre des expressions régulières
Les champs Temporal
possèdent des opérateurs qui peuvent appliquer des fonctions.
today()
, todayPlusDays()
, etc. permettent de définir des dates qui seront
évaluées à l'exécution de la règle.
Essayons d'écrire une règle de validation sur une IHM
public class Account extends Identity {
@SamplePath(field = SampleFieldId.TIMEZONE,
readable = "account timezone")
private Timezone timezone;
@SamplePath(field = SampleFieldId.PHONE_NUMBER,
readable = "account phone number")
private String phoneNumber;
@SamplePath(field = SampleFieldId.EMAIL,
readable = "account email")
private String email;
@SamplePath(field = SampleFieldId.EMAIL_ACCEPTED,
readable = "account email accepted")
private boolean acceptEmail;
@SamplePath(field = SampleFieldId.EMAILS_PREFERENCES,
readable = "account préférences mail")
private Collection emailTypes = new HashSet<>();
}
public static boolean validateAccount(User user, Account account, Configuration config) {
if (config == null) {
return false;
}
if (user == null || user.getBirthDate() == null) {
return false;
}
if (account == null || account.getCountry() == null || account.getPhoneNumber() == null) {
return false;
}
if (YEARS.between(user.getBirthDate(), LocalDate.now()) >= 18
&& account.getEmail().length() <= config.getMaxEmailSize()
&& account.getCountry().equals(Country.FR)
&& account.getPhoneNumber().startsWith("+33")) {
return true;
}
return false;
}
DOOV.when(userBirthdate().ageAt(today()).greaterOrEquals(18L)
.and(accountEmail().length().lesserOrEquals(configurationMaxEmailSize()))
.and(accountCountry().eq(Country.FR))
.and(accountPhoneNumber().startsWith("+33")))
.validate()
.registerOn(REGISTRY_ACCOUNT, VALID_ACCOUNT);
On utilise JMH pour évaluer les performances du DSL
Benchmark Mode Samples Mean Mean error Units
o.m.BenchmarkOldRule.valid_email thrpt 25 1860.553 42.269 ops/ms
o.m.BenchmarkRule.valid_email thrpt 25 1733.465 18.461 ops/ms
Le DSL est très proche des performances d'une règle POJO
Les règles du DSL sont fortements typés : les erreurs sont détectés à la compilation
public class Account {
@NotNull @Email
private Email email;
}
Bean validation exprime des contraintes sur les champs : difficile de faire de la validation croisée
public class Account {
@Pattern(regexp = "(FR)|(UK)")
private String country;
@Pattern(regexp = "???")
private String phoneNumber;
}
Bean validation peut faire des contrôles de syntaxe, dOOv peut faire en plus des contrôles de cohérence : ce qui est nécessaire pour un catalogue de règles métiers
Les règles DSL ne sont donc pas directement sur le modèle
Bean validation n'est pas exprimé en langage naturel et n'a pas d'arbre de syntaxe
@Size(min = 10, max = 200,
message = "About Me must be between 10 and 200 characters")
private String aboutMe;
userAboutMe().length().between(10, 200).validate().readable()
> When user about me length is between 10 and 200, validate with message...
Utilisation du registry et du readable afficher les règles par groupe
Registry.ACCOUNT.stream()
.map(ValidationRule::readable)
.forEach(System.out::println);
When (account email matches \w+[@]\w+\.com or account email matches \w+[@]\w+\.fr),
validate with message "email finishes with .com or .fr"
When (match any [((account country equals FR and ...),
((account country equals UK and ...)]),
validate with empty message
L'arbre de syntaxe permet d'afficher la règle de manière structurée
When
match any
- account country equals 'FR' and
account language equals 'FR' and
account phone number starts with '+33'
- account country equals 'UK' and
account language equals 'EN' and
account phone number starts with '+45'
validate with
empty message
Expression des règles en langage naturel
DOOV.when(emprunteurNaissance().ageAt(dateDeblocagePret()))
.greaterOrEqual(90))
.excludeFormules()
.exclusionRule();
Quels sont les avantages de notre DSL vs POJO ?
Ajouter doov-core
pour dépendre du DSL et des annotations
<dependency>
<groupId>io.doov</groupId>
<artifactId>doov-core</artifactId>
<version>LATEST</version>
</dependency>
Annoter votre modèle, exécuter le code générateur, puis écrivez vos règles :
DOOV...
contains()
, isEmpty()
, hasSize()
, etc