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
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)
}
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
C
R
U
D
omment
endre
seless
DD
@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.
- 3,247