BT

Diffuser les Connaissances et l'Innovation dans le Développement Logiciel d'Entreprise

Contribuez

Sujets

Sélectionner votre région

Accueil InfoQ Articles Fluent-API : Créer Un Code Plus Facile Et Plus Intuitif Avec Une API Fluent

Fluent-API : Créer Un Code Plus Facile Et Plus Intuitif Avec Une API Fluent

Points Clés

  • Qu'est-ce qu'une API Fluent ?
  • Comment créer une API Fluent avec Java ?
  • Les compromis avec les API Fluent

Nous savons que, dans un projet logiciel, rien ne remplace une bonne documentation. Cependant, il est également nécessaire de faire attention à l'intuitivité du code écrit. Après tout, plus le code est simple et naturel, meilleure est son expérience pour les utilisateurs.

La première règle pour tout développeur est la suivante : nous oublierons tout ce dont nous devons nous souvenir, une API qui vous "force" à vous souvenir est une preuve cruciale d'échec.

C'est pourquoi, dans cet article, nous allons présenter le sujet et vous montrer comment créer une API fluent à partir du concept Fluent-API.

 

Qu'est-ce qu'une Fluent-API, ou API Fluent ?

Lorsque l'on parle dans le contexte du génie logiciel, une fluent-API est une API orientée objet dont la conception est largement basée sur le chaînage de méthodes.

Ce concept, créé en 2005 par Eric Evans et Martin Fowler, vise à augmenter la lisibilité du code en créant un langage spécifique au domaine (DSL).

En pratique, créer une API fluent, c'est développer une API dans laquelle il n'est pas nécessaire de mémoriser les prochaines étapes ou méthodes, permettant une séquence naturelle et continue comme s'il s'agissait d'un menu d'options.

Cette cadence naturelle fonctionne de la même manière qu'un restaurant ou même une chaîne de restauration rapide en ce sens que lorsque vous préparez un plat, les options varient en fonction des choix que vous faites. Si, par exemple, vous choisissez un sandwich au poulet, les accompagnements vous seront proposés en fonction du plat choisi et ainsi de suite.

API fluent dans le contexte Java

Dans le monde Java, on peut penser à deux exemples célèbres de ce type d'implémentation.

Le premier est le framework JOOQ, un projet mené par Lukas Eder, qui vise à faciliter la communication entre Java et les bases de données relationnelles. La plus grande différence de JOOQ est qu'il est orienté données, ce qui permet d'éviter et/ou de réduire le problème d'impédance, ou la perte, associé aux relations et à l'orientation objet.

