Perché le classi non dovrebbero essere progettate per essere "aperte"?

44

Durante la lettura di varie domande di overflow dello stack e codice di altri, il consenso generale su come progettare le classi è chiuso. Ciò significa che di default in Java e C # tutto è privato, i campi sono definitivi, alcuni metodi sono definitivi e a volte le classi sono addirittura definitive .

L'idea alla base di questo è nascondere i dettagli di implementazione, che è un'ottima ragione. Tuttavia con l'esistenza di protected nella maggior parte dei linguaggi OOP e del polimorfismo, questo non funziona.

Ogni volta che desidero aggiungere o modificare funzionalità in una classe, di solito sono ostacolata da posizioni private e finali ovunque. Qui i dettagli dell'implementazione sono importanti: stai prendendo l'implementazione e la estendi, conoscendo perfettamente quali sono le conseguenze. Tuttavia, poiché non riesco ad accedere ai campi e ai metodi privati e finali, ho tre opzioni:

  • Non estendere la classe, basta aggirare il problema che porta al codice più complesso
  • Copia e incolla l'intera classe, eliminando la riusabilità del codice
  • Sposta il progetto

Queste non sono buone opzioni. Perché non viene utilizzato protected nei progetti scritti in lingue che lo supportano? Perché alcuni progetti proibiscono esplicitamente di ereditare dalle loro classi?

    
posta TheLQ 12.07.2011 - 20:01
fonte

8 risposte

55

Progettare le classi affinché funzionino correttamente quando sono estese, specialmente quando il programmatore che fa l'estensione non comprende appieno come dovrebbe funzionare la classe, richiede uno sforzo considerevole in più. Non puoi semplicemente prendere tutto ciò che è privato e renderlo pubblico (o protetto) e chiamarlo "aperto". Se consenti a qualcun altro di modificare il valore di una variabile, devi considerare in che modo tutti i possibili valori influenzeranno la classe. (Cosa succede se impostano la variabile su null? Una matrice vuota? Un numero negativo?) Lo stesso vale quando si consente ad altre persone di chiamare un metodo. Ci vuole un'attenta riflessione.

Quindi non è tanto che le classi non dovrebbero essere aperte, ma che a volte non vale la pena di aprirle.

Naturalmente, è anche possibile che gli autori delle biblioteche fossero semplicemente pigri. Dipende dalla libreria di cui stai parlando. : -)

    
risposta data 12.07.2011 - 21:40
fonte
24

Rendere tutto privato di default suona duro, ma guardalo dall'altra parte: quando tutto è privato per impostazione predefinita, la creazione di qualcosa di pubblico (o protetto, che è quasi la stessa cosa) dovrebbe essere una scelta consapevole; è il contratto dell'autore della classe con te, il consumatore, su come usare la classe. Questa è una comodità per entrambi: l'autore è libero di modificare il funzionamento interno della classe, purché l'interfaccia rimanga invariata; e tu sai esattamente quali parti della classe puoi contare e quali sono soggette a cambiamenti.

L'idea di base è "accoppiamento lento" (anche noto come "interfacce strette"); il suo valore sta nel ridurre la complessità. Riducendo il numero di modi in cui i componenti possono interagire, viene ridotta anche la quantità di interdipendenza tra di essi; e la interdipendenza è uno dei peggiori tipi di complessità quando si tratta di manutenzione e gestione dei cambiamenti.

Nelle librerie ben progettate, le classi che vale la pena estendere attraverso l'ereditarietà hanno protetto e membri pubblici nei giusti posti e nascondono tutto il resto.

    
risposta data 12.07.2011 - 21:44
fonte
19

Tutto ciò che è non privato è più o meno supposto esistere con un comportamento invariato in ogni versione futura della classe. In effetti, può essere considerato parte dell'API, documentato o meno. Pertanto, esporre troppi dettagli potrebbe causare problemi di compatibilità in seguito.

Riguardo "finale" resp. classi "sigillate", un caso tipico sono classi immutabili. Molte parti del framework dipendono dal fatto che le stringhe siano immutabili. Se la classe String non fosse definitiva, sarebbe facile (anche allettante) creare una sottoclasse String mutabile che causa tutti i tipi di bug in molte altre classi quando viene usata al posto della immutabile classe String.

    
risposta data 13.07.2011 - 01:07
fonte
14

In OO ci sono due modi per aggiungere funzionalità al codice esistente.

Il primo è per eredità: prendi una classe e ne tragga origine. Tuttavia, l'eredità dovrebbe essere usata con cura. È consigliabile utilizzare l'ereditarietà pubblica principalmente quando si ha una relazione isA tra la base e la classe derivata (ad esempio, un rettangolo è una forma ). Invece, si dovrebbe evitare l'ereditarietà pubblica per riutilizzare un'implementazione esistente. In sostanza, l'ereditarietà pubblica viene utilizzata per assicurarsi che la classe derivata abbia la stessa interfaccia della classe base (o di una più grande), in modo che tu possa applicare .

