Uno dei principi fondamentali per
una buona programmazione software è quello di codificare gli aspetti importanti
riguardanti il sistema che si sta sviluppando in modo chiaro e ben localizzato.
E’ un principio “generale” nel senso che può toccare ogni fase dello sviluppo.
Per un
progettista è essenziale trattare i problemi di realizzazione di un sistema
complesso in maniera il più possibile indipendente: decomporre ogni problema
complesso in problemi più semplici ed aggregarne le soluzioni per ottenere la
soluzione completa.
Questo
approccio risulta essere uno strumento insostituibile anche per il
programmatore il quale, attraverso le strutture del linguaggio scelto per
implementare le soluzioni, si propone di realizzare l’effettiva separazione dei
problemi.
L’Object Oriented Programming è
oramai diventato il paradigma per eccellenza, avendo rimpiazzato quasi
completamente quello procedurale. Uno dei più grandi vantaggi che ha introdotto
è stato quello di permettere di vedere l’intero sistema come una collezione di
classi, ognuna con un ben definito scopo ed una chiara responsabilità, la cui
collaborazione permette di raggiungere l’obiettivo globale dell’applicazione.
Strumenti come l’incapsulazione, l’ereditarietà e il polimorfismo, hanno
contribuito ad un passaggio importante dall’approccio procedurale basato su
procedure e funzioni, ad una migliore suddivisione basata sulle funzionalità.
Nonostante ciò, se da un lato
l’object orientation promuove ed incoraggia il riuso del codice attraverso la
modularità, l’esperienza sul campo ha mostrato che non sempre riesce a gestirlo
così efficacemente come era stato originariamente pensato. Ci sono parti del
sistema che non possono essere viste come di responsabilità di un singolo
componente ma toccano o tagliano
l’intera struttura o parte di essa. Ci sono cioè alcune funzionalità difficili
se non impossibili da esprimere in modo ben localizzato usando i costrutti di
modularizzazione tradizionali come le classi (di cui gli oggetti come istanze),
i componenti, o le procedure. Gestione della memoria, meccanismi di sicurezza,
condivisione di risorse, logging,
gestione degli errori e delle eccezioni, sono solo alcune delle
caratteristiche che risentono di questo problema, ma sono anche funzionalità alla
base dello sviluppo attuale del software la cui complessità cresce quanto le
dimensioni.
La Separation
of Concern, (separazione delle competenze
nella nostra lingua anche se la traduzione letterale non permette di coglierne
il pieno significato) rappresenta un importante principio oltre che la naturale
astrazione di quanto detto. Decomporre un sistema in parti, ognuna delle quali
si occupi ed incapsuli una particolare area di interesse, chiamata concern.
Il concetto
di modularizzare il codice rappresenta proprio la volontà di mantenere ben
distinte e separate le unità funzionali attribuendone precise competenze.
Quando però una proprietà del sistema non riesce ad essere espressa attraverso
un’effettiva separazione di competenza ma coinvolge più di un modulo
funzionale, si genera un problema che chiameremo di sovrapposizione delle competenze.
L’Aspect
Oriented Programming potrebbe essere il prossimo passo della costante
evoluzione del paradigma Object Oriented, o forse potrebbe evolversi verso un
nuovo paradigma completamente indipendente. Comunque sia, rappresenta un
recente ed interessante filone di ricerca che cerca di identificare i costrutti
linguistici appropriati per esprimere tali problematiche. L’AOP introduce un
nuovo elemento di analisi e di programmazione chiamato aspetto. L’obiettivo è di fornire metodi e
tecniche per decomporre i problemi in un certo numero di componenti funzionali
e in un certo numero di aspetti che coinvolgono tali componenti, e quindi di
comporre componenti e aspetti per ottenere l’implementazione del sistema.
L’obiettivo
ultimo delle teorie e dei metodi che sottostanno a qualsiasi campo
dell’ingegneria è quello di aiutare gli ingegneri nella produzione di prodotti
di qualità. Normalmente questo è ottenuto fornendo una distinzione attenta tra
prodotto e processo. Il prodotto è cosa
è visibile ai clienti mentre il processo è il come
questo obiettivo può essere realizzato. Il cosa e il come, tuttavia, sono i due
lati della stessa medaglia. E’ con il processo, infatti, che gli ingegneri
possono influenzare la qualità nei loro prodotti, ridurre il time to market, e
controllare il costo di produzione. Queste considerazioni
generali sono valide per ogni campo dell’ingegneria non meno in quella del
software. La natura stessa del software, infatti, rende la qualità del prodotto
difficile da ottenere e da valutare, a meno che non si adotti un processo
adeguato.
L’ingegneria del software si è evoluta negli ultimi
trent’anni proponendosi di identificare tali processi di produzione oltre che
principi di sviluppo, tecniche e notazioni, con lo scopo ultimo di ottenere un
prodotto di qualità, sviluppato nei tempi ed entro il budget previsti.
Se da un lato ha definito principi chiave come rigore e
formalismo, separazione dei concetti, anticipazione dei cambiamenti,
evolvibilità e scalabilità, il grosso sforzo è stato quello di capire come
raggiungere questi obiettivi. Sono stati quindi definiti metodi, metodologie e
strumenti per garantire la correttezza e l’efficacia del proprio procedere.
Figura
1 – Ingegneria del
Software
A tutt’oggi rimangono valide le proprietà che McCall
[McCall77], verso la fine degli anni 70, propose per delineare uno dei primi
modelli di qualità standard, tant’è che nel 1991 ne derivò il modello ISO/IEC
9126 che venne riproposto per una seconda versione nel 2001.
Figura
2 - Modello di
qualità di McCall
Il grafico mostra chiaramente la situazione, suddividendo
i fattori di qualità rispetto a tre principali dimensioni : l’operatività, la
revisione e la trasposizione del prodotto.
Caratteristiche
essenziali dell’operatività del
prodotto sono :
-
Correttezza: esprime in
quale misura il programma rispetta i suoi requisiti.
-
Affidabilità : quanto
bene le funzionalità offerte rispondono ai suoi requisiti.
-
Efficienza : tempi di risposta, uso
della memoria ...
-
Usabilità : lo sforzo per imparare ad operare con
il sistema.
-
Integrità : capacità di
sopportare attacchi alla sicurezza del sistema.
La revisione
del prodotto comporta :
-
Manutenibilità : sforzo
richiesto per localizzare e risolvere errori in un programma in esercizio.
-
Flessibilità : sforzo richiesto per
modificare un programma in esercizio.
-
Verificabilità : sforzo
richiesto per verificare il sistema, per assicurarsi che il sistema faccia
effettivamente ciò per cui è stato costruito.
E per quanto riguarda la trasposizione
del prodotto abbiamo :
-
Portabilità: sforzo
richiesto per trasferire il software da una configurazione ad altre
configurazioni.
-
Riusabilità: la misura
della facilità con cui parti di software possono venire re-impiegate in altre
applicazioni.
-
Interoperabilità: sforzo
richiesto per far cooperare/dialogare il sistema con altri sistemi.
Potremmo identificare tutte queste come proprietà primarie al soddisfacimento
della qualità. Se quelle relative all’operatività del prodotto (esterne)
risultano essere quelle di cui ne trarrà giovamento l’utente e in base le quali
valuterà la qualità del prodotto, quelle relative alla revisione e alla
trasposizione (interne) assumono un ruolo fondamentale, in termini progettuali,
economici e manageriali, per gli sviluppatori.
Nel 2001 si stima ancora che i costi di mantenimento sono
superiori al 60% dell’intero costo di produzione di cui il 20% di correzioni,
il 30% di adattamenti ed il 50% di perfezionamenti. Non per nulla la produzione
di software è spesso considerata come somma tra sviluppo e mantenimento.
La progettazione assume quindi un ruolo di importanza
strategica nello sviluppo di sistemi, fornendo da un lato una visione ad alto
livello delle componenti statiche e dinamiche fondamentali, dall’altro
rappresentando il
tratto d'unione tra un vasto elenco di requisiti, frutto dell'analisi, ed un
mare di particolari, di minuzie, frutto del design dettagliato.
Gli studiosi della progettazione del software oggi
enfatizzano la nozione di architettura
software, perché tale nozione
permette di organizzare le conoscenze progettuali, comprendere le proprietà
strutturali e favorire il riuso dei componenti.
Il motto fondamentale dei progettisti di software è :
progettare per il cambiamento
[Parnas72]
alla
luce delle considerazioni precedenti e del fatto che, in molti casi, vengono
progettate famiglie di applicazioni, non applicazioni singole.
Idealmente il
progettista sceglie un’architettura software, la modella, la valuta rispetto
alla specifica dei requisiti e dopo alcune iterazioni produce una specifica
dettagliata di progetto che i programmatori possono implementare.
Progettare
per il cambiamento ha inoltre una duplice scopo. Da un lato questa prevenzione
è finalizzata ai cambiamenti che avvengono durante la manutenzione, dopo che il
prodotto è stato consegnato. Esempi possono essere i cambiamenti dei requisiti dell’utente,
cambiamenti agli algoritmi (routine più efficienti), cambiamenti alla
rappresentazione dei dati (si veda il problema dell’anno 2000), cambiamenti
all’infrastruttura (nuovi sistemi operativi, protocolli di rete, DBMS),
introduzione di nuove periferiche o modifiche del contesto sociale (cambiamenti
del sistema di tassazione o del sistema monetario).
Dall’altro
lato è necessario ricordare che la visione della costruzione del software
paragonata ad altre discipline ingegneristiche da cui trarne idee ed approcci
ai problemi, (in letteratura è spesso identificata come “impollinazione”) è
un’idea sbagliata.
Ne è
testimonianza l’abbandono, a livello di analisi, del modello a cascata
(waterfall) per un modello più flessibile come quello a spirale fino ad
arrivare ai cosiddetti metodi agili come l’Extreme Programming; nello sviluppo
di software, a differenza della costruzione di un progetto architetturale come
un ponte, non si sa esattamente, al momento della partenza, che cosa si vuole
ottenere. Un progetto che specifichi completamente
un sistema software è perciò un'utopia e di questa “instabilità” è necessario
tenerne in considerazione.
Uno degli
aspetti dei moderni cicli di produzione che più influenzano la strategia di
progettazione è la necessità di costruire famiglie
di applicazioni, ovvero diversi prodotti che vengono visti come una
singola applicazione che viene riutilizzata, specializzata e modificata
opportunamente in contesti diversi, dando luogo a versioni differenti. Si
applica un ciclo di vita speciale, esteso, in cui la fase di progetto viene
effettuata su un dominio e non per un’applicazione.
Le
preoccupazioni principali durante la fase di progetto sono :
-
il disegno di un’architettura
software adeguata al problema
-
la decomposizione
dell’architettura in sottostrutture
-
il dettaglio di interfacce e implementazione dei
moduli
-
il riuso
di schemi di progetto, di codice, di applicazioni
Anche se
viene stabilita l’architettura di base, rimane problematica la sua
decomposizione.
In tutti
questi anni gli sforzi si sono concentrati sulla cosiddetta decomposizione modulare da cui ne
scaturisce il comune concetto di modularità.
Al fine di
stabilire cosa si intende per architettura modulare e metodo di progetto
modulare, è necessario introdurre dei criteri e dotarsi di opportuni principi:
-
Scomponibilità: un metodo di progetto deve
favorire la decomposizione del problema in sottoproblemi risolubili
separatamente.
-
Componibilità : l’architettura deve favorire il
riuso di elementi software già esistenti e in grado di configurarsi in base
alle funzionalità richieste.
-
Comprensibilità : le funzioni di un modulo devono
essere comprensibili indipendentemente dal resto del sistema.
-
Continuità : il metodo deve minimizzare le
variazioni necessarie per far evolvere il sistema e soddisfare eventuali nuove
specifiche.
-
Protezione : ogni modulo deve limitare la
propagazione di errori a run-time verso gli altri moduli.
Non è
difficile vedere come la modularità, per sua natura, impatta profondamente
sulle proprietà che McCall indicò per caratterizzare un
software di qualità [McCall77] e come quindi possa essere considerata come proprietà architetturale principale.
Criteri come
comprensibilità e scomponibilità favoriscono la manutenzione, la flessibilità e
la verificabilità del sistema, come la componibilità e la continuità possono
considerarsi alla base di un buon sistema riusabile e portabile.
Una
volta stabiliti gli obiettivi da raggiungere è necessario sviluppare metodologie
in grado di soddisfare i principi
e le proprietà introdotte.
Fino
all’inizio agli anni 80, la progettazione di un sistema software complesso era
eseguita seguendo due principi base. Quello del “divide et impera”, in cui il
sistema veniva decomposto in componenti funzionali più semplici ed
indipendenti, e quello della “centralizzazione” dove lo stato globale veniva
comunque gestito in un’area condivisa, anche in caso di programmazione non
sequenziale.
Le
strategie di progetto erano sostanzialmente orientate alla definizione dei
processi di elaborazione (progettazione funzionale) e l’enfasi era posta sulle
funzioni caratteristiche del sistema, viste come trasformatrici di dati in
ingresso in dati in uscita.
Il
paradigma era quello strutturato, guidato da diagrammi Entità-Relazione per
modellare i dati del sistema e da diagrammi Data-Flow per modellare il
comportamento o il funzionamento del sistema. La decomposizione modulare era
vista come decomposizione funzionale. Al modulo corrispondeva una certa funzionalità.
Il
maggior svantaggio fu la scarsa consistenza tra dati e comportamento
all’interno dell’intero sistema, nonché una difficile corrispondenza di
concetti tra il mondo reale e implementazione.
A partire degli anni 80, l’introduzione del concetto di abstact data type , in cui dati e
comportamento vengono strettamente accoppiati, contribuì in modo significativo
a gettare le basi per le strategie data-oriented, in cui l’enfasi viene messa
sulla definizione delle strutture dati, sulle operazioni che le manipolano e
sulle loro proprietà.
I principi divennero: “orientamento agli oggetti”, dove
il sistema viene decomposto in moduli riusabili ed autonomi, e
“decentralizzazione” attraverso cui lo stato globale viene distribuito anche
nel caso sequenziale.
Il paradigma object oriented la fece da padrone e si
impose come base per lo sviluppo di una varietà di linguaggi di programmazione,
database systems e approcci di modellizzazione.
Il successo della modellizzazione object-oriented derivò
sostanzialmente dal fatto che fu il primo vero traguardo al soddisfacimento
delle proprietà primarie di McCall, oltre che un’ottima concretizzazione dei
principi di modularità.
L’OO risultò quindi il paradigma più seguito, nonostante
un’evoluzione ostacolata agli inizi degli anni 90 dal fatto che più di
cinquanta approcci determinarono la cosiddetta “method war”, dove ognuno
cercava di imporsi come il metodo
corretto.
Alle tecniche strutturali, basate sull’idea di costruire
un sistema concentrandosi sulle funzioni, le tecniche object-oriented proposero
un rovesciamento di questo rapporto costruendo il sistema a partire dalla
classificazione degli oggetti da manipolare.
L’idea quindi è di non chiedersi cosa fa il sistema ma a
cosa serve, di quali oggetti è composto e come si opera su di essi. Questo
obiettivo permette di modellare gli aspetti della realtà nel modo più diretto e
astratto possibile, mediante le nozioni di oggetto, classi e relazioni. Uno degli strumenti più interessanti che vennero introdotti fu quello delle
schede CRC (Class, Responsibility, and Collaboration) [Beck89] in risposta al
bisogno di documentare le relazioni di collaborazione e responsabilità tra
oggetti in fase di design.
Attraverso questi elementi e con l’introduzione di
concetti come ereditarietà e polimorfismo è possibile esprimere in modo
chiaro e naturale i principi di una buona architettura modulare. La
scomponibilità e la componibilità trovano espressione dal fatto che il sistema
viene costruito in termini di moduli, sulla base del principio che ogni classe
è un modulo. La natura stessa della classe, attraverso costrutti di incapsulazione che possano delinearne
parti private e pubbliche, fornisce uno strumento per riflettere la volontà di
proteggere le parti delicate del software permettendo solo un accesso
controllato e protetto a dati e funzioni. Legando gli aspetti comportamentali a
quelli strutturali e modellando i concetti attraverso l’ereditarietà di ottiene
un aumento della comprensibilità del sistema, la cui astrazione è garantita attraverso
il polimorfismo che permette un riuso di concetti tra tipi diversi di oggetti.
Figura
3 - Modello
funzionale e Modello dell’oggetto
Come
accennato in precedenza, l’attività fondamentale di un buon designer/architetto
del software è la decomposizione del sistema al fine di soddisfare tutte le
proprietà necessarie al raggiungimento di un software di qualità.
Il principio
alla base delle scelte di decomposizione è quello della Separation of Concern (Separazione delle Competenze) [Dijkst76]
che a tutt’oggi viene universalmente riconosciuto come uno dei principi
fondamentali dell’ingegneria del software: decomporre un sistema in parti,
ognuna delle quali si occupi ed incapsuli una particolare area di interesse,
chiamata concern. Questo principio riconosce che non possiamo trattare più
problemi alla volta ma, al contrario, dobbiamo trattare con ognuno di essi,
separatamente. Afferma, tra l’altro, che gli aspetti importanti dovrebbero
essere rappresentati nel programma intenzionalmente (in modo esplicito,
dichiarativo, con poca o senza aggiunta di “rumore”) e dovrebbero essere ben
localizzati. Questo facilita la comprensibilità, l’adattabilità, la riusabilità
e molte altre buone qualità di un programma, perché intenzionalità e
localizzazione ci permettono di verificare in modo più semplice come un
programma implementa i nostri requisiti.
Alla luce di
ciò, si può astrarre la problematica di decomposizione seguendo due principali
approcci dove uno non esclude necessariamente l’altro:
Una rappresentazione della
differenza tra aspetti e unità modulari è mostrata in Figura4. Il disegno alla
sinistra mostra come le unità modulari vengano incapsulate in modo chiaro e organizzate in
gerarchie. La figura sulla destra mostra che gli aspetti tagliano
(cross-cuts) un certo numero di
unità modulari.
Figura
4 - Decomposizione
modulare e decomposizione ad aspetti.
La seguente
Figura5 mostra un’altra rappresentazione di un aspetto: il Modello A è un
aspetto del Modello B perché si riferisce a diverse locazioni di B. Per
esempio, B potrebbe essere una classe che implementa un certo tipo di struttura
di dati astratti e A potrebbe essere una specifica di sincronizzazione riferita
ai metodi di tale struttura.
Figura
5 – Un altro
esempio di aspetto
La
decomposizione modulare ci permette di ottenere perfezionamenti in modo
semplice aggiungendo strutture che non attraversano i contorni delle unità
modulari già stabilite. Ogni volta che dobbiamo aggiungere una struttura che
oltrepassa questi contorni, stiamo applicando una decomposizione ad aspetti.
La
decomposizione ad aspetti e quella modulare possono essere combinate, poiché
una rappresenta il complemento dell’altra. Entrambe corrispondono alla
strategia umana e naturale di modellizzazione : dipaniamo i problemi
investigandoli sotto diverse prospettive e li organizziamo gerarchicamente.
La
decomposizione in aspetti rappresenta le fondamenta della Programmazione
Orientata agli Aspetti che verrà presentata in seguito. Come già accennato,
tale decomposizione è comunque presente, in modo più o meno evidente, nello
sviluppo di software, ad esempio nei diversi aspetti usati nei metodi di
analisi e di design, ma la ricerca nel campo dell’AOP fornisce, alla
decomposizione ad aspetti, alcune nuove prospettive. L’AOP incoraggia
l’introduzione di nuovi aspetti piuttosto che l’uso di un ridotto set di
aspetti generici (come nel caso dei metodi esistenti di OOA/D); enfatizza la
necessità di aspetti specializzati oltre che una loro combinazione per differenti
categorie di domini. L’AOP dichiara inoltre aperta la sfida di introdurre la
decomposizione ad aspetti non solo nei modelli di design ma a livello di
implementazione del sistema. In particolare c’è la necessità di nuovi
meccanismi di composizione e nuovi costrutti di linguaggio, tutto questo,
ovviamente, allo scopo di ottenere miglioramenti di qualità come la riduzione
di codice “aggrovigliato”, riduzione di ridondanza, migliore localizzazione del
codice, migliore comprensione, manutenibilità, e riusabilità.
La separazione delle competenze segue il fondamentale
principio di nascondere la complessità attraverso l’astrazione. Astraendo le
competenze e separandole, gestirne i relativi comportamenti diventa sostanzialmente
meno complesso.
Sfortunatamente,
gli aspetti rilevanti in un’applicazione sono solitamente “sovrapposti” e in
mutua dipendenza tra loro; quindi, se tentiamo di rappresentare tutti questi
aspetti esplicitamente e localmente, introdurremo inevitabilmente molta
ridondanza. E’ un problema da non sottovalutare poiché dobbiamo assicurarci che
tutte le rappresentazioni ridondanti rimangano consistenti. Il modello globale
del sistema rischia di diventare molto complesso vista la necessità di controllare
tutte le differenti rappresentazioni. D’altra parte, se decidiamo di mantenere
una rappresentazione con bassa ridondanza, alcuni aspetti risulteranno ben
localizzati ma altri non lo saranno. E’ un’idea simile alla rappresentazione di
un segnale nel dominio del tempo e in quello delle frequenze. Nel dominio del
tempo riusciamo a vedere l’ampiezza del segnale in ogni istante di tempo ma non
riusciamo a vederne le componenti in frequenza; nel dominio delle frequenza,
d’altra parte, riusciamo a vedere le componenti in frequenza ma non l’ampiezza
dell’intero segnale in un dato istante. Se decidiamo di tenere entrambe le
rappresentazioni ne vedremo entrambe le proprietà ma introdurremo ridondanza.
Idealmente
vorremmo implementare e memorizzare la soluzione di un problema attraverso una
rappresentazione efficiente, ossia una con minima ridondanza, ed avere qualche
meccanismo di supporto che permetta di estrarre dal modello ogni prospettiva di
cui abbiamo bisogno. Stesse informazioni sotto diversi aspetti.
Progettare un sistema basandosi su tale principio è un
compito che a tutt’oggi è più arduo di quello che si possa pensare e, con le
tecnologie attuali, risulta essere una soluzione impraticabile. E’ necessario
quindi orientarci su soluzioni più pratiche: lo scopo delle odierne tecniche di
modellizzazione è di sviluppare un modello che rispecchi i requisiti
(funzionali e di qualità, come performance, throughput, availability, failure
safety, ecc.) e, allo stesso tempo, bilanci i seguenti obiettivi :
-
avere gli aspetti di maggior importanza il più
possibile localizzati.
-
diminuire la complessità di composizione di tali aspetti (ci si
riferisce a questo problema come view rendering o complexity of compiler)
poiché la decomposizione e la conseguente localizzazione di certi aspetti può
considerevolmente aumentare la complessità delle trasformazioni necessarie alla
loro ricomposizione.
-
minima ridondanza
-
abilità di soddisfare con lo stesso grado cambiamenti previsti e imprevisti.
Vedremo con maggior dettaglio ognuno di questi obiettivi
quando discuteremo delle tecniche e dei meccanismi che sono stati proposti nel
campo della programmazione orientata agli aspetti.
Prima di
concludere è necessario ribadire la necessità di introdurre meccanismi per
poter seguire l’approccio della separazione delle competenze in tutto lo
sviluppo del sistema. A tale scopo è possibile distinguere due differenti
livelli di separazione.
In un livello concettuale bisogna fornire una
chiara definizione delle competenze,
e identificarle in modo tale che ognuna di esse possa essere distinta
concettualmente dalle altre. E’ necessario inoltre assicurarsi che i concetti siano individualmente “primitivi”
nel senso che non risultino composizione di altri concetti.
In un livello d’implementazione, la separazione
delle competenze deve provvedere a un’adeguata organizzazione che isoli tali
competenze. L’obiettivo di questo livello è di separare i blocchi di codice che
implementano le differenti competenze, provvedendo a rendere minimo
l’accoppiamento tra di essi.
Figura
6 - Livello
concettuale e livello d'implementazione
I concern
identificati nel livello concettuale sono usualmente mappati nel livello
d’implementazione attraverso i costrutti messi a disposizione dai linguaggi di
programmazione. I concern non primari nel livello concettuale hanno però
l’effetto indesiderato di essere rappresentati in costrutti monolitici (di
linguaggio) che cercano di gestire i differenti concern allo stesso tempo. Nonostante l’astrazione concettuale
venga riconosciuta in molte metodologie di design, pochi linguaggi di
programmazione ne permettono un’effettiva implementazione. L’organizzazione del
codice risulta monolitica e intrecciata come rappresentato nei Linguaggi A o B
in Figura6.
Solo attraverso un’effettiva applicazione della
separazione delle competenze ad entrambi i livelli se ne possono ottenere i
reali benefici.
Copyright (C)2002 Fabrizio Rovelli. La copia letterale e la distribuzione di questa pagina nella sua integrità sono permesse con qualsiasi mezzo, a condizione che questa nota sia riprodotta.