DSL.using(java)
   .toGoBeyond(BeanValidation)
   .at(SoftShake.eq(ch));

Welcome to the Furets !

@dubreuia – Alexandre Dubreuil

  • Québécois exilé à Paris depuis 2009
  • Software Architect at LesFurets.com


@gdigugli – Gilles Di Guglielmo

  • Designer of sweet cooked software since 1999
  • Software Architect at LesFurets.com

  • 1 website, 5 Insurance Products : Car, Health, Home, Bike, Loan
  • 1 codebase, 450k lines of code, 60k unit tests, 150 selenium tests
  • 22 Developers, 2 DevOps, 4 Architects
  • 19 production servers including Load balancers, Frontend, Backend, Databases, BI
  • 1 release per day
  • 9 years of code history
  • 3M quotes/year, 40% of market share, 4M of customers

Fluent API

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);
              

Fluent API

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(" ");
              

Fluent API

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"));
              

DSL

"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"

Génèse du DSL : étape 1

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);
    }
}
              

Génèse du DSL : étape 1


public ExclusionRule exclusionRule() {
    return DOOV.when(dateContrat().after(todayPlusDays(60)))
                    .excludeFormules()
                    .withMessage(DATE_EFFET_PLUS_60_JOURS)
                    .exclusionRule();
}
              

Exclusions assureurs

Hiérarchie de 492 classes endettées, aucune gouvernance ni auditabilité

Génèse du DSL : étape 2

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;

}
              

Model-map : socle du DSL

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);
              

Live code 1 : model-map accesseurs du modèle


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));
              

Génèse du DSL : étape 3

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());
              

Génèse du DSL : étape 3

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, "");
              

Pourquoi un DSL ?

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

Design du DSL : point d'entré et règle

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");
              

Design du DSL : langage naturel

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"
              

Design du DSL : gouvernance

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);
              

Design du DSL : exécution

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());
              

Design du DSL : type safety

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
              

Design de l'arbre de syntaxe

"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"

Design de l'arbre de syntaxe : DSL, when, matchAny ...

L'arbre de syntaxe commence toujours par un token when, puis on compose un prédicat :

  • En utilisant directement un champ
  • En composant des prédicats sur plusieurs champs
    avec matchAll, matchAny, ou matchOne

Design de l'arbre de syntaxe : Champs Numeric et Enuméré

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

Design de l'arbre de syntaxe : Champs String et Boolean

On retrouve les opérations de base sur les String :
matches et contains qui prennent en paramètre des expressions régulières

Design de l'arbre de syntaxe : Champs Date, Time et DateTime

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.

It's time to play !

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<>();

}
              

Live code 2 : convertir en DSL


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;
}
              

Live code 2 : DSL


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);
              

Performance - Java Microbenchmark Harness

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

Beyond Bean Validation : types

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;

}
              

Beyond Bean Validation : validation complexe

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;

}
              

Beyond Bean Validation : syntaxe versus cohérence

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

Beyond Bean Validation : langage naturel

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;
              

DSL


userAboutMe().length().between(10, 200).validate().readable()
> When user about me length is between 10 and 200, validate with message...
              

DSL : Gouvernance des règles

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
              

DSL : Auditabilité

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
              

DSL : Expressivité

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 ?

Avantages

  • DSL s'exprime dans une syntaxe declarative
  • DSL peut limiter des contraintes métiers
  • DSL permet l'auditabilité et la gouvernance
  • DSL peut afficher quels noeuds de l'arbre ne valident pas

Inconvénients

  • DSL s'exprime dans une syntaxe declarative
  • DSL nécessite l'annotation et la code génération du modèle
  • DSL plus difficile à debug

Pre-requis / installation

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...
              

What's next

  • Rename model-map to dOOv (domain object oriented validation) DONE!
  • Support most java types of standard library DONE!
  • Add different readable formats (html, markdown, etc.) DONE!
  • Collections support with contains(), isEmpty(), hasSize(), etc
  • Add extension points to define own DSL conditions
  • Add code generator options for different styles

Enjoy dOOv.org

http://www.dOOv.org

Thank You!