L'altro metodo per aggiungere funzionalità consiste nell'utilizzare la delega o la composizione. Scrivi la tua classe che usa la classe esistente come oggetto interno e vi delega parte del lavoro di implementazione.

Non sono sicuro di quale fosse l'idea alla base di tutte quelle finali della biblioteca che stai tentando di utilizzare. Probabilmente i programmatori volevano assicurarsi che non avresti ereditato una nuova classe da loro, perché non è il modo migliore per estendere il loro codice.

    
risposta data 12.07.2011 - 22:35
fonte
11

La maggior parte delle risposte ha ragione: le classi devono essere sigillate (finale, NotOverridable, ecc.) quando l'oggetto non è progettato o progettato per essere esteso.

Tuttavia, non considererei semplicemente la chiusura di tutto il corretto design del codice. La "O" in SOLID è per "Open-Closed Principle", affermando che una classe dovrebbe essere "chiusa" alle modifiche, ma "aperta" all'estensione. L'idea è che hai un codice in un oggetto. Funziona bene. L'aggiunta di funzionalità non dovrebbe richiedere l'apertura di quel codice e l'esecuzione di modifiche chirurgiche che potrebbero interrompere il comportamento precedente. Invece, si dovrebbe essere in grado di usare l'ereditarietà o l'iniezione di dipendenza per "estendere" la classe per fare cose aggiuntive.

Ad esempio, una classe "ConsoleWriter" potrebbe ricevere del testo e inviarlo alla console. Lo fa bene. Tuttavia, in un certo caso hai ANCHE bisogno che lo stesso output sia scritto su un file. Aprendo il codice di ConsoleWriter, cambiando la sua interfaccia esterna per aggiungere un parametro "WriteToFile" alle funzioni principali, e posizionando il codice di scrittura del file aggiuntivo accanto al codice di scrittura della console sarebbe generalmente considerato una cosa negativa.

Invece, potreste fare una di queste due cose: potreste derivare da ConsoleWriter per formare ConsoleAndFileWriter ed estendere il metodo Write () per chiamare prima l'implementazione di base, e poi anche scrivere su un file. Oppure, puoi estrarre un'interfaccia IWriter da ConsoleWriter e reimplementare quell'interfaccia per creare due nuove classi; un FileWriter e un "MultiWriter" che possono essere dati ad altri IWriter e "trasmetteranno" qualsiasi chiamata fatta ai suoi metodi a tutti gli scrittori dati. Quale sceglierai dipende da cosa avrai bisogno; la derivazione semplice è, beh, più semplice, ma se hai una scarsa previsione di dover eventualmente inviare il messaggio a un client di rete, tre file, due named pipe e la console, vai avanti e prenditi il disturbo di estrarre l'interfaccia e creare il "Y-adattatore"; ti farà risparmiare tempo nel back-end.

Ora, se la funzione Write () non è mai stata dichiarata virtuale (o è stata sigillata), ora sei nei guai se non controlli il codice sorgente. A volte anche se lo fai. Questa è generalmente una brutta posizione in cui entrare e frode gli utenti di API closed-source o a sorgente limitata senza fine quando succede. Ma ci sono motivi legittimi per cui Write (), o l'intera classe ConsoleWriter, sono sigillati; possono esserci informazioni sensibili in un campo protetto (a sua volta sovrascritte da una classe base per fornire dette informazioni). Ci può essere una convalida insufficiente; quando scrivi una API, devi assumere che i programmatori che consumano il tuo codice non siano più intelligenti o benevoli rispetto alla media "utente finale" del sistema finale; in questo modo non sei mai deluso. Prendi il tempo necessario per convalidare qualsiasi cosa il tuo consumatore possa fare per estendere la tua API, o blocca l'API come Fort Knox, in modo che possano fare esattamente ciò che ti aspetti.

    
risposta data 13.07.2011 - 01:27
fonte
2

Penso che probabilmente è da tutte le persone che usano un linguaggio oo per la programmazione in c. Ti sto guardando, java. Se il tuo martello ha la forma di un oggetto, ma vuoi un sistema procedurale e / o non veramente capisci le classi, allora il tuo progetto dovrà essere bloccato.

In sostanza, se stai scrivendo in una lingua oo, il tuo progetto deve seguire il paradigma oo, che include l'estensione. Ovviamente, se qualcuno ha scritto una libreria in un paradigma diverso, forse il tuo non dovrebbe estenderlo comunque.

    
risposta data 12.07.2011 - 20:09
fonte
2

Perché non sanno di meglio.

Gli autori originali si stanno probabilmente aggrappando a un fraintendimento dei principi SOLID che hanno avuto origine in un mondo C ++ confuso e complicato.

