Ce talk contient des silver bullets pouvant rendre dépendant les adeptes du culte du cargo

@JulienTopcu

@JulienTopcu

?🤪?

@JulienTopcu

Je veux rechercher les trains qui desservent ma destination

Je crée une ressource "Recherche"

@JulienTopcu

C

R

U

D

omment

endre

seless

DD

@JulienTopcu

Interface Uniforme

Architectural Styles and the Design of Network-based Software Architectures - Roy Fielding

@JulienTopcu

Problème d'impédance

© Paramount Pictures - Interstellar

@JulienTopcu

© Paramount Pictures - Interstellar

Julien Topçu

Tech Coach

beyondxscratch.com

julien.topcu@owasp.org

craftsrecords.org

@JulienTopcu

Informagicien™

@JulienTopcu

© Marvel Studio - "Doctor Strange"

🤯

@JulienTopcu

REST next level :
Ecrire des APIs web
orientées métier

@JulienTopcu

PB#1 : Fausses Croyances

© Lucasfilms - "STAR WARS: Revenge of the Sith"

@JulienTopcu

interface BookSpaceTrains {
    Booking fromTheSelectionOf(Search search)
}
POST /bookings

@JulienTopcu

interface RebookSpaceTrains {

    Booking atANewDate(UUID bookingId, LocalDate newdate)
}
POST /bookings
interface BookSpaceTrains {
    Booking fromTheSelectionOf(Search search)
}
POST /bookings

@JulienTopcu

bookSpaceTrains.fromTheSelectionOf(search)
rebookSpaceTrains.atANewDate(bookingId,newdate)
POST /bookings
{
  "searchId" : "...",
}
POST /bookings
{
  "bookingId" : "...",
  "newDate": "..."
}

Je réserve ce que j'ai sélectionné

Je re-réserve le même train à une date ultérieure

Perte de la notion de sélection

Confusion sémantique avec le cas du dessus

@JulienTopcu

BookSpaceTrains.fromTheSelectionOf(search)
rebookSpaceTrains.atANewDate(bookingId,newdate)
POST /rebookings
{
  "searchId" : "...",
}
POST /bookings
{
  "bookingId" : "...",
  "newDate": "..."
}

@JulienTopcu

Re-réserver

est une action,
et non pas une entité métier

Pseudo-fonctionnel

@JulienTopcu

REST

ne doit pas piloter

la conception de votre métier !!!

@JulienTopcu

Pseudo-fonctionnel

Rebooking

Complexité Accidentelle

@JulienTopcu

POST /bookings/{id}/rebook
{
  "newDate": "..."
}
booking {
  "bookingId": "{newId}",
  "date": "{newDate}"
}
GET /bookings/{newId}

@JulienTopcu

Nouns VS Verbs

@JulienTopcu

Binding Semantics to URI

 At no time whatsoever do the server or client software need to know or understand the meaning of a URI -- they merely act as a conduit through which the creator of a resource (a human naming authority) can associate representations with the semantics identified by the URI

Architectural Styles and the Design of Network-based Software Architectures - Roy Fielding

@JulienTopcu

C'est l'un des secrets du tour de magie !

🧙🏿‍♂️

@JulienTopcu

PB#2 :
Confondre modèle et métier

© Lucasfilms - "STAR WARS: Revenge of the Sith"

@JulienTopcu

Le métier 👩‍💼

@JulienTopcu

Search
|_ availableSpaceTrains
|_ selection
  |_ selectedSpaceTrains
PATCH /searches/{id}/selection

Le Modèle 📐

@JulienTopcu

Je sélectionne un train et un tarif

Je mets à jour ma sous-ressource "Selection" dans ma ressource "Recherche"

@JulienTopcu

Au pire

@JulienTopcu

Au Mieux

@JulienTopcu

Complexité accidentelle d'adaptation

Exposer son modèle à son consommateur

@JulienTopcu

