Domanda

Il principio aperto-chiuso afferma che le entità software (classi, moduli, funzioni, ecc.) dovrebbero essere aperte per l'estensione, ma chiuse per modifica " ;.

Tuttavia, Joshua Bloch nel suo famoso libro "Efficace Java" fornisce i seguenti consigli: "Progetta e documento per ereditarietà, oppure proibiscili" e incoraggia i programmatori a utilizzare il "finale" modificatore per vietare la sottoclasse.

Penso che questi due principi si contraddicano chiaramente (sbaglio?). Quale principio segui quando scrivi il tuo codice e perché? Lasci le tue lezioni aperte, non ammetti l'eredità su alcune di esse (quali?) O usi il modificatore finale ogni volta che è possibile?

È stato utile?

Soluzione

Francamente penso che il principio aperto / chiuso sia più un anacronismo che no. Parte dagli anni '80 e '90 quando i framework OO sono stati costruiti sul principio che tutto deve ereditare da qualcos'altro e che tutto dovrebbe essere subclassabile.

Questo era il più tipico nei framework dell'interfaccia utente dell'epoca come MFC e Java Swing. In Swing, hai un'eredità ridicola in cui il pulsante (iirc) estende la casella di controllo (o viceversa) dando uno di quei comportamenti che non viene utilizzato (penso che sia la chiamata setDisabled () sulla casella di controllo). Perché condividono un antenato? Nessun altro motivo se non quello di avere alcuni metodi in comune.

Oggi la composizione è preferita rispetto all'eredità. Mentre Java consentiva l'ereditarietà per impostazione predefinita, .Net ha adottato l'approccio (più moderno) di non consentirlo per impostazione predefinita, che ritengo sia più corretto (e più coerente con i principi di Josh Bloch).

DI / IoC hanno inoltre avanzato la tesi per la composizione.

Josh Bloch sottolinea anche che l'eredità rompe l'incapsulamento e fornisce alcuni buoni esempi del perché. È stato anche dimostrato che la modifica del comportamento delle raccolte Java è più coerente se eseguita dalla delega anziché l'estensione delle classi.

Personalmente ritengo che l'ereditarietà sia poco più che un dettaglio di implementazione in questi giorni.

Altri suggerimenti

Non credo che le due affermazioni si contraddicano a vicenda. Un tipo può essere aperto per estensione e comunque chiuso per eredità.

Un modo per farlo è utilizzare l'iniezione di dipendenza. Invece di creare istanze dei propri tipi di supporto, un tipo può essere fornito al momento della creazione. Ciò consente di modificare le parti (ovvero aperte per l'estensione) del tipo senza cambiare il tipo stesso (ovvero chiudere per modifica).

In principio aperto-chiuso (aperto per estensione, chiuso per modifica) tu può ancora usare il modificatore finale. Ecco un esempio:

public final class ClosedClass {

    private IMyExtension myExtension;

    public ClosedClass(IMyExtension myExtension)
    {
        this.myExtension = myExtension;
    }

    // methods that use the IMyExtension object

}

public interface IMyExtension {
    public void doStuff();
}

ClosedClass è chiuso per modifica all'interno della classe, ma aperto per estensione attraverso un altro. In questo caso può trattarsi di tutto ciò che implementa l'interfaccia IMyExtension . Questo trucco è una variazione dell'iniezione di dipendenza poiché stiamo alimentando la classe chiusa con un'altra, in questo caso attraverso il costruttore. Poiché l'estensione è un'interfaccia non può essere final ma può esserlo la sua classe di implementazione.

L'uso di final sulle classi per chiuderle in Java è simile all'uso di sigillato in C #. Ci sono discussioni simili al riguardo sul lato .NET.

Oggi uso il modificatore finale per impostazione predefinita, quasi in modo riflessivo come parte del boilerplate. Rende le cose più facili da ragionare, quando sai che un determinato metodo funzionerà sempre come visto nel codice che stai guardando in questo momento.

Certo, a volte ci sono situazioni in cui una gerarchia di classi è esattamente ciò che vuoi, e sarebbe sciocco non usarne una. Ma avere paura delle gerarchie di più di due livelli o di quelle in cui le classi non astratte sono ulteriormente sottoclassate. Una classe dovrebbe essere astratta o finale.

