Perché è una buona idea che i livelli di applicazione "inferiori" non siano consapevoli di quelli "superiori"?


66

In un'app Web MVC tipica (ben progettata), il database non è a conoscenza del codice del modello, il codice del modello non è a conoscenza del codice del controller e il codice del controller non è a conoscenza del codice della vista. (Immagino che potresti anche iniziare fino all'hardware, o forse anche oltre, e il modello potrebbe essere lo stesso.)

Andando nella direzione opposta, puoi scendere di un solo livello. La vista può essere a conoscenza del controller ma non del modello; il controller può essere a conoscenza del modello ma non del database; il modello può essere a conoscenza del database ma non del sistema operativo. (Qualsiasi cosa più profonda è probabilmente irrilevante.)

Posso capire intuitivamente perché questa è una buona idea ma non riesco ad articolarla. Perché questo stile unidirezionale di stratificazione è una buona idea?


10
Forse è perché i dati arrivano "dal database" alla vista. "Inizia" nel database e "arriva" alla vista. La consapevolezza del livello va nella direzione opposta mentre i dati "viaggiano". Mi piace usare "virgolette".
Jason Swett,

1
L'hai taggato nell'ultima frase: unidirezionale. Perché le liste collegate sono molto più tipiche delle liste doppiamente collegate? Il mantenimento delle relazioni diventa infinitamente più semplice con un elenco collegato singolarmente. Creiamo grafici di dipendenza in questo modo perché le chiamate ricorsive diventano molto meno probabili e le caratteristiche grafiche generali diventano più facili da ragionare nel suo insieme. Le strutture ragionevoli sono intrinsecamente più gestibili e le stesse cose che influenzano i grafici a livello micro (implementazione) lo fanno anche a livello macro (architettura).
Jimmy Hoffa,

2
In realtà non penso che sia buona prassi nella maggior parte dei casi che View sia a conoscenza del controller. Poiché i controller sono quasi sempre a conoscenza della vista, avere la vista a conoscenza del controller crea un riferimento circolare
Amy Blankenship,

8
pessima analogia: per lo stesso motivo il tipo che ti guida da dietro con una macchina mentre guidi è il responsabile e responsabile dell'incidente, nel caso generale. Può vedere cosa stai facendo e dovresti avere il controllo, e se non poteva evitarti significa che non rispettava le regole di sicurezza. Non il contrario. E, incatenato, questo lo libera dal preoccuparsi di ciò che sta accadendo dietro di lui.
haylem,

1
Ovviamente una vista è a conoscenza di un modello di vista fortemente tipizzato.
DazManCat,

Risposte:


121

Strati, moduli, e in effetti l'architettura stessa, sono strumenti per rendere più facile la comprensione da parte degli umani dei programmi per computer . Il metodo numericamente ottimale per risolvere un problema è quasi sempre un disordinato pasticcio aggrovigliato di codice non modulare, autoreferenziale o persino auto-modificante, sia che si tratti di codice assembler fortemente ottimizzato in sistemi embedded con vincoli di memoria paralizzante o sequenze di DNA dopo milioni di anni di pressione di selezione. Tali sistemi non hanno livelli, nessuna direzione percepibile del flusso di informazioni, in realtà nessuna struttura che possiamo discernere affatto. A tutti tranne che al loro autore, sembrano funzionare per pura magia.

Nell'ingegneria del software, vogliamo evitarlo. La buona architettura è una decisione deliberata di sacrificare un po 'di efficienza per rendere il sistema comprensibile alla gente normale. Capire una cosa alla volta è più semplice della comprensione di due cose che hanno senso solo se usate insieme. Ecco perché moduli e layer sono una buona idea.

Ma inevitabilmente i moduli devono chiamare funzioni l'uno dall'altro e i layer devono essere creati uno sopra l'altro. Quindi, in pratica, è sempre necessario costruire sistemi in modo che alcune parti richiedano altre parti. Il compromesso preferito è costruirli in modo tale che una parte ne richieda un'altra, ma quella parte non richiede la prima. E questo è esattamente ciò che ci offre la stratificazione unidirezionale: è possibile comprendere lo schema del database senza conoscere le regole di business e comprendere le regole di business senza conoscere l'interfaccia utente. Sarebbe bello avere indipendenza in entrambe le direzioni, consentendo a qualcuno di programmare una nuova interfaccia utente senza sapere nullasulle regole aziendali - ma in pratica questo non è praticamente mai possibile. Regole empiriche come "Nessuna dipendenza ciclica" o "Dipendenze devono scendere solo di un livello" semplicemente catturano il limite praticamente realizzabile dell'idea fondamentale che una cosa alla volta è più facile da capire di due cose.