PB#3 :
La trahison de l'encapsulation

© Lucasfilms - "STAR WARS: Revenge of the Sith"

@JulienTopcu

class Search {

    UUID id
    Criteria criteria
    SpaceTrains spaceTrains
    Selection selection

    UUID getId() {...}
    void setId(UUID id) {...}
    Criteria getCriteria() {...}
    void setCriteria(Criteria criteria) {...}

    /* only getters and setters */
}

Modèle Anémique

@JulienTopcu

class Search { //Aggregate
    /*attributes*/
    /*no setters*/
    Search(Selection selection, SpaceTrains spacetrains, ...){...}
    
    Search selectSpaceTrainWithFare(UUID spaceTrainId, UUID fareId) {}
    Boolean isSelectionComplete() {...}
}

Domain-Driven Design

on encapsule les états
pour exposer des comportements

@JulienTopcu

GET /searches/{id}
{
        "id": "c956d89b-73fc-49ce-a911-f0058c0672f6",
        "criteria": {/*...*/},
        "spaceTrains": [/*...*/],
        "selection": {/*...*/}
}

REST

on désencapsule les comportements

pour exposer des états

@JulienTopcu

© Lucasfilms - "STAR WARS: Attack of the Clones"

@JulienTopcu

bookSpaceTrains.fromTheSelectionOf(search)
rebookSpaceTrains.atANewDate(bookingId,newdate)
POST /bookings
{
  "searchId" : "...",
}
POST /bookings
{
  "bookingId" : "...",
  "newDate": "..."
}

On est obligé de lire le contenu du payload pour en déduire le cas d'utilisation

Perte de sens par la désencapsulation

@JulienTopcu

@JulienTopcu

PATCH /searches/{id}/selection
[
  { 
    "op": "add", 
    "path": "/selectedSpaceTrains/-", 
    "value": {"trainNumber" : "MOON421", "fare" : "FIRST"}
  }
]

Complexité accidentelle d'adaptation

Désencapsulation

@JulienTopcu

POST /searches/{id}/spaceTrains/{number}/fares/{code}/select
Search /* Modèle du Domaine */
|_ availableSpaceTrains
|_ selection
  |_ selectedSpaceTrains

@JulienTopcu

POST /searches/{id}/spaceTrains/{number}/fares/{code}/select

Mais comment le front va t-il savoir qu'il peut faire ça ???

© Touchstone Pictures - "The Hitchhiker's Guide to the Galaxy"

@JulienTopcu

PB#4 : L'API Anémique

© Marvel Studio - "Guardians of the Galaxy Vol.1"

en terme de workflow...

@JulienTopcu

POST /searches   /*RoundTrip Criteria Submitted*/

GET  /searches/{id}/spaceTrains?bound=OUTBOUND

POST /searches/{id}/spaceTrains/{number}/fares/{code}/select

GET  /searches/{id}/spaceTrains?bound=INBOUND

POST /searches/{id}/spaceTrains/{number}/fares/{code}/select

POST /bookings   /*Trip Booked*/

Comment sait-on que l'on doit procéder dans cet ordre ?

@JulienTopcu

Qu'est ce-qui empêche le front de :

  • Afficher les trains retours avant les allers ?

  • Tenter de récupérer des retours pour des allers simples ?

  • Appeler l'endpoint de création d'un booking alors que la sélection n'est pas complète ?

@JulienTopcu

RIEN car nos APIs sont anémiques

Le front est le seul à implémenter le workflow métier !

@JulienTopcu

if(searchType == "roundtrip" && outBoundIsSelected && inBoundIsSelected){
  displayBookingButton()
}

@JulienTopcu

if(searchType == "roundtrip" && outBoundIsSelected && inBoundIsSelected){
  displayBookingButton()
}

Une partie de la logique métier est déportée dans le front...

@JulienTopcu

Il faut encapsuler le workflow et son pilotage dans l'API !

