@JulienTopcu
@JulienTopcu
?đ€Ș?
@JulienTopcu
I want to search for trains going to my destination
@JulienTopcu
Architectural Styles and the Design of Network-based Software Architectures - Roy Fielding
@JulienTopcu
© Paramount Pictures - Interstellar
@JulienTopcu
© Paramount Pictures - Interstellar
@JulienTopcu
@JulienTopcu
@JulienTopcu
© 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": "..."
}
I book what I have selected
I'm rebooking the same train at a new date
Loss of the notion of Selection
Semantic confusion with the previous use case
@JulienTopcu
BookSpaceTrains.fromTheSelectionOf(search)
rebookSpaceTrains.atANewDate(bookingId,newdate)
POST /rebookings
{
"searchId" : "...",
}
POST /bookings
{
"bookingId" : "...",
"newDate": "..."
}
@JulienTopcu
Re-booking
Pseudo-fonctionnel
@JulienTopcu
@JulienTopcu
@JulienTopcu
POST /bookings/{id}/rebook
{
"newDate": "..."
}
booking {
"bookingId": "{newId}",
"date": "{newDate}"
}
GET /bookings/{newId}
@JulienTopcu
@JulienTopcu
 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
@JulienTopcu
© Lucasfilms - "STAR WARS: Revenge of the Sith"
@JulienTopcu
@JulienTopcu
Search
|_ availableSpaceTrains
|_ selection
|_ selectedSpaceTrains
PATCH /searches/{id}/selection
@JulienTopcu
I select a fare on a train
@JulienTopcu
@JulienTopcu
@JulienTopcu
@JulienTopcu
© 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 */
}
@JulienTopcu
data class Search { //Aggregate
/*attributes*/
/*no setters*/
fun changeCriteria(criteria : Criteria) : Search {
return this.copy(criteria = criteria, spaceTrains = emptyList())
}
}
@JulienTopcu
GET /searches/{id}
{
"id": "c956d89b-73fc-49ce-a911-f0058c0672f6",
"criteria": {/*...*/},
"spaceTrains": [/*...*/],
"selection": {/*...*/}
}
@JulienTopcu
© Lucasfilms - "STAR WARS: Attack of the Clones"
@JulienTopcu
@JulienTopcu
bookSpaceTrains.fromTheSelectionOf(search)
rebookSpaceTrains.atANewDate(bookingId,newdate)
POST /bookings
{
"searchId" : "...",
}
POST /bookings
{
"bookingId" : "...",
"newDate": "..."
}
We have to read the payload to deduce the use case
@JulienTopcu
@JulienTopcu
PATCH /searches/{id}/selection
[
{
"op": "add",
"path": "/selectedSpaceTrains/-",
"value": {"trainNumber" : "MOON421", "fare" : "FIRST"}
}
]
Accidental Complexity of adaptation
De-encapsulation
@JulienTopcu
POST /searches/{id}/spaceTrains/{number}/fares/{code}/select
Search /* Domain Model */
|_ availableSpaceTrains
|_ selection
|_ selectedSpaceTrains
@JulienTopcu
POST /searches/{id}/spaceTrains/{number}/fares/{code}/select
© Touchstone Pictures - "The Hitchhiker's Guide to the Galaxy"
@JulienTopcu
© Marvel Studio - "Guardians of the Galaxy Vol.1"
regarding the workflow...
@JulienTopcu
@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*/
@JulienTopcu
@JulienTopcu
@JulienTopcu
if(searchType == "roundtrip" && outBoundIsSelected && inBoundIsSelected){
displayBookingButton()
}
@JulienTopcu
if(searchType == "roundtrip" && outBoundIsSelected && inBoundIsSelected){
displayBookingButton()
}
@JulienTopcu
© Paramount Pictures - "Star Trek: The Next Generation"
@JulienTopcu
© martinfowler.com - "Richardson Maturity Model"
@JulienTopcu
Hypermedia As The Engine of Application State
@JulienTopcu
@JulienTopcu
@JulienTopcu
â
â
â
â
âïž
âïž
âïž
âïž
@JulienTopcu
â
â
â
â
âïž
âïž
âïž
âïž
@JulienTopcu
Repository
@JulienTopcu
© 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
@JulienTopcu
Je sélectionne un train et un tarif
@JulienTopcu
@JulienTopcu
@JulienTopcu
@JulienTopcu
BookSpaceTrains.fromTheSelectionOf(search)
rebookSpaceTrains.atANewDate(bookingId,newdate)
POST /bookings
{
"searchId" : "...",
}
POST /bookings
{
"bookingId" : "...",
"newDate": "..."
}
@JulienTopcu
đ
@JulienTopcu
@JulienTopcu
@JulienTopcu
@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
@JulienTopcu
@JulienTopcu
@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
"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
Bucket GraphQL
@JulienTopcu
@JulienTopcu
@JulienTopcu
"price" : {
"amount" : 1000,
"currency" : "FR"
}
@JulienTopcu
"price" : {
"amount" : 150,
"currency" : "EUR"
}
@JulienTopcu
@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
@JulienTopcu
@JulienTopcu
=> Logique déportée dans le consommateur exemple quand est-ce qu'on est pret a booker
@JulienTopcu
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.
@JulienTopcu
@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> {}
}
@JulienTopcu
class Search {
fun create() : Search {}
fun read() : Search {}
fun update() : Search {}
fun delete() : Search {}
}
@JulienTopcu
© Peugeot - "Le modeleur"
@JulienTopcu
© The Simpsons - "Lisa's Pony"
@JulienTopcu
@JulienTopcu
@JulienTopcu
@JulienTopcu