1
Cosa intendi per "rendere comprensibile il sistema da parte delle persone normali "? Penso che formulato in questo modo incoraggi i nuovi programmatori a rifiutare i tuoi punti positivi perché, come la maggior parte delle persone, pensano di essere più intelligenti della maggior parte delle persone e questo non sarà un problema per loro. Direi "rendere comprensibile il sistema dagli umani"
Thomas Bonini,

12
Questa lettura è necessaria per coloro che pensano che il disaccoppiamento completo sia l'ideale per cui lottare, ma non riesce a capire perché non funziona.
Robert Harvey,

6
Bene, @Andreas, c'è sempre Mel .
TRiG,

6
Penso che "più facile da capire" non sia abbastanza. Si tratta anche di semplificare la modifica, l'estensione e la manutenzione del codice.
Mike Weller,

1
@Peri: esiste una legge del genere, vedi en.wikipedia.org/wiki/Law_of_Demeter . Che tu sia d'accordo o no è un'altra questione.
Mike Chamberlain,

61

La motivazione fondamentale è questa: vuoi essere in grado di strappare un intero strato e sostituirne uno completamente diverso (riscritto), e NESSUNO DOVREBBE (ESSERE IN GRADO) NOTARE LA DIFFERENZA.

L'esempio più ovvio è strappare il livello inferiore e sostituirne uno diverso. Questo è ciò che fai quando sviluppi gli strati superiori contro una simulazione dell'hardware e poi sostituisci l'hardware reale.

L'esempio successivo è quando si strappa uno strato intermedio e si sostituisce un altro strato intermedio. Si consideri un'applicazione che utilizza un protocollo che funziona su RS-232. Un giorno, devi cambiare completamente la codifica del protocollo, perché "qualcos'altro è cambiato". (Esempio: passare dalla codifica ASCII diretta alla codifica Reed-Solomon dei flussi ASCII, perché stavi lavorando su un collegamento radio dal centro di Los Angeles a Marina Del Rey, e ora stai lavorando su un collegamento radio dal centro di Los Angeles a una sonda in orbita attorno a Europa , una delle lune di Giove, e quel collegamento richiede una correzione degli errori in avanti molto migliore.)

L'unico modo per farlo funzionare è se ogni livello esporta un'interfaccia nota e definita al livello precedente e si aspetta un'interfaccia definita e definita al livello sottostante.

Ora, non è esattamente il caso che gli strati inferiori non sappiano NIENTE sugli strati superiori. Piuttosto, ciò che il livello inferiore sa è che il livello immediatamente sopra funzionerà esattamente secondo la sua interfaccia definita. Non può sapere altro, perché per definizione tutto ciò che non è nell'interfaccia definita è soggetto a modifiche SENZA PREAVVISO.

Il livello RS-232 non sa se sta eseguendo ASCII, Reed-Solomon, Unicode (codepage araba, codepage giapponese, codepage rigelliana beta) o cosa. Sa solo che sta ottenendo una sequenza di byte e sta scrivendo quei byte su una porta. La prossima settimana, potrebbe ricevere una sequenza di byte completamente diversa da qualcosa di completamente diverso. Non gli importa. Si sposta solo byte.

La prima (e migliore) spiegazione del design a strati è la classica carta di Dijkstra "Struttura del sistema multiprogrammazione" . È necessario leggere in questo settore.


Questo è utile e grazie per il link. Vorrei poter selezionare due risposte come la migliore. Fondamentalmente ho lanciato una moneta nella mia testa e ho scelto l'altro, ma ho ancora votato il tuo.
Jason Swett,

+1 per esempi eccellenti. Mi piace la spiegazione fornita dal JRS
ViSu

@JasonSwett: Se avessi lanciato la moneta, l'avrei lanciata fino a quando non indicava quella risposta! ^^ +1 a John.
Olivier Dulac