© Paramount Pictures - "Star Trek: The Next Generation"

@JulienTopcu

© martinfowler.com - "Richardson Maturity Model"

@JulienTopcu

HATEOAS

Un système de liens (URLs) permettant de :

  • Exposer les relations entre les objets métiers

  • Découvrir le domaine

  • Encapsuler le workflow métier

Hypermedia As The Engine of Application State

@JulienTopcu

HATEOAS

@JulienTopcu

Les Liens

Le front ne doit pas chercher à comprendre comment les URLs sont formées !

Un meilleur découplage entre la consommation et l'implémentation du modèle métier

@JulienTopcu

  • Des fausses croyances sur la nomenclature REST
  • L'utilisation de verbes enrichit la sémantique de l'API
     
  • Exposition du modèle en direct
  • Faire une adaptation orientée consommation côté back
     
  • Exposition des états des objets métiers
  • Encapsulation en comportements métiers rattachés aux objets qui en sont responsables dans l'API
     
  • Une définition de workflow inexistante (API anémique)
  • Utilisation de HATEOAS pour le pilotage du workflow

Un problème d'impédance de REST ?

✔️

✔️

✔️

✔️

@JulienTopcu

  • Des fausses croyances sur la nomenclature REST
  • L'utilisation de verbes enrichit la sémantique de l'API
     
  • Exposition du modèle en direct
  • Faire une adaptation orientée consommation côté back
     
  • Exposition des états des objets métiers
  • Encapsulation en comportements métiers rattachés aux objets qui en sont responsables dans l'API
     
  • Une définition de workflow inexistante (API anémique)
  • Utilisation de HATEOAS pour le pilotage du workflow

Un problème d'impédance de REST ?

✔️

✔️

✔️

✔️

@JulienTopcu

Repository

@JulienTopcu

MERCI !🙏🏻

© Paramount Pictures - "Star Trek"

@JulienTopcu

© Kirsten Luce for The New York Times

@JulienTopcu

© Kirsten Luce for The New York Times

@JulienTopcu

© Kirsten Luce for The New York Times

@JulienTopcu

© Kirsten Luce for The New York Times

© Cottonbro - Pexels

C

R

U

D

@JulienTopcu

Je sélectionne un train et un tarif

Je mets à jour ma sous-ressource "Selection" dans ma ressource "Recherche"

@JulienTopcu

Outbound (Aller)

Inbound (Retour)

@JulienTopcu

Bound

Outbound ?

Inbound ?

???

@JulienTopcu

JourneyOrder

0

2

1

@JulienTopcu

BookSpaceTrains.fromTheSelectionOf(search)
rebookSpaceTrains.atANewDate(bookingId,newdate)
POST /bookings
{
  "searchId" : "...",
}
POST /bookings
{
  "bookingId" : "...",
  "newDate": "..."
}

@JulienTopcu

Criteria⚙️

Search🔍

Selection✔️

Ubiquitous Language

🚅

@JulienTopcu

Les Liens

Une étiquette sémantique + une URL

@JulienTopcu

Les Liens

Une étiquette sémantique + une URL

Le front ne doit pas chercher à comprendre comment les URLs sont formées !

@JulienTopcu

Ubiquitous Language

Booking🎫

@JulienTopcu

@JulienTopcu

interface SearchForSpaceTrains {
    Search satisfying(Criteria criteria)
}
POST /searches

@JulienTopcu

From the Earth to the Moon

@JulienTopcu

@JulienTopcu

POST /searches
GET /searches/{id}

🔍

@JulienTopcu

POST /searches
GET /searches/{id}

🔍

✔️

PATCH /searches/{id}/selection
GET /searches/{id}/selection

@JulienTopcu

POST /searches
GET /searches/{id}
PATCH /searches/{id}/selection
GET /searches/{id}/selection
POST /bookings
GET /bookings/{id}

✔️

🔍

🎫

