Introduzione
In questo e negli articoli seguenti vi mostreremo come creare un’applicazione che implementa il gioco del tris (in inglese americano chiamato Tic-Tac-Toe), utilizzando un paradigma orientato ai servizi.
Esplicitiamo il significato di “utilizzando un paradigma orientato ai servizi”: intendiamo che il problema verrà pensato unicamente come una composizione di servizi e non altro. Non troverete perciò né classi, né funzioni nel codice che seguirà, ma solamente servizi.
A tal fine utilizzeremo un linguaggio di nuova generazione, esplicitamente pensato per la programmazione orientata ai servizi: Jolie, in cui ogni cosa è un servizio e non è possibile fare null’altro che programmare servizi.
L’obiettivo è mostrare come un cambio di paradigma possa trasformare, in modo importante, il modo con cui modelleremo e svilupperemo le applicazioni nei prossimi anni. Siamo fortemente convinti che modelli architetturali come i microservizi si diffonderanno sempre di più e ben presto emergerà anche a livello globale la necessità di avere nuovi strumenti “linguistici” per affrontare lo sviluppo di sistemi orientati ai servizi. La riduzione dei tempi e la semplificazione della complessità collegata alla programmazione dei sistemi distribuiti sono i principali motivi che spingeranno verso questa rivoluzione.
Partiremo spiegando un esempio, dando anche informazioni specifiche su Jolie così da introdurvi al linguaggio qualora foste interessati ad approfondirlo.
Speriamo in tal modo di riuscire a fornirvi i concetti fondamentali dei microservizi attraverso l’utilizzo di un linguaggio nuovo che può essere un valido strumento anche di tipo educativo. Jolie infatti permette da subito di “ragionare” a servizi senza dover introdurre strati tecnologici specifici per ottenere l’astrazione del concetto di servizio. Speriamo di aver già stuzzicato la vostra curiosità, vi lasciamo alla lettura del resto di questo articolo e, per qualsiasi domanda, non esitate a contattarci.
Uno sguardo al linguaggio Jolie
Prima di iniziare a spiegare come poter implementare il gioco del tris utilizzando un paradigma a servizi, daremo qui qualche cenno sul linguaggio Jolie [1]. Approfondimenti sulle specifiche primitive del linguaggio saranno dati nei punti in cui le si dovranno utilizzare.
Jolie nasce in Italia all’interno dell’Università di Bologna dopo che i suoi creatori avevano modellato i principi delle SOA con un sistema formale chiamato tecnicamente process calculus il cui nome specifico era, ironicamente, SOCK. SOCK modellava i principi cardine della programmazione a servizi introducendo i concetti di comunicazione sincrona e asincrona così come il concetto di API, sebbene tale termine, al tempo, non fosse stato esplicitamente utilizzato nel modello. Creato il modello, però, i suoi creatori decisero di avere uno strumento ad esso collegato che consentisse la programmazione vera e propria a servizi e così, sulle fondamenta teoriche di SOCK, nacque Jolie.
In particolare Jolie è un linguaggio che permette di
- programmare i servizi semplici basilari che effettuano calcoli o che gestiscono dati;
- programmare servizi chiamati orchestratori, ossia servizi che aggregano e coordinano altri servizi;
- programmare servizi che svolgono funzioni architetturali quali l’aggregazione di altri servizi o la redirezione a mo’ di proxy.
I microservizi
I microservizi sono una variante dello stile SOA (Service Oriented Architecture) in cui si cerca di costruire un’applicazione come un insieme di servizi, lascamente connessi.
I microservizi sono in se stessi un’applicazione, a grana molto fine — cioè implementano risposte molto elementari — e sono supportati da protocolli a basso impatto (lightweight).
Riprendono l’idea di Unix “fai una cosa ma falla bene”!
L’idea di fondo è che un’applicazione complessa arrivi ad essere un sistema distribuito di microservizi. È possibile modificare separatamente un componente del sistema senza necessariamente doverlo ricambiare totalmente.
Tra i benefici di strutturare un’applicazione in microservizi, citiamo i seguenti punti:
- modularità;
- scalabilità: posso aumentare il carico sopportato dall’applicazione in modo lineare con le richieste e specificatamente sui singoli componenti che rappresentano dei colli di bottiglia per l’applicazione; infatti i microservizi sono indipendenti e le loro istanze possono quindi essere create senza alcun vincolo.
- integrabilità: seguendo lo stesso approccio che si ha con le SOA, i microservizi possono essere utilizzati per integrare sistemi diversi tra di loro;
- sviluppo distribuito: ogni team può lavorare indipendentemente, su diversi microservizi in quanto l’importante è l’interfaccia (contratto) con cui essi comunicano con il mondo.
Wikipedia offre un ottimo articolo introduttivo [2].
Tic-Tac-Toe (il gioco del “tris”)
Quello che andremo a implementare è il famoso gioco del tris [3]. In moltissimi lo conoscono e ci hanno giocato. Come nota, ricorderemo che fu usato anche nella sceneggiatura del film Wargames – Giochi di guerra del 1983 [4].
L’architettura a servizi dell’applicazione
In figura 2, vediamo come, a livello di servizi, Jolie andrà a gestire il gioco tra due giocatori.
L’idea di base è di avere un microservizio che gestisce le scacchiere:
- ogni scacchiera sarà identificata da un identificatore univoco;
- gestendo la scacchiera di gioco, dovrà gestire le mosse effettuate dai contendenti e informare il rispettivo avversario dello stato della scacchiera.
Ogni giocatore potrà interrogare il servizio gestore scacchiere per ottenere un elenco degli avversari che attendono un avversario (scacchiere disponibili).
A questo punto il giocatore potrà o sedersi a una delle scacchiere già esistenti e iniziare il gioco, oppure creare una nuova scacchiera e attendere un avversario.
Ad ogni giocatore sarà associato un servizio che si interfaccerà con la il gestore delle scacchiere.
Nel diagaramma di figura 3 vediamo un esempio di una possibile sequenza di gioco.
Contract first: le interfacce
In Jolie, al contrario di altri linguaggi, è essenziale definire per prima cosa come i vari microservizi andranno a interagire con il mondo circostante, definendo modalità di interazione con gli altri servizi (contract first).
Tale specifica avviene tramite due strumenti: le interfacce (operazioni e dati ad esse associate) e le porte (come fisicamente parliamo all’esterno).
Il servizio gestione scacchiera avrà tre operazioni richiamabili dai microservizi di gestione dei giocatori e, a sua volta, questi ultimi avranno un’operazione richiamabile dal gestore della scacchiera.
L’interfaccia del gestore della scacchiera
Qui di seguito riportiamo il listato di definizione dell’interfaccia del gestore della scacchiera. Il codice è disponibile su Github [5].
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 )
}
Visto che le interfacce possono essere utilizzate su più microservizi, si suole scriverle in un file esterno con estensione .iol e importarle nel codice di Jolie.
Operations
Le operations messe a disposizione dal gestore scacchiere sono:
- move per la gestione delle mosse;
- listOpenGames che restituisce l’elenco delle scacchiere che attendono un avversario;
- startGame che avvia la partita se l’avversario è già presente, oppure crea una scacchiera in attesa di un’avversario.
In Jolie le operations definite in interfaccia possono essere di due tipi: a senso unico e a richiesta-risposta.
Le operazioni a senso unico (one-way), non richiedono una risposta al chiamante. Non sono bloccanti: il chiamante, dopo aver invocato queste operazioni, può subito proseguire con l’esecuzione del codice successivo.
Le operazioni a richiesta-risposta (request response) fanno sì che al chiamante sia restituito un messaggio contenente dei dati. L’operazione è bloccante per il chiamante, che deve attendere la risposta prima di poter proseguire nell’esecuzione del codice.
Ogni operazione può essere associata a dei messaggi, che noi esprimiamo con i tipi dati. Come in programmazione a oggetti (OOP), abbiamo una signature per le operazioni di ingresso.
Nel caso specifico del gestore della scacchiera, come si vede dalla definizione, tutte le operazioni esposte sono bloccanti, sono cioè delle request response.
Al termine della serie, riporteremo un documento con la sintassi formale delle interfacce.
L’interfaccia dell’utente
Vediamo il codice dell’interfaccia dell’utente:
type SyncPlacesRequest: void {
.places[9,9]: int
.message: string
.status_game: string // play | stay | end
}
interface UserInterface {
OneWay:
syncPlaces( SyncPlacesRequest )
}
Abbiamo qui un’unica operazione syncPlaces (a senso unico), non bloccante, che verrà richiamata dal gestore della scacchiera al fine di aggiornare i giocatori sullo stato del gioco (posizionamento croci e cerchi, stato del gioco, diritto a fare la prossima mossa).
Possiamo quindi riformulare lo schema generico della partita utilizzando le interfacce introdotte (figura 5).
Conclusioni
In questo primo articolo abbiamo cominciato a illustrare come realizzare il gioco del tris utilizzando una programmazione puramente orientata ai servizi. Ogni componente realizzato è un servizio e le interazioni tra i servizi sono scambi di messaggi. Grazie a questo esempio abbiamo introdotto il concetto di architettura a microservizi e abbiamo parlato di Jolie, un linguaggio di nuova generazione interamente orientato alla programmazione a servizi. Abbiamo dato qualche dettaglio tecnico riguardo alla programmazione a servizi mostrando come si scrivono le interfacce e infine abbiamo commentato quelle principali utilizzate nel gioco del tris.
Riteniamo che il cambio di paradigma verso una programmazione a servizi sia fondamentalmente inevitabile. Il Cloud Computing è sempre più matura come tecnologia e l’utilizzo della “containerizzazione” per mettere in esecuzione componenti software indipendentemente dalla piattaforma cloud sta velocemente prendendo il sopravvento. Ben presto le esigenze di semplificazione si sposteranno dal piano della gestione infrastrutturale al piano delle applicazioni, ed è per questo che il service-oriented programming sarà una keyword di cui sentiremo parlare sempre più spesso.