Il più delle volte, usare la composizione è la strada da percorrere. Metti tutti i macchinari comuni in una classe, metti i diversi casi in classi diverse, quindi le istanze composte per avere un funzionamento completo.

Puoi chiamare questa "iniezione di dipendenza", o "modello di strategia" o " modello di visitatore " o qualsiasi altra cosa, ma ciò a cui si riduce è usare la composizione anziché l'eredità per evitare la ripetizione.

Rispetto moltissimo Joshua Bloch e considero Java efficace praticamente la Bibbia di Java . Ma penso che l'impostazione predefinita per l'accesso privato sia spesso un errore. Tendo a rendere le cose protette di default in modo che possano essere accessibili almeno estendendo la classe. Ciò è in gran parte nato dalla necessità di unit test componenti, ma lo trovo utile anche per ignorare il comportamento predefinito delle classi. Lo trovo molto fastidioso quando lavoro nella base di codice della mia azienda e finisco per dover copiare & amp; modifica la fonte perché l'autore ha scelto di " nascondi " qualunque cosa. Se è assolutamente in mio potere, faccio pressioni affinché l'accesso sia cambiato in protetto per evitare la duplicazione, il che è molto peggio di IMHO.

Tieni anche presente che lo sfondo di Bloch è nella progettazione di molto pubblico librerie API bedrock; la barra per ottenere tale codice "corretto" deve essere impostato su un valore molto alto, quindi è probabile che non sia la stessa situazione della maggior parte del codice che scriverai. Importanti biblioteche come la stessa JRE tendono ad essere più restrittive al fine di garantire che la lingua non venga abusata. Vedi tutte le API obsolete in JRE? È quasi impossibile modificarli o rimuoverli. La tua base di codice probabilmente non è impostata su pietra, quindi hai hai l'opportunità di sistemare le cose se si scopre che hai fatto un errore inizialmente.

Le due dichiarazioni

  

Le entità software (classi, moduli, funzioni, ecc.) devono essere aperte per l'estensione, ma chiuse per modifica.

e

  

Progetta e documenta l'eredità, oppure proibiscili.

non sono in diretta contraddizione l'uno con l'altro. È possibile seguire il principio aperto-chiuso fintanto che si progetta e si documenta per esso (secondo il consiglio di Bloch).

Non credo che Bloch affermi che dovresti preferire di proibire l'ereditarietà usando il modificatore finale, solo che dovresti scegliere esplicitamente di consentire o vietare l'ereditarietà in ogni classe che crei. Il suo consiglio è che dovresti pensarci e decidere tu stesso, invece di accettare il comportamento predefinito del compilatore.

Non credo che il principio Open / closed come presentato in origine consenta l'interpretazione secondo cui le classi finali possono essere estese attraverso l'iniezione di dipendenze.

Secondo la mia comprensione, il principio consiste nel non consentire modifiche dirette al codice che è stato messo in produzione, e il modo per ottenerlo pur consentendo modifiche alla funzionalità è utilizzare l'ereditarietà dell'implementazione.

Come sottolineato nella prima risposta, questo ha radici storiche. Decenni fa, l'ereditarietà era a favore, i test degli sviluppatori erano inauditi e la ricompilazione della base di codice spesso richiedeva troppo tempo.

Inoltre, considera che in C ++ i dettagli di implementazione di una classe (in particolare, campi privati) erano comunemente esposti in " .h " file di intestazione, quindi se un programmatore dovesse modificarlo, tutti i client richiederebbero la ricompilazione. Si noti che questo non è il caso di linguaggi moderni come Java o C #. Inoltre, non credo che gli sviluppatori di allora potessero contare su sofisticati IDE in grado di eseguire analisi delle dipendenze al volo, evitando la necessità di frequenti ricostruzioni complete.

Nella mia esperienza, preferisco fare l'esatto contrario: "le classi dovrebbero essere chiuse per estensione ( final ) per impostazione predefinita, ma aperte per modifiche " ;. Pensaci: oggi preferiamo pratiche come controllo versione (semplifica il recupero / confronto delle versioni precedenti di una classe), refactoring (che ci incoraggia a modificare il codice per migliorare design o come preludio all'introduzione di nuove funzionalità) e test per sviluppatori , che fornisce una rete di sicurezza durante la modifica del codice esistente.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top