@JulienTopcu

Dossier

✈️🚅🚘🏨

@JulienTopcu

MetaDossier

✈️🚅🚘🏨

@JulienTopcu

MetaDossier

LightweightDossier

@JulienTopcu

BookSpaceTrains.fromTheSelectionOf(search)
rebookSpaceTrains.atANewDate(bookingId,newdate)
POST /rebookings
{
  "searchId" : "...",
}
POST /bookings
{
  "bookingId" : "...",
  "newDate": "..."
}

@JulienTopcu

class Search {}
interface SearchManager {
    Search createSearch(Criteria criteria)
    void updateSearch(UUID searchId, Selection selection)
    Booking toBooking(UUID searchId)
}

@JulienTopcu

class Search { //Aggregate
    /*attributes*/
    /*no setters*/
    Search(Selection selection, SpaceTrains spacetrains, ...){...}
    
    Search selectSpaceTrainWithFare(UUID spaceTrainId, UUID fareId) {}
    Boolean isSelectionComplete() {...}
}

interface SearchForSpaceTrains { //DomainService
    Search satisfying(Criteria criteria)
}

interface BookSpaceTrains { //DomainService
    Booking fromTheSelectionOf(Search thisSearch)
}

@JulienTopcu

interface SearchManager {
    Search createSearch(Criteria criteria)
    void updateSearch(UUID searchId, Selection selection)
    // Search <>-- SpaceTrains && Selection <>-- SpaceTrains
    void updateSearch(UUID searchId, SpaceTrains spaceTrains)
    Booking toBooking(UUID searchId)
}

@JulienTopcu

interface SearchManager {
    Search createSearch(Criteria criteria)
    void updateSearch(UUID searchId, Selection selection)
    // Search <>-- SpaceTrains && Selection <>-- SpaceTrains
    void updateSearch(UUID searchId, SpaceTrains spaceTrains)
    Booking toBooking(UUID searchId)
}

@JulienTopcu

interface SearchManager {
    Search createSearch(Criteria criteria)
    void updateSearch(UUID searchId, Selection selection)
    // Search <>-- SpaceTrains && Selection <>-- SpaceTrains
    void updateSearch(UUID searchId, SpaceTrains spaceTrains)
    Booking toBooking(UUID searchId)
}

@JulienTopcu

interface SearchManager {
    Search createSearch(Criteria criteria)
    void updateSearch(UUID searchId, Selection selection)
    // Search <>-- SpaceTrains && Selection <>-- SpaceTrains
    void updateSearch(UUID searchId, SpaceTrains spaceTrains)
    Booking toBooking(UUID searchId)
}

???

@JulienTopcu

interface SearchManager {
    Search createSearch(Criteria criteria)
    void updateSearch(UUID searchId, Selection selection)
    // Search <>-- SpaceTrains && Selection <>-- SpaceTrains
    void updateSearch(UUID searchId, SpaceTrains spaceTrains)
    Booking toBooking(UUID searchId)
}

@JulienTopcu

interface ColumbiadExpressService {
    Boolean addSpaceTrain(UUID searchId,UUID spaceTrainId,UUID fareId)
    Boolean isSelectionComplete(UUID searchId)
}
interface SearchManager {
    Search createSearch(Criteria criteria)
    void updateSearch(UUID searchId, Selection selection)
    // Search <>-- SpaceTrains && Selection <>-- SpaceTrains
    void updateSearch(UUID searchId, SpaceTrains spaceTrains)
    Booking toBooking(UUID searchId)
}

@JulienTopcu

interface SearchManager {
    Search createSearch(Criteria criteria)
    void updateSearch(UUID searchId, Selection selection)
    // Search <>-- SpaceTrains && Selection <>-- SpaceTrains
    void updateSearch(UUID searchId, SpaceTrains spaceTrains)
    Booking toBooking(UUID searchId)
}
interface ColumbiadExpressService {
    Boolean addSpaceTrain(UUID searchId,UUID spaceTrainId,UUID fareId)
    Boolean isSelectionComplete(UUID searchId)
}

