Perché molti sviluppatori di software violano il principio di apertura / chiusura?


74

Perché molti sviluppatori di software violano il principio di apertura / chiusura modificando molte cose come rinominare le funzioni che interromperanno l'applicazione dopo l'aggiornamento?

Questa domanda mi viene in mente dopo le versioni veloci e continue nella libreria React .

Ogni breve periodo noto molti cambiamenti nella sintassi, nei nomi dei componenti, ... ecc

Esempio nella prossima versione di React :

Nuovi avvisi di deprecazione

Il cambiamento più grande è che abbiamo estratto React.PropTypes e React.createClass nei loro pacchetti. Entrambi sono ancora accessibili tramite l'oggetto React principale, ma l'utilizzo di uno dei due registrerà un avviso di deprecazione una tantum alla console quando si è in modalità di sviluppo. Ciò consentirà future ottimizzazioni delle dimensioni del codice.

Questi avvisi non influiranno sul comportamento dell'applicazione. Tuttavia, ci rendiamo conto che potrebbero causare qualche frustrazione, in particolare se si utilizza un framework di test che considera console.error come un errore.


  • Questi cambiamenti sono considerati una violazione di tale principio?
  • Come principiante a qualcosa come React , come posso impararlo con questi rapidi cambiamenti nella libreria (è così frustrante)?

6
Questo è chiaramente un esempio di osservazione e la tua affermazione "così tanti" non è comprovata. I progetti Lucene e RichFaces sono esempi noti e l'API della porta COMM di Windows, ma non riesco a pensare a nessun altro. E React è davvero un "grande sviluppatore di software"?
user207421

62
Come ogni principio, l'OCP ha il suo valore. Ma richiede che gli sviluppatori abbiano una lungimiranza infinita. Nel mondo reale, le persone spesso sbagliano il loro primo progetto. Col passare del tempo, alcuni preferiscono aggirare i loro vecchi errori per ragioni di compatibilità, altri preferiscono eventualmente eliminarli per avere una base di codice compatta e senza problemi.
Theodoros Chatzigiannakis,

1
Quando è stata l'ultima volta che hai visto un linguaggio orientato agli oggetti "come originariamente previsto"? Il principio fondamentale era un sistema di messaggistica che significava che ogni parte del sistema è infinitamente estendibile da chiunque. Ora confrontalo con il tuo tipico linguaggio simile a OOP: quanti ti consentono di estendere un metodo esistente dall'esterno? Quanti lo rendono abbastanza facile da essere utile?
Luaan,

L'eredità fa schifo. 30 anni di esperienza hanno dimostrato che dovresti scaricare completamente l' eredità e ricominciare da capo, in ogni momento. Oggi tutti hanno connessioni ovunque in ogni momento, quindi l'eredità è totalmente irrilevante oggi. l'esempio finale è stato "Windows contro Mac". Tradizionalmente Microsoft ha cercato di "supportare l'eredità", lo vedi in molti modi. Apple ha sempre detto "F- - - You" agli utenti legacy. (Questo vale per tutto, dalle lingue ai dispositivi, ai sistemi operativi.) In effetti, Apple era totalmente corretta e MSFT era totalmente sbagliata, chiara e semplice.
Fattie,

4
Perché ci sono esattamente zero "principi" e "modelli di progettazione" che funzionano il 100% delle volte nella vita reale.
Matti Virkkunen,

Risposte:


148

La risposta di IMHO JacquesB, sebbene contenga molta verità, mostra un fondamentale fraintendimento dell'OCP. Per essere onesti, la tua domanda esprime già anche questo malinteso: le funzioni di ridenominazione interrompono la retrocompatibilità , ma non l'OCP. Se sembra necessaria l'interruzione della compatibilità (o il mantenimento di due versioni dello stesso componente per non interrompere la compatibilità), l'OCP era già stato interrotto in precedenza!

Come già menzionato da Jörg W Mittag nei suoi commenti, il principio non dice "non è possibile modificare il comportamento di un componente" - si dice, si dovrebbe provare a progettare i componenti in modo che siano aperti per essere riutilizzati (o estesi) in diversi modi, senza la necessità di modifiche. Questo può essere fatto fornendo i giusti "punti di estensione", o, come menzionato da @AntP, "decomponendo una struttura di classe / funzione al punto in cui ogni punto di estensione naturale è presente di default." L'IMHO che segue l'OCP non ha nulla in comune con "mantenere invariata la vecchia versione per la retrocompatibilità" ! Oppure, citando il commento di @ DerekElkin di seguito:

L'OCP è un consiglio su come scrivere un modulo [...], non sull'implementazione di un processo di gestione delle modifiche che non consente mai ai moduli di cambiare.

I bravi programmatori usano la loro esperienza per progettare componenti tenendo a mente i punti di estensione "giusti" (o - ancora meglio - in modo che non siano necessari punti di estensione artificiali). Tuttavia, per farlo correttamente e senza inutili sovrastorie, è necessario sapere in anticipo come potrebbero apparire i casi d'uso futuri del componente. Anche i programmatori esperti non possono guardare al futuro e conoscere in anticipo tutti i requisiti imminenti. Ed è per questo che a volte è necessario violare la compatibilità con le versioni precedenti, indipendentemente da quanti punti di estensione ha il componente o da quanto segue l'OCP rispetto a determinati tipi di requisiti, ci sarà sempre un requisito che non può essere implementato facilmente senza modificare il componente.


14
Il principale motivo dell'IMO per "violare" l'OCP è che ci vuole molto sforzo per conformarsi ad esso in modo adeguato. Eric Lippert ha un eccellente post sul blog sul perché molte delle classi di framework .NET sembrano violare OCP.
BJ Myers,

2
@BJMyers: grazie per il link. Jon Skeet ha un eccellente post sull'OCP in quanto molto simile all'idea di variazione protetta.
Doc Brown,

8
QUESTO! L'OCP dice che dovresti scrivere codice che può essere modificato senza essere toccato! Perché? Quindi devi solo testare, rivedere e compilare una volta. Il nuovo comportamento dovrebbe derivare dal nuovo codice. Non avvitando con il vecchio codice provato. Che dire di refactoring? Bene refactoring è una chiara violazione di OCP! Ecco perché è un peccato scrivere il codice pensando che lo rifatterai solo se le tue assunzioni cambiano. No! Metti ogni presupposto nella sua piccola scatola. Quando è sbagliato non aggiustare la scatola. Scrivi uno nuovo. Perché? Perché potresti aver bisogno di tornare a quello vecchio. Quando lo fai, sarebbe bello se funzionasse ancora.
candied_orange

7
@CandiedOrange: grazie per il tuo commento. Non vedo refactoring e OCP così contrari mentre lo descrivi. Per scrivere componenti che seguono l'OCP sono spesso necessari diversi cicli di refactoring. L'obiettivo dovrebbe essere un componente che non necessita di modifiche per risolvere un'intera "famiglia" di requisiti. Tuttavia, non si dovrebbero aggiungere punti di estensione arbitrari a un componente "per ogni evenienza", il che porta troppo facilmente alla sovraingegneria. Affidarsi alla possibilità di refactoring può essere la migliore alternativa a questo in molti casi.
Doc Brown,

4
Questa risposta fa un buon lavoro nel mettere in evidenza gli errori nella (attualmente) risposta migliore. Penso che la cosa chiave per avere successo con open / closed sia smettere di pensare in termini di "punti di estensione" e iniziare a pensare di decomporre il tuo struttura di classe / funzione al punto in cui ogni punto di estensione naturale è presente per impostazione predefinita. La programmazione "outside in" è un ottimo modo per raggiungere questo obiettivo, in cui ogni scenario a cui si rivolge il metodo / la funzione corrente viene trasferito a un'interfaccia esterna, che costituisce un naturale punto di estensione per decoratori, adattatori ecc.
Ant P

67

Il principio di apertura / chiusura presenta vantaggi, ma presenta anche alcuni gravi inconvenienti.

In teoria il principio risolve il problema della retrocompatibilità creando codice che è "aperto per estensione ma chiuso per modifica". Se una classe ha alcuni nuovi requisiti, non modificare mai il codice sorgente della classe stessa ma crea invece una sottoclasse che sostituisce solo i membri appropriati necessari per modificare il comportamento. Pertanto, tutto il codice scritto rispetto alla versione originale della classe non è interessato, quindi puoi essere certo che la modifica non ha interrotto il codice esistente.