Non sono d'accordo con questo, perché raramente vuoi essere in grado di strappare il livello delle regole di business e scambiarlo con un altro. Le regole aziendali cambiano molto più lentamente dell'interfaccia utente o delle tecnologie di accesso ai dati.
Andy,

Ding Ding Ding !!! Penso che la parola che stavi cercando sia "disaccoppiamento". Ecco a cosa servono le buone API. Definire le interfacce pubbliche di un modulo in modo che possa essere utilizzato universalmente.
Evan Plaice,

8

Perché i livelli più alti potrebbero cambiare.

Quando ciò accade, sia a causa di modifiche ai requisiti, nuovi utenti, tecnologie diverse, un'applicazione modulare (ovvero a strati unidirezionali) dovrebbero richiedere meno manutenzione ed essere più facilmente adattati per soddisfare le nuove esigenze.


4

Penso che il motivo principale sia che rende le cose più strettamente accoppiate. Più stretto è l'accoppiamento, maggiori sono le probabilità di incorrere in problemi in un secondo momento. Vedi questo articolo maggiori informazioni: Accoppiamento

Ecco un estratto:

svantaggi

I sistemi strettamente accoppiati tendono ad esibire le seguenti caratteristiche di sviluppo, che sono spesso viste come svantaggi: un cambiamento in un modulo di solito forza un effetto a catena di cambiamenti in altri moduli. L'assemblaggio di moduli potrebbe richiedere più impegno e / o tempo a causa della maggiore dipendenza tra moduli. Un modulo particolare potrebbe essere più difficile da riutilizzare e / o test poiché è necessario includere moduli dipendenti.

Detto questo, il motivo per cui un sistema accoppiato più stretto è per motivi di prestazioni. L'articolo che ho citato contiene anche alcune informazioni al riguardo.


4

IMO, è molto semplice. Non puoi riutilizzare qualcosa che continua a fare riferimento al contesto in cui è utilizzato.


4

I livelli non dovrebbero avere dipendenze a doppio senso