@JulienTopcu

interface SearchManager {
    Search createSearch(Criteria criteria)
    void updateSearch(UUID searchId, Selection selection)
    // Search <>-- SpaceTrains && Selection <>-- SpaceTrains
    void updateSearch(UUID searchId, SpaceTrains spaceTrains)
    Booking toBooking(UUID searchId)
}
interface ColumbiadExpressService {
    Boolean addSpaceTrain(UUID searchId,UUID spaceTrainId,UUID fareId)
    Boolean isSelectionComplete(UUID searchId)
}

???

@JulienTopcu

interface SearchManager {
    Search createSearch(Criteria criteria)
    void updateSearch(UUID searchId, Selection selection)
    // Search <>-- SpaceTrains && Selection <>-- SpaceTrains
    void updateSearch(UUID searchId, SpaceTrains spaceTrains)
    Booking toBooking(UUID searchId)
}
interface ColumbiadExpressService {
    Boolean addSpaceTrain(UUID searchId,UUID spaceTrainId,UUID fareId)
    Boolean isSelectionComplete(UUID searchId)
}

@JulienTopcu

© Mickael Jackson - Thriller

@JulienTopcu

class Search {

    UUID id
    Criteria criteria
    SpaceTrains spaceTrains
    Selection selection

    UUID getId() {...}
    void setId(UUID id) {...}
    Criteria getCriteria() {...}
    void setCriteria(Criteria criteria) {...}

    /* only getters and setters */
}

@JulienTopcu

class Search {
  SpaceTrains getSpaceTrains(){}
  
  SpaceTrains getSpaceTrains(Bound bound){}  
    
  Selection getSelection() {}
  Boolean isSelectionComplete() {}
  
  Search selectSpaceTrainWithFare(...) {}
  Search selectSeatOptions(...) {}
}
GET /searches/{id}/spacetrains
GET /searches/{id}/spacetrains?bound={bound}
GET /searches/{id}/selection
GET /searches/{id}/selection/complete

@JulienTopcu

interface SearchForSpaceTrains {
    infix fun satisfying(criteria: Criteria): Search // POST /searches
}

interface BookSomeSpaceTrains {
    infix fun `from the selection of`(search: Search): Booking // POST /bookings
}


data class Search(/*...*/) {
  
  fun selectSpaceTrainWithFare(/*...*/) : Search // PUT /searches/{id}/selection
  fun selectSeatOptions(/*...*/) : Search // PUT /searches/{id}/selection
  fun isSelectionComplete() : Boolean // GET /searches/{id}/selection
  
}

@JulienTopcu

/*Search*/
{
        "id": "c956d89b-73fc-49ce-a911-f0058c0672f6",
        "criteria": {/*...*/},
        "spaceTrains": [/*...*/],
        "selection": {/*...*/}
}

@JulienTopcu

@JulienTopcu

Les Liens

"selection": {
            "href": "http://localhost/searches/{id}/selection"
},

"create-booking": {
            "href": "http://localhost/bookings?searchId={id}"
},

"all-outbounds": {
            "href": "http://localhost/searches/{id}/spacetrains?journeyOrder=0&onlySelectable=false"
},

"outbounds-for-current-selection": {
            "href": "http://localhost/searches/{id}/spacetrains?journeyOrder=0&onlySelectable=true"
},

@JulienTopcu

Problèmes Majeurs

Bucket GraphQL

FALCOR

@JulienTopcu

GRAPHQL

@JulienTopcu

?

+

@JulienTopcu

1999

"price" : {
  "amount" : 1000,
  "currency" : "FR"
}

=> 1000 FR

@JulienTopcu

2002

"price" : {
  "amount" : 150,
  "currency" : "EUR"
}

=> 150 FR