In realtà si finisce facilmente con il codice gonfio e un pasticcio confuso di classi obsolete. Se non è possibile modificare alcuni comportamenti di un componente tramite l'estensione, è necessario fornire una nuova variante del componente con il comportamento desiderato e mantenere invariata la versione precedente per la compatibilità con le versioni precedenti.

Supponi di scoprire un difetto di progettazione fondamentale in una classe base da cui ereditano molte classi. Supponiamo che l'errore sia dovuto a un campo privato di tipo errato. Non è possibile risolvere questo problema ignorando un membro. Fondamentalmente devi scavalcare l'intera classe, il che significa che finisci per estenderti Objectper fornire una classe base alternativa - e ora devi anche fornire alternative a tutte le sottoclassi, finendo così con una gerarchia di oggetti duplicata, una gerarchia imperfetta, una migliorata . Ma non è possibile rimuovere la gerarchia difettosa (poiché la cancellazione del codice è una modifica), tutti i futuri client saranno esposti ad entrambe le gerarchie.

Ora la risposta teorica a questo problema è "progettala correttamente la prima volta". Se il codice è perfettamente decomposto, senza difetti o errori, e progettato con punti di estensione preparati per tutte le possibili future modifiche ai requisiti, si evita il disordine. Ma in realtà tutti commettono errori e nessuno può prevedere perfettamente il futuro.

Prendi qualcosa come .NET Framework: porta ancora con sé l'insieme delle classi di raccolta che sono state progettate prima dell'introduzione dei farmaci generici più di un decennio fa. Questo è sicuramente un vantaggio per la retrocompatibilità (è possibile aggiornare il framework senza dover riscrivere nulla), ma gonfia anche il framework e offre agli sviluppatori un ampio set di opzioni in cui molti sono semplicemente obsoleti.

Apparentemente gli sviluppatori di React hanno ritenuto che non valesse la pena il costo in termini di complessità e code-bloat seguire rigorosamente il principio aperto / chiuso.

L'alternativa pragmatica all'apertura / chiusura è la deprecazione controllata. Invece di interrompere la retrocompatibilità in una singola versione, i componenti vecchi vengono mantenuti in giro per un ciclo di rilascio, ma i clienti vengono informati tramite avvisi del compilatore che il vecchio approccio verrà rimosso in una versione successiva. Questo dà ai clienti il ​​tempo di modificare il codice. Questo sembra essere l'approccio di React in questo caso.

(La mia interpretazione del principio si basa su The Open-Closed Principle di Robert C. Martin)


37
"Il principio in sostanza dice che non è possibile modificare il comportamento di un componente. Invece è necessario fornire una nuova variante del componente con il comportamento desiderato e mantenere invariata la versione precedente per compatibilità con le versioni precedenti." - Non sono d'accordo con questo. Il principio dice che dovresti progettare i componenti in modo tale che non dovrebbe essere necessario cambiarne il comportamento perché puoi estenderlo per fare quello che vuoi. Il problema è che non abbiamo ancora capito come farlo, soprattutto con le lingue che sono attualmente in uso diffuso. Il problema dell'espressione è una parte di ...
Jörg W Mittag,

8
... quello, per esempio. Né Java né C♯ hanno una soluzione per l'espressione. Haskell e Scala lo fanno, ma la loro base di utenti è molto più piccola.
Jörg W Mittag,

1
@Giorgio: in Haskell, la soluzione sono le classi di tipi. In Scala, la soluzione è implicita e oggetti. Siamo spiacenti, non ho i link a portata di mano, al momento. Sì, i multimetodi (in realtà, non hanno nemmeno bisogno di essere "multi", è piuttosto la natura "aperta" dei metodi di Lisp richiesti) sono anche una possibile soluzione. Si noti che ci sono più frasi del problema dell'espressione, perché in genere gli articoli sono scritti in modo tale che l'autore aggiunge una limitazione al problema dell'espressione che si traduce nel fatto che tutte le soluzioni attualmente esistenti diventano non valide, quindi mostra come le sue ...
Jörg W Mittag,