Query query = create.select(BOOK.TITLE, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
                    .from(BOOK)
                    .join(AUTHOR)
                    .on(BOOK.AUTHOR_ID.eq(AUTHOR.ID))
                    .where(BOOK.PUBLISHED_IN.eq(1948));

String sql = query.getSQL();
List<Object> bindValues = query.getBindValues();

Un autre exemple est celui des bases de données non relationnelles, c'est-à-dire NoSQL, dans le cadre des spécifications du monde Java d'entreprise. Parmi elles, Jakarta EE, qui est la première spécification du genre et qui, sous l'égide de la Fondation Eclipse, est devenue Jakarta NoSQL.

Le but de cette spécification est d'assurer une communication fluent entre les bases de données NoSQL et Java.

DocumentQuery query = select().from("Person").where(eq(Document.of("_id", id))).build();
Optional<Person> person = documentTemplate.singleResult(query);
System.out.println("Entity found: " + person);

D'une manière générale, une API fluent est divisée en trois parties :

  1. L'objet ou le résultat final : dans l'ensemble, l'API fluent ressemble au modèle de conception builder, mais la plus grande dynamique est couplée avec DSL. Dans les deux cas, le résultat final a tendance à être une instance pour représenter le résultat d'un processus ou d'une nouvelle entité.

  2. Les options : dans ce cas, ce sont les collections d'interfaces ou de classes qui serviront comme "notre menu interactif". L'idée est qu'à partir d'une action, n'afficher que les options disponibles pour l'étape suivante, en suivant une séquence intuitive.

  3. Le résultat : après tout ce processus, la réponse peut ou non aboutir à une instance que ce soit pour une entité, un processus, etc. Le point important est que le résultat doit être un résultat valide.

API fluent en pratique

Pour démontrer une partie de ce concept, nous allons créer une commande de sandwich avec comme résultat attendu une commande avec le prix d'achat correspondant. Le flux sera comme indiqué ci-dessous.

Bien sûr, il existe plusieurs façons d'implémenter cette fonctionnalité en API fluent, mais nous avons opté pour une version extrêmement simple.

Comme nous avons déjà évoqué les trois parties d'une API - objet, options et résultat -, nous allons commencer par la commande qui sera représenté par l'interface « Order ». Un point important est que cette interface a quelques interfaces, qui seront chargées de fournir nos options.

public interface Order {


    interface SizeOrder {
        StyleOrder size(Size size);
    }

    interface StyleOrder {

        StyleQuantityOrder vegan();

        StyleQuantityOrder meat();
    }

    interface StyleQuantityOrder extends DrinksOrder {
        DrinksOrder quantity(int quantity);
    }


    interface DrinksOrder {
        Checkout softDrink(int quantity);

        Checkout cocktail(int quantity);

        Checkout softDrink();

        Checkout cocktail();

        Checkout noBeveragesThanks();
    }

    static SizeOrder bread(Bread bread) {
        Objects.requireNonNull(bread, "Bread is required o the order");
        return new OrderFluent(bread);
    }
}

Le résultat de cette API sera notre classe de commande. Elle contiendra le sandwich, la boisson et leurs quantités respectives.

Un petit complément avant de revenir au tutoriel

Un point sur lequel nous ne nous concentrerons pas dans cet article, mais qui mérite d'être mentionné, est lié à la représentation de données monétaires.

Pour les opérations numériques, l'idéal est d'utiliser BigDecimal. C'est parce que, en suivant des références comme le livre Java Effective et le blog When Make a Type, nous comprenons que les types complexes ont besoin d'un type unique. Ce raisonnement, couplé au pragmatisme du « ne vous répétez pas » (DRY, Dont Repeat Yourself ou ne vous répétez pas), le résultat est l'utilisation de la spécification Java pour des données monétaires : Money API.

import javax.money.MonetaryAmount;
import java.util.Optional;

public class Checkout {

    private final Sandwich sandwich;

    private final int quantity;

    private final Drink drink;

    private final int drinkQuantity;

    private final MonetaryAmount total;

  //...
}

La dernière étape est la mise en œuvre de l'API. Elle sera responsable de la partie « moche » du code, rendant l'API magnifique.

La liste de prix sera placée directement dans le code, puisque nous n'utilisons pas de base de données, ou autre référentiel de données, et notre intention est de rendre l'exemple aussi simple que possible. Mais il convient de souligner que, dans un environnement réel, ces informations seraient dans une base de données ou dans un service.

import javax.money.MonetaryAmount;
import java.util.Objects;

class OrderFluent implements Order.SizeOrder, Order.StyleOrder, Order.StyleQuantityOrder, Order.DrinksOrder {

    private final PricingTables pricingTables = PricingTables.INSTANCE;

    private final Bread bread;

    private Size size;

    private Sandwich sandwich;

    private int quantity;

    private Drink drink;

    private int drinkQuantity;

    OrderFluent(Bread bread) {
        this.bread = bread;
    }

    @Override
    public Order.StyleOrder size(Size size) {
        Objects.requireNonNull(size, "Size is required");
        this.size = size;
        return this;
    }

    @Override
    public Order.StyleQuantityOrder vegan() {
        createSandwich(SandwichStyle.VEGAN);
        return this;
    }

    @Override
    public Order.StyleQuantityOrder meat() {
        createSandwich(SandwichStyle.MEAT);
        return this;
    }

    @Override
    public Order.DrinksOrder quantity(int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("You must request at least one sandwich");
        }
        this.quantity = quantity;
        return this;
    }

    @Override
    public Checkout softDrink(int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("You must request at least one sandwich");
        }
        this.drinkQuantity = quantity;
        this.drink = new Drink(DrinkType.SOFT_DRINK, pricingTables.getPrice(DrinkType.SOFT_DRINK));
        return checkout();
    }

    @Override
    public Checkout cocktail(int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("You must request at least one sandwich");
        }
        this.drinkQuantity = quantity;
        this.drink = new Drink(DrinkType.COCKTAIL, pricingTables.getPrice(DrinkType.COCKTAIL));
        return checkout();
    }

    @Override
    public Checkout softDrink() {
        return softDrink(1);
    }

    @Override
    public Checkout cocktail() {
        return cocktail(1);
    }

    @Override
    public Checkout noBeveragesThanks() {
        return checkout();
    }

    private Checkout checkout() {
        MonetaryAmount total = sandwich.getPrice().multiply(quantity);
        if (drink != null) {
            MonetaryAmount drinkTotal = drink.getPrice().multiply(drinkQuantity);
            total = total.add(drinkTotal);
        }
        return new Checkout(sandwich, quantity, drink, drinkQuantity, total);
    }

    private void createSandwich(SandwichStyle style) {
        MonetaryAmount breadPrice = pricingTables.getPrice(this.bread);
        MonetaryAmount sizePrice = pricingTables.getPrice(this.size);
        MonetaryAmount stylePrice = pricingTables.getPrice(SandwichStyle.VEGAN);
        MonetaryAmount total = breadPrice.add(sizePrice).add(stylePrice);
        this.sandwich = new Sandwich(style, this.bread, this.size, total);
    }
}

