Last time on Tic-tac-Jolie
Nella precedente parte abbiamo:
- introdotto il concetto e la filosofia delle architettura a microservizi;
- presentato un’introduzione a Jolie;
- analizzato l’impostazione a microservizi della gestione del Tris;
- introdotto il concetto di interfaccia ed elencato quelle utilizzate;
- in ultimo abbiamo riformulato lo schema a servizi del gioco utilizzando le operazioni esposte.
In questa seconda parte della serie, vediamo alcuni aspetti importanti — quali i tipi di dati e le porte che consentono il dialogo con l’esterno — e cominciamo a definire la “scacchiera” su cui il gioco si svolgerà.
I tipi di dati
I dati gestiti in Jolie sono di due tipi:
- elementari (basiliari): int, string, bool, raw, long, double, void;
- tipi personalizzati (structured data tree): la struttura è un albero — simile a quello di XML o JSON — con una radice a cui diamo un nome. In particolare, il nodo radice può avere a sua volta un suo valore (void se non ha alcun valore) e ha dei nodi figli per i quali si può ripetere ricorsivamente la stessa struttura.
Nella appendice A4 e A5, che pubblicheremo al termine della serie, sarà riportata la sintassi formale.
Analisi dei dati usati nel gioco
Veniamo adesso ad analizzare i dati usati nel gioco, alternando il listato con la spiegazione.
type StartGameRequest: void {
.game?: string
.user_location: string
}
Definiamo un tipo StartGameRequest che nella radice non contiene alcun valore e ha un nodo game (opzionale, indicato dal carattere “?”) di tipo stringa, e un altro nodo user_location (obbligatorio) di tipo stringa.
StartGameRequest.user_location
Fa riferimento alla collocazione dell’utente, vale a dire, dove è presente l’utente sulla rete.
StartGameRequest.game
È il nome della scacchiera a cui vogliamo sederci e iniziare a giocare.
type StartGameResponse: void {
.game_token: string
.role_token: string
.role_type: string
}
Qui abbiamo un tipo con tre nodi obbligatori: game_token, role_token, role_type.
- game_token identificherà il giocatore durante il gioco (valore univoco);
- role_token è il token che identifica il giocatore;
- role_type identifica il simbolo di cerchio (“circle“) o croce (“cross“) con cui un giocatore fa le sue mosse sulla scacchiera.
type ListOpenGamesRequest: void
ListOpenGamesRequest è un alias per void, quindi la chiamata all’operazione avverrà senza invio di dati.
type ListOpenGamesResponse: void {
.game_token*: string
}
L’asterisco * definisce una cardinalità potenzialmente infinita tipo game_token, cioè definiamo un array di lunghezza arbitraria.
Dati per l’operazione interna
C’è anche un tipo di dato per l’operazione interna.
type InitiateGameRequest: void {
.game_token: string
.circle_participant: string {
.location: string
}
.cross_participant: string {
.location: string
}
}
type InitiateGameRequest: void { è il messaggio passato all’operazione interna; game_token rappresenta il nome della schacchiera/partita; circle_participant indica l’ubicazione sulla rete del giocatore rappresentato dal cerchio e cross_participant, analogamente, è l’ubicazione sulla rete del giocatore con la croce.
Le porte: accedere al mondo reale e dialogare con esso
Abbiamo visto in precedenza il paradigma linguistico: tipi, messaggi e interfacce. Il paradigma linguistico cerca di catturare l’essenza del servizio (linguaggio con sintassi e semantica adatte a implementare i vari aspetti del servizio).
Il punto di vista operativo (deployment) considera il servizio come una scatola nera di solito messa in opera dentro un suo container ed eseguita. Dal suo punto di vista, un’architettura a servizi non è altro che una rete di microservizi connessi tra loro tramite porte.
In Jolie i due aspetti hanno trovato una sintesi. Mentre con le interfacce descriviamo l’insieme delle operazioni che il servizio espone logicamente e i messaggi in esse scambiati (contract first), con le porte andiamo a descrivere come nella realtà comunichiamo con il resto del mondo. Questo è il punto di “accoppiamento”.
La sintassi delle porte sarà formalmente descritta nell’appendice A6.
Porte del gestore della scacchiera
Ecco il listato di dichiarazione delle porte:
outputPort User {
Protocol: sodep
Interfaces: UserInterface
}
outputPort MySelf {
Interfaces: InternalInterface
}
inputPort Local {
Location: "local"
Protocol: sodep
Interfaces: InternalInterface
}
inputPort Tris {
Location: "socket://localhost:9000"
Protocol: sodep
Interfaces: TrisGameInterface
}
Come si evince abbastanza facilmente, abbiamo sia porte in uscita (outputPort) sia in ingresso (inputPort). Ecco di seguito i vari brani del codice con il relativo commento.
outputPort User {
Protocol: sodep
Interfaces: UserInterface
}
Porta in uscita per comunicare con l’utente che utilizza come messaggi quelli esposti dall’interfaccia UserInterface. Sodep è un protocollo sviluppato per Jolie.
outputPort MySelf {
Interfaces: InternalInterface
}
Porta utilizzata per chiamare l’operazione interna.
inputPort Local {
Location: "local"
Protocol: sodep
Interfaces: InternalInterface
}
Anche nel caso di un’operazione interna devo dichiarare la corrispettiva porta. A tale scopo pongo Location = local per indicare una porta locale.
inputPort Tris {
Location: "socket://localhost:9000"
Protocol: sodep
Interfaces: TrisGameInterface
}
Porta d’ingresso del servizio su cui arriveranno le richieste dei giocatori.
Porte del gestore del giocatore
outputPort TrisGame {
Location: "socket://localhost:9000"
Protocol: sodep
Interfaces: TrisGameInterface
}
inputPort User {
Location: UserLocation
Protocol: sodep
Interfaces: UserInterface
}
Vediamo anche qui i commenti alle varie porzioni di codice.
outputPort TrisGame {
Location: "socket://localhost:9000"
Protocol: sodep
Interfaces: TrisGameInterface
}
Porta verso il gestore della scacchiera. Il gestore è sulla stessa macchina su cui si esegue il giocatore.
inputPort User {
Location: UserLocation
Protocol: sodep
Interfaces: UserInterface
}
Porta su cui si riceveranno gli aggiornamenti della scacchiera e lo stato del gioco, a seguito dell’operazione move(.)(.).
Il codice di gestione della scacchiera (Behaviour)
Abbiamo illustrato la parte contract first (interfacce + porte) che definiamo deployment. Ora andiamo a illustrare il codice implementativo del gestore delle scacchiere (Behaviour), il cui commento sarà concluso nella prossima parte della serie.
Ecco intanto il listato implementativo:
include "TrisGameInterface.iol"
include "UserInterface.iol"
include "runtime.iol"
include "console.iol"
cset {
token: MoveRequest.game_token
}
execution{ concurrent }
type InitiateGameRequest: void {
.game_token: string
.circle_participant: string {
.location: string
}
.cross_participant: string {
.location: string
}
}
interface InternalInterface {
OneWay:
initiateGame( InitiateGameRequest )
}
outputPort User {
Protocol: sodep
Interfaces: UserInterface
}
outputPort MySelf {
Interfaces: InternalInterface
}
inputPort Local {
Location: "local"
Protocol: sodep
Interfaces: InternalInterface
}
inputPort Tris {
Location: "socket://localhost:9000"
Protocol: sodep
Interfaces: TrisGameInterface
}
define checkVictory {
/* check the rows */
for ( r = 0, r < 3, r++ ) {
if ( (places[0+(3*r)] + places[1+(3*r)] + places[2+(3*r)]) == 3 ) { circle_wins = true };
if ( (places[0+(3*r)] + places[1+(3*r)] + places[2+(3*r)]) == -3 ) { cross_wins = true }
};
/* check the columns */
for( c = 0, c < 3, c++ ) {
if ( (places[c]+places[c+3]+places[c+6]) == 3 ) { circle_wins = true };
if ( (places[c]+places[c+3]+places[c+6]) == -3 ) { cross_wins = true }
};
/* check diagonal */
if ( (places[0]+places[4]+places[8]) == 3 ) { circle_wins = true };
if ( (places[0]+places[4]+places[8]) == -3 ) { cross_wins = true };
if ( (places[2]+places[4]+places[6]) == 3 ) { circle_wins = true };
if ( (places[2]+places[4]+places[6]) == -3 ) { cross_wins = true };
/* send final messages */
if ( !circle_wins && !cross_wins ) {
circle_message = cross_message = "Nobody wins"
} else {
if ( circle_wins ) {
circle_message = "You win!";
cross_message = "You loose!"
} else {
cross_message = "You win!";
circle_message = "You loose!"
}
;
usr.places -> places;
usr.status_game = "end";
User.location = circle_participant.location;
usr.message = circle_message;
syncPlaces@User( usr );
User.location = cross_participant.location;
usr.message = cross_message;
syncPlaces@User( usr )
}
}
init {
getLocalLocation@Runtime()( MySelf.location )
}
main {
[ initiateGame( request ) ] {
csets.token = request.game_token;
circle_participant = request.circle_participant;
circle_participant.location = request.circle_participant.location;
cross_participant = request.cross_participant;
cross_participant.location = request.cross_participant.location;
for( i = 0, i < 9, i++ ) { places[i] = 0 };
/* send start messages */
User.location = cross_participant.location;
usr.places -> places; usr.message = "Wait for a move from circle player";
usr.status_game = "stay";
syncPlaces@User( usr );
User.location = circle_participant.location;
usr.status_game = "play";
usr.message = "It is your turn to play";
syncPlaces@User( usr );
/* start game */
moves = 0; circle_wins = false; cross_wins = false;
while( moves < 9 && !circle_wins && !cross_wins ) {
/* waiting for a move */
scope( move ) {
install( MoveNotAllowed => nullProcess );
move( mv_request )() {
/* check if the place is empty */
if ( places[ mv_request.place ] != 0 ) { throw( MoveNotAllowed, "The place is already occupied")};
/* check the turn */
if ( (moves%2) == 0 ) {
/* circle move */
if ( mv_request.participant_token != circle_participant ) {
throw( MoveNotAllowed, "It is not your turn" )
} else {
places[ mv_request.place ] = 1;
User.location = circle_participant.location;
usr.places -> places; usr.message = "Wait for a move from cross player";
usr.status_game = "stay";
syncPlaces@User( usr );
User.location = cross_participant.location;
usr.message = "It is your turn to play";
usr.status_game = "play";
syncPlaces@User( usr )
}
} else {
/* cross move */
if ( mv_request.participant_token != cross_participant ) {
throw( MoveNotAllowed, "It is not your turn" )
} else {
places[ mv_request.place ] = -1;
User.location = cross_participant.location;
usr.places -> places; usr.message = "Wait for a move from circle player";
usr.status_game = "stay";
syncPlaces@User( usr );
User.location = circle_participant.location;
usr.status_game = "play";
usr.message = "It is your turn to play";
syncPlaces@User( usr )
}
}
}
;
moves++;
checkVictory
}
}
;
if ( !circle_wins && ! cross_wins ) {
usr.places -> places;
usr.status_game = "end";
User.location = circle_participant.location;
usr.message = "";
syncPlaces@User( usr );
User.location = cross_participant.location;
usr.message = "";
syncPlaces@User( usr )
}
}
[ listOpenGames( request )( response ) {
count = 0;
foreach( g : global.games ) {
response.game_token[ count ] = g;
count++
}
}]
[ startGame( request )( response ) {
new_game = false;
if ( !is_defined( global.games.( request.game ) ) ) {
new_game = true;
token = new;
global.games.( token ) = true;
global.games.( token ).circle_participant = new;
global.games.( token ).circle_participant.location = request.user_location;
global.games.( token ).cross_participant = new;
response.game_token = token;
response.role_token = global.games.( token ).circle_participant;
response.role_type = "circle"
} else {
response.game_token = request.game;
response.role_token = global.games.( request.game ).cross_participant;
global.games.( request.game ).cross_participant.location = request.user_location;
response.role_type = "cross"
}
}] {
if ( !new_game ) {
with( initiate_request ) {
.game_token = request.game;
.circle_participant = global.games.( request.game ).circle_participant;
.circle_participant.location = global.games.( request.game ).circle_participant.location;
.cross_participant = global.games.( request.game ).cross_participant;
.cross_participant.location = global.games.( request.game ).cross_participant.location
};
initiateGame@MySelf( initiate_request );
undef( global.games.( request.game ) )
}
}
}
Per completezza esplicitiamo le interfacce importate:
TrisGameInterface.iol
type ListOpenGamesRequest: void
type ListOpenGamesResponse: void {
.game_token*: string
}
type MoveRequest: void {
.game_token: string
.participant_token: string
.place: int
}
type StartGameRequest: void {
.game?: string
.user_location: string
}
type StartGameResponse: void {
.game_token: string
.role_token: string
.role_type: string
}
interface TrisGameInterface {
RequestResponse:
listOpenGames( ListOpenGamesRequest )( ListOpenGamesResponse ),
move( MoveRequest )( void ) throws MoveNotAllowed( string ),
startGame( StartGameRequest )( StartGameResponse )
UserInterfaces.iol
type SyncPlacesRequest: void {
.places[9,9]: int
.message: string
.status_game: string // play | stay | end
}
interface UserInterface {
OneWay:
syncPlaces( SyncPlacesRequest )
}
Conclusioni
In questa parte abbiamo affrontato (in prima battuta) la definizione di dati (tipi) in Jolie; poi, abbiamo elencato i principali tipi di dati utilizzati nelle interfacce; successivamente abbiamo introdotto il concetto di porta e la sua giustificazione teorica ed elencato e commentato le porte utilizzate. Infine abbiamo introdotto il listato completo dei servizio gestione scacchiera. Nel prossimo numero ripartiremo da qui.
Ha cominciato con l'informatica da autodidatta, nel lontano 1982 — anni ruggenti — con il mitico Lemon II (clone di Apple II) e registratore a cassette. Ha poi conseguito una laurea vecchio ordinamento in ingegneria elettronica indirizzo informatica presso l’Università degli Studi di Bologna.
Negli anni, ha spaziato in diversi settori — assicurazione qualità, pianificazione produzione, logistica — mantenendo sempre un occhio sulla parte informatica, settore in cui ha poi rifocalizzato i suoi interessi. Lavora attualmente presso Imola Informatica, con il ruolo di software engineer.
Claudio Guidi è un ricercatore ed un imprenditore nell‘area dei microservices. Co-Leader del progetto Jolie (http://www.jolie-lang,org), ha conseguito il suo Ph.D. in computer science presso l‘Università di Bologna con una tesi sulla formalizzazione dei linguaggi per il Service Oriented Computing. Insieme a Fabrizio Montesi, l‘altro creatore di Jolie, ha fondato italianaSoftware che già oggi vende soluzioni legate alla system integration orientate ai microservizi.