I vantaggi di un'architettura a strati sono che gli strati dovrebbero essere utilizzabili indipendentemente:

  • dovresti essere in grado di creare un livello di presentazione diverso oltre al primo senza modificare il livello inferiore (ad esempio, creare un livello API oltre a un'interfaccia Web esistente)
  • dovresti essere in grado di refactoring o eventualmente sostituire lo strato inferiore senza cambiare lo strato superiore

Queste condizioni sono sostanzialmente simmetriche . Spiegano perché in genere è meglio avere una sola direzione di dipendenza, ma non quale .

La direzione della dipendenza dovrebbe seguire la direzione del comando

Il motivo per cui preferiamo una struttura di dipendenza dall'alto verso il basso è perché gli oggetti in alto creano e usano gli oggetti in basso . Una dipendenza è sostanzialmente una relazione che significa "A dipende da B se A non può funzionare senza B". Quindi se gli oggetti in A usano gli oggetti in B, ecco come dovrebbero andare le dipendenze.

Questo è in qualche modo arbitrario. In altri modelli, come MVVM, il controllo scorre facilmente dagli strati inferiori. Ad esempio, puoi impostare un'etichetta la cui didascalia visibile è associata a una variabile e cambia con essa. Normalmente è comunque preferibile avere dipendenze dall'alto verso il basso, tuttavia, perché gli oggetti principali sono sempre quelli con cui l'utente interagisce e quegli oggetti svolgono la maggior parte del lavoro.

Mentre dall'alto in basso utilizziamo l'invocazione del metodo, dal basso verso l'alto (in genere) utilizziamo gli eventi. Gli eventi consentono alle dipendenze di andare verso il basso anche quando il controllo scorre al contrario. Gli oggetti del livello superiore si iscrivono agli eventi del livello inferiore. Il livello inferiore non sa nulla del livello superiore, che funge da plug-in.

Esistono anche altri modi per mantenere un'unica direzione, ad esempio:

  • continuazioni (passando un lambda o un metodo da chiamare ed evento a un metodo asincrono)
  • sottoclasse (creare una sottoclasse in A di una classe genitore in B che viene quindi iniettata nel livello inferiore, un po 'come un plugin)

3

Vorrei aggiungere i miei due centesimi a quello che Matt Fenwick e Kilian Foth hanno già spiegato.

Un principio dell'architettura software è che i programmi complessi dovrebbero essere costruiti componendo blocchi più piccoli e autonomi (scatole nere): questo minimizza le dipendenze riducendo la complessità. Quindi, questa dipendenza unidirezionale è una buona idea perché semplifica la comprensione del software e la gestione della complessità è una delle questioni più importanti nello sviluppo del software.

Quindi, in un'architettura a strati, gli strati inferiori sono scatole nere che implementano strati di astrazione sopra i quali sono costruiti gli strati superiori. Se uno strato inferiore (diciamo, strato B) può vedere i dettagli di uno strato superiore A, allora B non è più una scatola nera: i suoi dettagli di implementazione dipendono da alcuni dettagli del suo stesso utente, ma l'idea di una scatola nera è che la sua il contenuto (la sua implementazione) è irrilevante per il suo utente!


3

Solo per divertimento.

Pensa a una piramide di cheerleader. La riga inferiore supporta le righe sopra di esse.

Se le cheerleader su quella fila guardano in basso sono stabili e rimarranno in equilibrio in modo che quelli sopra di lei non cadano.

Se alza lo sguardo per vedere come stanno tutti quelli sopra di lei, perderà l'equilibrio causando la caduta dell'intero stack.

Non molto tecnico, ma era un'analogia che pensavo potesse aiutare.


3

Sebbene la facilità di comprensione e in una certa misura i componenti sostituibili siano certamente buone ragioni, una ragione altrettanto importante (e probabilmente la ragione per cui i livelli sono stati inventati in primo luogo) è dal punto di vista della manutenzione del software. La linea di fondo è che le dipendenze possono potenzialmente spezzare le cose.

Ad esempio, supponiamo che A dipenda da B. Dato che nulla dipende da A, gli sviluppatori sono liberi di modificare A in base al loro contenuto senza preoccuparsi di poter interrompere qualcosa di diverso da A. Tuttavia, se lo sviluppatore desidera cambiare B, qualsiasi modifica in B che viene prodotto potrebbe potenzialmente rompere A. Questo era un problema frequente nei primi giorni del computer (pensa allo sviluppo strutturato) in cui gli sviluppatori avrebbero risolto un bug in una parte del programma e avrebbe sollevato bug in parti del programma apparentemente totalmente non correlate altrove. Tutto a causa delle dipendenze.

Per continuare con l'esempio, supponiamo ora che A dipenda da B AND B dipende da A. IOW, una dipendenza circolare. Ora, ogni volta che viene apportata una modifica ovunque, potrebbe potenzialmente rompere l'altro modulo. Un cambiamento in B potrebbe ancora rompere A, ma ora un cambiamento in A potrebbe anche rompere B.

Quindi nella tua domanda originale, se fai parte di una piccola squadra per un piccolo progetto, tutto ciò è praticamente eccessivo perché puoi cambiare liberamente i moduli a tuo piacimento. Tuttavia, se si è su un progetto considerevole, se tutti i moduli dipendevano dagli altri, ogni volta che era necessaria una modifica potrebbe potenzialmente rompere gli altri moduli. In un grande progetto, conoscere tutti gli impatti potrebbe essere difficile da determinare, quindi probabilmente ti mancheranno alcuni impatti.

Peggiora in un grande progetto in cui ci sono molti sviluppatori (ad esempio alcuni che lavorano solo il livello A, alcuni livello B e alcuni livello C). Poiché è probabile che ogni modifica debba essere rivista / discussa con i membri degli altri livelli al fine di assicurarsi che le modifiche non vengano interrotte o forzare la rilavorazione su ciò su cui stanno lavorando. Se le tue modifiche impongono cambiamenti agli altri, allora devi convincerli che dovrebbero apportare le modifiche, perché non vogliono intraprendere più lavoro solo perché hai questo nuovo fantastico modo di fare le cose nel tuo modulo. IOW, un incubo burocratico.

Ma se hai limitato le dipendenze ad A dipende da B, B dipende da C, solo le persone di livello C devono coordinare le loro modifiche ad entrambi i team. Il livello B deve solo coordinare le modifiche con il gruppo Livello A e il livello A squadra è libero di fare tutto ciò che vogliono perché il loro codice non influenza il livello B o C. Quindi idealmente, progetterai i tuoi livelli in modo che il livello C cambi molto poco, il livello B cambia leggermente e il livello A fa la maggior parte del cambiamento.


+1 Al mio datore di lavoro abbiamo in realtà un diagramma interno che descrive l'essenza del tuo ultimo paragrafo in quanto si applica al prodotto su cui lavoro, vale a dire più si scende nella pila più basso è (e dovrebbe essere) il tasso di cambiamento.
RobV,

1

Il motivo fondamentale per cui gli strati inferiori non devono essere consapevoli degli strati superiori è che ci sono molti più tipi di strati superiori. Ad esempio, ci sono migliaia e migliaia di programmi diversi sul tuo sistema Linux, ma chiamano la stessa mallocfunzione di libreria C. Quindi la dipendenza è da questi programmi a quella libreria.

Si noti che gli "strati inferiori" sono in realtà gli strati intermedi.

Pensa a un'applicazione che comunica attraverso il mondo esterno attraverso alcuni driver di dispositivo. Il sistema operativo è nel mezzo .

Il sistema operativo non dipende dai dettagli all'interno delle applicazioni, né nei driver di dispositivo. Esistono molti tipi di driver di dispositivo dello stesso tipo e condividono lo stesso framework di driver di dispositivo. A volte gli hacker del kernel devono inserire una gestione dei casi speciali nel framework per il bene di un particolare hardware o dispositivo (esempio recente mi sono imbattuto in: codice specifico PL2303 nel framework seriale USB di Linux). Quando ciò accade, di solito inseriscono commenti su quanto fa schifo e dovrebbe essere rimosso. Anche se il sistema operativo chiama le funzioni nei driver, le chiamate passano attraverso hook che fanno sembrare i driver uguali, mentre quando i driver chiamano il sistema operativo spesso usano funzioni specifiche direttamente per nome.

Quindi, in un certo senso, il sistema operativo è in realtà un livello inferiore dal punto di vista dell'applicazione e dal punto di vista dell'applicazione: una sorta di hub di comunicazione in cui le cose si connettono e i dati vengono scambiati per seguire i percorsi appropriati. Aiuta la progettazione dell'hub di comunicazione di esportare un servizio flessibile che può essere utilizzato da qualsiasi cosa e di non spostare alcun hack specifico per dispositivo o applicazione nell'hub.


Sono felice finché non devo preoccuparmi di impostare tensioni specifiche su specifici pin della CPU :)
un CVn del

1

La separazione delle preoccupazioni e gli approcci di divisione / conquista possono essere un'altra spiegazione per queste domande. La separazione delle preoccupazioni offre la capacità di portabilità e, in alcune architetture più complesse, offre vantaggi di ridimensionamento e prestazioni indipendenti dalla piattaforma.

In questo contesto, se si pensa a un'archittettura a 5 livelli (client, presentazione, bussiness, integrazione e livello di risorse) il livello inferiore di architettura non dovrebbe essere consapevole della logica e del business dei livelli superiori e viceversa. Intendo per livello inferiore come livello di integrazione e risorse. Le interfacce di integrazione del database fornite in integrazione e database reali e servizi Web (fornitori di dati di terze parti) appartengono al livello delle risorse. Quindi supponiamo che cambi il tuo database MySQL in un DB NoSQL come MangoDB in termini di scalabilità o altro.

In questo approccio, il livello aziendale non si preoccupa del modo in cui il livello di integrazione fornisce la connessione / trasmissione dalla risorsa. Cerca solo gli oggetti di accesso ai dati forniti dal livello di integrazione. Questo potrebbe essere esteso a più scenari, ma fondamentalmente, la separazione delle preoccupazioni potrebbe essere la ragione numero uno per questo.


1

Espandendo la risposta di Kilian Foth, questa direzione di stratificazione corrisponde a una direzione in cui un umano esplora un sistema.

Immagina di essere un nuovo sviluppatore incaricato di correggere un bug nel sistema a più livelli.

I bug di solito sono una discrepanza tra ciò di cui il cliente ha bisogno e ciò che ottiene. Mentre il cliente comunica con il sistema tramite l'interfaccia utente e ottiene risultati tramite l'interfaccia utente (l'interfaccia utente significa letteralmente "interfaccia utente"), anche i bug vengono segnalati in termini di interfaccia utente. Quindi, come sviluppatore, non hai molta scelta ma iniziare a guardare anche l'interfaccia utente, per capire cosa è successo.

Ecco perché è necessario disporre di connessioni di livello top-down. Ora, perché non abbiamo connessioni che vanno in entrambe le direzioni?

Bene, hai tre scenari su come potrebbe mai verificarsi quel bug.

Potrebbe verificarsi nel codice UI stesso e quindi essere localizzato lì. È facile, devi solo trovare un posto e ripararlo.

Potrebbe verificarsi in altre parti del sistema a seguito di chiamate effettuate dall'interfaccia utente. Il che è moderatamente difficile, traccia un albero di chiamate, trova un posto in cui si verifica l'errore e lo risolvi.

E potrebbe verificarsi a seguito di una chiamata INTO al tuo codice UI. Il che è difficile, devi prendere la chiamata, trovare la sua fonte, quindi capire dove si verifica l'errore. Considerando che un punto in cui si inizia si trova in profondità in un singolo ramo di un albero delle chiamate, E prima è necessario trovare un albero delle chiamate corretto, potrebbero esserci diverse chiamate nel codice dell'interfaccia utente, è necessario ritagliare il debug per te.

Per eliminare il più possibile il caso più difficile, le dipendenze circolari sono fortemente scoraggiate, gli strati si collegano principalmente in modo discendente. Anche quando è necessaria una connessione dall'altra parte, di solito è limitata e chiaramente definita. Ad esempio, anche con i callback, che sono una sorta di connessione inversa, il codice chiamato in callback di solito fornisce questo callback in primo luogo, implementando una sorta di "opt-in" per le connessioni inverse e limitando il loro impatto sulla comprensione di un sistema.

La stratificazione è uno strumento e principalmente rivolta agli sviluppatori che supportano un sistema esistente. Bene, anche le connessioni tra i livelli lo riflettono.


-1

Un altro motivo che vorrei vedere esplicitamente menzionato qui è la riusabilità del codice . Abbiamo già avuto l'esempio del supporto RS232 che viene sostituito, quindi facciamo un passo avanti ...

Immagina di sviluppare driver. È il tuo lavoro e scrivi un bel po '. I protocolli potrebbero probabilmente iniziare a ripetersi ad un certo punto, così come i supporti fisici.

Quindi quello che inizierai a fare - a meno che tu non sia un grande fan di fare sempre la stessa cosa - è scrivere livelli riutilizzabili per queste cose.

Supponi di dover scrivere 5 driver per i dispositivi Modbus. Uno di questi utilizza Modbus TCP, due usano Modbus su RS485 e il resto passa su RS232. Non dovrai reimplementare Modbus 5 volte, perché stai scrivendo 5 driver. Inoltre non reimplementerai Modbus 3 volte, perché hai 3 diversi livelli fisici sotto di te.

Quello che fai è scrivere un TCP Media Access, un RS485 Media Access e possibilmente un RS232 Media Access. È intelligente sapere che ci sarà un livello modbus sopra, a questo punto? Probabilmente no. Il prossimo driver che si intende implementare potrebbe anche utilizzare Ethernet ma utilizzare HTTP-REST. Sarebbe un peccato se dovessi reimplementare Ethernet Media Access per comunicare via HTTP.

Un livello sopra, implementerai Modbus solo una volta. Quel livello Modbus, ancora una volta, non conoscerà i driver, che sono a un livello superiore. Questi driver, ovviamente, dovranno sapere che dovrebbero parlare modbus e dovrebbero sapere che usano Ethernet. Comunque, implementato nel modo in cui l'ho appena descritto, non si può semplicemente strappare uno strato e sostituirlo. puoi ovviamente - e questo per me è il più grande vantaggio di tutti, andare avanti e riutilizzare quel livello Ethernet esistente per qualcosa di assolutamente estraneo al progetto che originariamente ne ha causato la creazione.

Questo è qualcosa che probabilmente vediamo ogni giorno come sviluppatori e che ci fa risparmiare un sacco di tempo. Ci sono innumerevoli librerie per tutti i tipi di protocolli e altre cose. Questi esistono a causa di principi come la direzione della dipendenza che segue la direzione del comando, che ci consente di costruire livelli di software riutilizzabili.


la riusabilità è già stata esplicitamente menzionata in una risposta pubblicata più di sei mesi fa
moscerino
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.