Spero che noterete che i mondi rubino, pitone e perl non hanno i problemi che le risposte qui affermano essere la ragione per sigillare. Si noti che è ortogonale alla digitazione dinamica. I modificatori di accesso sono facili da utilizzare nella maggior parte delle lingue (tutte?). I campi C ++ possono essere ingannati con il cast di un altro tipo (il C ++ è più debole). Java e C # possono usare la riflessione. I modificatori di accesso rendono le cose abbastanza difficili da impedirti di farlo a meno che tu NON lo voglia REALMENTE.

Le classi di suggellamento e la marcatura di ogni membro privato violano esplicitamente il principio secondo cui le cose semplici dovrebbero essere semplici e le cose difficili dovrebbero essere possibili. All'improvviso le cose dovrebbero essere semplici, non lo sono.

Ti incoraggio a cercare di capire il punto di vista degli autori originali. Gran parte di esso deriva da un'idea accademica di incapsulamento che non ha mai dimostrato un successo assoluto nel mondo reale. Non ho mai visto un framework o una libreria dove qualche sviluppatore da qualche parte non volesse che funzionasse in modo leggermente diverso e non avesse buoni motivi per cambiarlo. Ci sono due possibilità che potrebbero aver afflitto gli sviluppatori di software originali che hanno sigillato e reso privati i membri.

  1. Arroganza: credevano davvero che fossero aperti per l'estensione e chiusi per la modifica
  2. Complacence - sapevano che potrebbero esserci altri casi d'uso ma hanno deciso di non scrivere per quei casi d'uso

Penso che nella struttura aziendale wold, # 2 sia probabilmente il caso. Questi framework C ++, Java e .NET devono essere "fatti" e devono seguire alcune linee guida. Tali linee guida di solito indicano tipi sigillati a meno che il tipo non sia esplicitamente progettato come parte di una gerarchia di tipi e membri privati per molte cose che potrebbero essere utili per altri utenti .. ma poiché non si riferiscono direttamente a quella classe, non sono esposti . Estrarre un nuovo tipo sarebbe troppo costoso per supportare, documentare, ecc ...

L'idea alla base dei modificatori di accesso è che i programmatori dovrebbero essere protetti da se stessi. "La programmazione C è male perché ti permette di spararti ai piedi." Non è una filosofia che concordo con un programmatore.

Preferisco di gran lunga l'approccio al mangling dei nomi di Python. Puoi facilmente (molto più facile della riflessione) sostituire i privati, se necessario. Una bella recensione su di esso è disponibile qui: link

Il modificatore privato di Ruby è in realtà più simile al protetto in C # e non ha un modificatore privato come C #. Protetto è un po 'diverso. C'è una grande spiegazione qui: link

Ricorda che il tuo linguaggio statico non deve conformarsi agli stili antiquati del codice scritto in quella lingua.

    
risposta data 13.07.2011 - 04:11
fonte
1

EDIT: Credo che le classi dovrebbero essere progettate per essere aperte. Con alcune restrizioni, ma non dovrebbe essere chiuso per ereditarietà.

Perché: "Le classi finali" o "classi sigillate" mi sembrano strane. Non devo mai contrassegnare una delle mie classi come "finale" ("sealed"), perché potrei dover ereditare quelle classi, più tardi.

Ho acquistato / scaricato librerie di terze parti con classi, (la maggior parte dei controlli visivi) e davvero grato che nessuna di quelle librerie abbia utilizzato "final", anche se supportato dal linguaggio di programmazione, in cui sono codificate, perché ho terminato di estendere quelle classi, anche se ho compilato librerie senza il codice sorgente.

A volte, ho a che fare con alcune classi "sigillate" occasionali e ho terminato di creare un nuovo "wrapper" di classe che contiene la classe data, che ha membri simili.

class MyBaseWrapper {
  protected FinalClass _FinalObject;

  public virtual DoSomething()
  {
    // these method cannot be overriden,
    // its from a "final" class
    // the wrapper method can  be open and virtual:
    FinalObject.DoSomething();
  }
} // class MyBaseWrapper


class MyBaseWrapper: MyBaseWrapper {

  public override DoSomething()
  {
    // the wrapper method allows "overriding",
    // a method from a "final" class:
    DoSomethingNewBefore();
    FinalObject.DoSomething();
    DoSomethingNewAfter();
  }
} // class MyBaseWrapper

I clasifier di ambito consentono di "sigillare" alcune funzionalità senza limitare l'ereditarietà.

Quando sono passato da Progr strutturato. a OOP, ho iniziato a utilizzare i membri della classe private , ma, dopo molti progetti, ho terminato di utilizzare protected e, infine, di promuovere tali proprietà o metodi a public , se necessario.

P.S. Non mi piace "final" come parola chiave in C #, preferisco usare "sealed" come Java, o "sterile", seguendo la metafora dell'ereditarietà. Inoltre, "final" è una parola ambiguos usata in diversi contesti.

    
risposta data 13.07.2011 - 00:53
fonte

Leggi altre domande sui tag