1
... il linguaggio può persino risolvere questa versione "più difficile". Ad esempio, Wadler ha inizialmente definito il problema dell'espressione non solo per estensione modulare, ma estensione modulare staticamente sicura. I multimetodi Lisp comuni tuttavia non sono staticamente sicuri, sono solo dinamicamente sicuri. Odersky lo ha poi rafforzato ancora di più affermando che dovrebbe essere staticamente sicuro modulare, vale a dire che la sicurezza dovrebbe essere staticamente controllabile senza guardare l'intero programma, solo guardando il modulo di estensione. Questo in realtà non può essere fatto con le classi di tipo Haskell, ma può essere fatto con Scala. E nel ...
Jörg W Mittag il

2
@Giorgio: Esatto. La cosa che fa sì che i multimetodi del Common Lisp risolvano l'EP non è in realtà la spedizione multipla. È il fatto che i metodi sono aperti. Nella tipica FP (o programmazione procedurale), la discriminazione di tipo è legata alle funzioni. Nella tipica OO, i metodi sono legati ai tipi. I metodi Lisp comuni sono aperti , possono essere aggiunti alle classi dopo il fatto e in un modulo diverso. Questa è la caratteristica che li rende utilizzabili per risolvere l'EP. Ad esempio, i protocolli di Clojure sono a invio singolo, ma risolvono anche l'EP (purché non si insista sulla sicurezza statica).
Jörg W Mittag,

20

Definirei l'ideale principio aperto / chiuso. Come tutti gli ideali, tiene in scarsa considerazione le realtà dello sviluppo del software. Inoltre, come tutti gli ideali, è impossibile realizzarlo nella pratica: uno si sforza semplicemente di avvicinarsi a quell'ideale nel miglior modo possibile.

L'altro lato della storia è noto come le manette d'oro. Le manette dorate sono ciò che ottieni quando ti schiavi troppo del principio aperto / chiuso. Le manette dorate sono ciò che accade quando il tuo prodotto che non rompe mai la compatibilità all'indietro non può crescere perché sono stati commessi troppi errori del passato.

Un famoso esempio di ciò si trova nel gestore della memoria di Windows 95. Come parte del marketing per Windows 95, è stato affermato che tutte le applicazioni Windows 3.1 avrebbero funzionato in Windows 95. Microsoft ha effettivamente acquisito licenze per migliaia di programmi per testarle in Windows 95. Uno dei casi problematici era Sim City. In realtà, Sim City aveva un bug che lo faceva scrivere nella memoria non allocata. In Windows 3.1, senza un "corretto" gestore di memoria, questo era un piccolo passo falso. Tuttavia, in Windows 95, il gestore della memoria lo rileva e provoca un errore di segmentazione. La soluzione? In Windows 95, se il nome dell'applicazione è simcity.exe, il sistema operativo allenterà effettivamente i vincoli del gestore della memoria per impedire l'errore di segmentazione!

Il vero problema alla base di questo ideale sono i concetti ridotti di prodotti e servizi. Nessuno fa davvero l'uno o l'altro. Tutto si allinea da qualche parte nella regione grigia tra i due. Se pensi a un approccio orientato al prodotto, aprire / chiudere suona come un grande ideale. I tuoi prodotti sono affidabili. Tuttavia, quando si tratta di servizi, la storia cambia. È facile dimostrare che con il principio di apertura / chiusura, la quantità di funzionalità che il tuo team deve supportare deve avvicinarsi asintoticamente all'infinito, perché non puoi mai ripulire la vecchia funzionalità. Ciò significa che il tuo team di sviluppo deve supportare sempre più codice ogni anno. Alla fine raggiungi un punto di rottura.