@JulienTopcu

CAS Train

+

Systeme référentiel

Data as a Service

@JulienTopcu

Complexité accidentelle d'adaptation => Adaptation de isSelectionComplete de Search (aggreggat necessitant criteria) vers un /searches/../selection et les select* alors qu'ils devraient être niveau Search

@JulienTopcu

"Charge cognitive" => on se positionne sur une ressource selection dans l'API alors que l'utilisateur clique sur une fare dans un spacetrain

@JulienTopcu

Perte d'encapsulation mise a jours des états

JSON + CRUD => API anémique

PUT ../selection pour SeatOptions & Fares

POST /searches et bookings qui peuvent être vide

Le workflow métier ne ressort pas

Faire une représentation event storming du workflow et montrer qu'on perd ca dans l'API
Techniquement on peut créer un booking avant la recherche

@JulienTopcu

Impedance Mismatch

@JulienTopcu

=> Logique déportée dans le consommateur exemple quand est-ce qu'on est pret a booker

@JulienTopcu

6.2.4 Binding Semantics to URI

As mentioned above, a resource can have many identifiers. In other words, there may exist two or more different URI that have equivalent semantics when used to access a server. It is also possible to have two URI that result in the same mechanism being used upon access to the server, and yet those URI identify two different resources because they don't mean the same thing.

Semantics are a by-product of the act of assigning resource identifiers and populating those resources with representations. At no time whatsoever do the server or client software need to know or understand the meaning of a URI -- they merely act as a conduit through which the creator of a resource (a human naming authority) can associate representations with the semantics identified by the URI. In other words, there are no resources on the server; just mechanisms that supply answers across an abstract interface defined by resources. It may seem odd, but this is the essence of what makes the Web work across so many different implementations.

C

R

U

D

omment

endre

seless

DD

@JulienTopcu

C

R

U

D

@JulienTopcu

interface SearchForSpaceTrains {
    infix fun satisfying(criteria: Criteria): Search
}

interface BookSomeSpaceTrains {
    infix fun `from the selection of`(search: Search): Booking
}


data class Search(/*...*/) {
  
  fun getSpaceTrainWithNumber(wantedNumber: String) : SpaceTrain {}
  fun selectSpaceTrainWithFare(spaceTrainNumber: String, fareId: UUID, resetSelection: Boolean) : Search {}
  fun isSelectionComplete() : Boolean {}
  fun selectableSpaceTrains(bound: Bound) : List<SpaceTrain> {}
  
}

?

 

?

C

R

U

D

@JulienTopcu

class Search {
  
  fun create() : Search {}
  fun read() : Search {}
  fun update() : Search {}
  fun delete() : Search {}
  
}

Paugeot Anemic

@JulienTopcu

© Peugeot - "Le modeleur"

@JulienTopcu

© The Simpsons - "Lisa's Pony"

Domain-Driven Design @ericevans0

@JulienTopcu

@JulienTopcu

@JulienTopcu

@JulienTopcu

C

R

U

D

REST next level : Ecrire des APIs web orientées métier

By Julien Topçu

REST next level : Ecrire des APIs web orientées métier

Vous venez de coder votre logique métier, et peut-être que vous avez même fait l'effort d'appliquer les principes du Domain-Driven Design ! Mais au moment de l'écriture de votre API... Catastrophe ! Toute l'intention et l'expression de votre domaine partent en fumée pour rentrer dans le moule des méthodes GET, POST, etc. Dénaturé par la couche REST, le métier se voit alors en partie réimplémenté côté front pour compenser le vocabulaire limité de ce protocole basé sur un CRUD... Lors de ce talk, nous verrons comment HATEOAS - le dernier niveau de maturité d'une architecture REST - peut nous aider à écrire une API web orientée métier qui aura la puissance de guider vos consommateurs à travers le workflow de votre domaine.

  • 769
Loading comments...

More from Julien Topçu