Le résultat est une API qui nous renverra le paiement de manière très simple et intuitive.

     Checkout checkout = Order.bread(Bread.PLAIN)
                .size(Size.SMALL)
                .meat()
                .quantity(2)
                .softDrink(2);

En quoi l'API Fluent est-elle différente des autres normes pour API ?

Il est très courant d'avoir une comparaison entre deux normes pour API, qui sont Builder et Fluent-API. La raison en est qu'ils utilisent tous les deux des méthodes pour le processus de création d'une instance.

Cependant, Fluent-API est "lié à un DSL", et cela force un chemin facile pour le faire. Mais pour rendre ces différences encore plus évidentes, nous avons séparé les points importants pour chacun de ces modèles :

Builder :

  • Il a tendance à être beaucoup plus facile à mettre en œuvre

  • Il n'est pas clair de savoir quelles méthodes de construction sont nécessaires

  • La grande majorité des problèmes se produiront au moment de l'exécution

  • Il existe des outils et des frameworks qui le créent automatiquement

  • Nécessite une validation plus forte dans la méthode de génération pour vérifier quelles méthodes obligatoires n'ont pas été invoquées

Fluent-API :

  • Il a tendance à avoir une implémentation plus complexe, en particulier avec plus d'options

  • Force l'utilisateur/développeur à suivre le flux et rend plus visible les champs facultatifs

  • Si le chemin n'est pas suivi, il est possible que le code ne se compile pas

Builder et API fluent :

  • Il est important que, pour chaque méthode, il y ait une validation et que l'erreur soit levée si le paramètre est invalide, souvenez-vous de la prémisse Fail Fast 

  • Il doit retourner un objet valide à la fin du processus.

Et maintenant, est-il plus facile de comprendre les similitudes et les différences entre ces normes pour API ?

C'était notre introduction au concept de Fluent-API ou d'API Fluent. Comme pour toutes les solutions, il n'y a pas de « solution miracle », car il existe une grande complexité qui n'est souvent pas justifiée pour l'ensemble du processus.

Notre collègue Rafael Ponte dit souvent que « plus il est facile d'utiliser l'API pour le client, plus il sera difficile de la concevoir et de l'implémenter pour le développeur ».

C'est un excellent outil qui aide à créer une sécurité intégrée pour vous et les autres utilisateurs. Vous pouvez en savoir plus sur le sujet sur le Github de Sou Java.

 

A propos de l'auteur

Otávio Santana est un ingénieur logiciel avec une vaste expérience dans le développement open source, avec plusieurs contributions à JBoss Weld, Hibernate, Apache Commons et à d'autres projets. Axé sur le développement multilingue et les applications haute performance, Otávio a travaillé sur de grands projets dans les domaines de la finance, du gouvernement, des médias sociaux et du commerce électronique. Membre du comité exécutif du JCP et de plusieurs groupes d'experts JSR, il est également un champion Java et a reçu le JCP Outstanding Award et le Duke's Choice Award.

 

Evaluer cet article

Pertinence
Style

Contenu Éducatif

BT