La maggior parte dei software oggi, specialmente open source, segue una versione rilassata comune del principio aperto / chiuso. È molto comune vedere aperti / chiusi seguiti con slancio per le versioni minori, ma abbandonati per le versioni principali. Ad esempio, Python 2.7 contiene molte "cattive scelte" dai giorni Python 2.0 e 2.1, ma Python 3.0 li ha spazzati via tutti. (Inoltre, il passaggio dal codice di base di Windows 95 al codice di base di Windows NT quando pubblicarono Windows 2000 ha battuto tutti i tipi di cose, ma ha fatto dire che non abbiamo mai a che fare con un gestore di memoria controllando il nome dell'applicazione per decidere il comportamento!)


Questa è una storia abbastanza bella su SimCity. Hai una fonte?
BJ Myers,

5
@BJMyers È una vecchia storia, Joel Spoleky la menziona verso la fine di questo articolo . Inizialmente l'ho letto come parte di un libro sullo sviluppo di videogiochi anni fa.
Cort Ammon,

1
@BJMyers: Sono abbastanza sicuro che abbiano avuto simili "hack" di compatibilità per dozzine di applicazioni popolari.
Doc Brown,

3
@BJMyers ci sono molte cose come questa, se vuoi una buona lettura vai al blog di The Old New Thing di Raymond Chen , sfoglia il tag History o cerca "compatibilità". C'è un sacco di racconti, incluso qualcosa di molto vicino al già citato caso SimCity - Addentum: a Chen non piace chiamare nomi da incolpare.
Theraot,

2
Pochissime cose si sono rotte anche nella transizione 95-> NT. Il SimCity originale per Windows funziona ancora alla grande su Windows 10 (32 bit). Anche i giochi DOS funzionano ancora perfettamente se si disabilita l'audio o si utilizza qualcosa come VDMSound per consentire al sottosistema console di gestire correttamente l'audio. Microsoft prende molto sul serio la compatibilità con le versioni precedenti e non sta prendendo alcuna scorciatoia "mettiamola in una macchina virtuale". A volte ha bisogno di una soluzione alternativa, ma è comunque piuttosto impressionante, soprattutto in termini relativi.
Luaan,

11

La risposta di Doc Brown è la più accurata e accurata, le altre risposte illustrano incomprensioni del principio chiuso aperto.

Per articolare in modo esplicito l'equivoco, sembra che ci sia la convinzione che l'OCP significa che non si deve fare i cambiamenti all'indietro incompatibili (o anche eventuali modifiche o qualcosa in questo senso.) L'OCP è sulla progettazione di componenti in modo che non ha bisogno di apportare modifiche per estenderne la funzionalità, indipendentemente dal fatto che tali modifiche siano o meno compatibili con le versioni precedenti. Esistono molte altre ragioni oltre all'aggiunta di funzionalità che è possibile apportare modifiche a un componente, indipendentemente dal fatto che siano compatibili all'indietro (ad esempio refactoring o ottimizzazione) o incompatibili all'indietro (ad esempio, deprecando e rimuovendo la funzionalità). Il fatto che tu possa apportare queste modifiche non significa che il tuo componente abbia violato l'OCP (e sicuramente non significa che tu stanno violando l'OCP).

Davvero, non si tratta affatto del codice sorgente. Una dichiarazione più astratta e pertinente dell'OCP è: "un componente dovrebbe consentire l'estensione senza la necessità di violare i suoi confini di astrazione". Vorrei andare oltre e dire che una versione più moderna è: "un componente dovrebbe imporre i suoi confini di astrazione ma consentire l'estensione". Anche nell'articolo sull'OCP di Bob Martin mentre "descrive" "chiuso alla modifica" come "il codice sorgente è inviolato", in seguito inizia a parlare di incapsulamento che non ha nulla a che fare con la modifica del codice sorgente e tutto a che fare con l'astrazione confini.

Quindi, la premessa errata nella domanda è che l'OCP è (inteso come) una linea guida sulle evoluzioni di una base di codice. L'OCP è tipicamente slogan come "un componente dovrebbe essere aperto alle estensioni e chiuso alle modifiche da parte dei consumatori". Fondamentalmente, se un consumatore di un componente desidera aggiungere funzionalità al componente, dovrebbe essere in grado di estendere il vecchio componente in uno nuovo con la funzionalità aggiuntiva, ma non dovrebbe essere in grado di cambiare il vecchio componente.

L'OCP non dice nulla sul creatore di un componente che modifica o rimuove la funzionalità. L'OCP non sta sostenendo per sempre la compatibilità dei bug . Come creatore, tu non stai violando l'OCP cambiando o addirittura rimuovendo un componente. Tu, o meglio i componenti che hai scritto, stai violando l'OCP se l'unico modo in cui i consumatori possono aggiungere funzionalità ai tuoi componenti è mutarlo, ad esempio con il patching delle scimmieo avere accesso al codice sorgente e ricompilare. In molti casi, nessuna di queste sono opzioni per il consumatore, il che significa che se il componente non è "aperto per l'estensione" sono sfortunati. Semplicemente non possono usare il componente per le loro esigenze. L'OCP sostiene di non mettere i consumatori della tua biblioteca in questa posizione, almeno per quanto riguarda una classe identificabile di "estensioni". Anche quando è possibile apportare modifiche al codice sorgente o anche alla copia primaria del codice sorgente, è meglio "fingere" di non poterlo modificare poiché ci sono molte potenziali conseguenze negative nel farlo.

Quindi, per rispondere alle tue domande: No, queste non sono violazioni dell'OCP. Nessuna modifica apportata da un autore può costituire una violazione dell'OCP perché l'OCP non è una dimostrazione di modifiche. Le modifiche, tuttavia, possono creare violazioni dell'OCP e possono essere motivate da guasti dell'OCP nelle versioni precedenti della base di codice. L'OCP è una proprietà di un particolare pezzo di codice, non della storia evolutiva di una base di codice.

Al contrario, la compatibilità con le versioni precedenti è una proprietà di una modifica del codice. Non ha senso dire che un pezzo di codice è o non è retrocompatibile. Ha senso solo parlare della retrocompatibilità di alcuni codici rispetto ad alcuni vecchi codici. Pertanto, non ha mai senso parlare del fatto che il primo taglio di un codice sia retrocompatibile o meno. Il primo taglio di codice può soddisfare o non soddisfare l'OCP, e in generale possiamo determinare se un certo codice soddisfa l'OCP senza fare riferimento a nessuna versione storica del codice.

Per quanto riguarda la tua ultima domanda, è discutibilmente fuori tema per StackExchange in generale perché è principalmente basato sull'opinione pubblica, ma in breve è il benvenuto nella tecnologia e in particolare JavaScript, dove negli ultimi anni il fenomeno che descrivi è stato chiamato stanchezza JavaScript . (Sentiti libero di cercare su Google una varietà di altri articoli, alcuni satirici, parlando di questo da più punti di vista.)


3
"Tu, come creatore, non stai violando l'OCP cambiando o addirittura rimuovendo un componente." - puoi fornire un riferimento per questo? Nessuna delle definizioni del principio che ho visto afferma che "il creatore" (qualunque cosa significhi) è esente dal principio. La rimozione di un componente pubblicato è chiaramente un cambiamento decisivo.
Jacques B,

1
@JacquesB Le persone e persino le modifiche al codice non violano l'OCP, i componenti (ovvero i pezzi di codice effettivi) lo fanno. (E, per essere perfettamente chiari, ciò significa che il componente non è all'altezza dell'OCP stesso, non che viola l'OCP di qualche altro componente.) L'intero punto della mia risposta è che l'OCP non sta parlando di modifiche al codice , rompendo o altro. Un componente è aperto all'estensione e chiuso alla modifica, oppure no, proprio come un metodo può essere privateo meno. Se un autore fa un privatemetodo in publicseguito, ciò non significa che abbiano violato il controllo di accesso, (1/2)
Derek Elkins,

2
... né significa che il metodo non era realmente privateprima. "La rimozione di un componente pubblicato è chiaramente un cambiamento decisivo", non è un sequitur. O i componenti della nuova versione soddisfano l'OCP o no, non è necessario lo storico della base di codice per determinarlo. Secondo la tua logica, non potrei mai scrivere codice che soddisfi l'OCP. State combinando la retrocompatibilità, una proprietà del codice cambia, con l'OCP, una proprietà del codice. Il tuo commento ha più senso del dire che quicksort non è retrocompatibile. (2/2)
Derek Elkins

3
@JacquesB Per prima cosa, nota ancora che si tratta di un modulo conforme all'OCP. L'OCP è un consiglio su come scrivere un modulo in modo che, dato il vincolo che il codice sorgente non può essere modificato, il modulo possa comunque essere esteso. Precedentemente nel documento parla della progettazione di moduli che non cambiano mai, non dell'implementazione di un processo di gestione del cambiamento che non consente mai ai moduli di cambiare. Facendo riferimento alla modifica alla tua risposta, non "rompere l'OCP" modificando il codice del modulo. Invece, se "si estende" il modulo richiede di modificare il codice sorgente, (1/3)
Derek Elkins,

2
"L'OCP è una proprietà di un particolare pezzo di codice, non della storia evolutiva di una base di codice." - eccellente!
Doc Brown,
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.