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


44

Quando si leggono varie domande di Stack Overflow e il codice di altri, il consenso generale su come progettare le classi viene chiuso. Ciò significa che per impostazione predefinita in Java e C # tutto è privato, i campi sono finali, alcuni metodi sono definitivi e talvolta le classi sono persino definitive .

L'idea alla base di ciò è quella di nascondere i dettagli dell'implementazione, che è un'ottima ragione. Tuttavia, con l'esistenza della protectedmaggior parte delle lingue e del polimorfismo OOP, questo non funziona.

Ogni volta che desidero aggiungere o modificare funzionalità a una classe, di solito sono ostacolato da posizioni private e finali posizionate ovunque. Qui i dettagli dell'implementazione sono importanti: stai prendendo l'implementazione e la stai estendendo, conoscendo bene 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, solo aggirare il problema che porta al codice più complesso
  • Copia e incolla l'intera classe, uccidendo la riusabilità del codice
  • Fork il progetto

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


1
Sono d'accordo, ho avuto questo problema con i componenti Java Swing, è davvero un male.
Jonas,


3
Vi sono buone probabilità che se si riscontra questo problema, le classi sono state progettate in modo errato in primo luogo - o forse si sta tentando di utilizzarle in modo errato. Non chiedi informazioni a una classe, chiedi che faccia qualcosa per te - quindi non dovresti generalmente aver bisogno dei suoi dati. Anche se questo non è sempre vero, se trovi che devi accedere ai dati di una classe, è molto probabile che qualcosa sia andato storto.
Bill K,

2
"la maggior parte delle lingue OOP?" So molto di più dove le lezioni non possono essere chiuse. Hai lasciato fuori la quarta opzione: cambiare lingua.
Kevin Cline,

@Jonas Uno dei maggiori problemi di Swing è che espone troppe implementazioni. Questo uccide davvero lo sviluppo.
Tom Hawtin: affronta il

Risposte:


55

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

Quindi non è così tanto che le lezioni non dovrebbero essere aperte, ma che a volte non vale la pena per aprirle.

Certo, è anche possibile che gli autori della biblioteca fossero semplicemente pigri. Dipende dalla biblioteca di cui stai parlando. :-)


13
Sì, questo è il motivo per cui le classi sono sigillate per impostazione predefinita. Poiché non è possibile prevedere i molti modi in cui i clienti possono estendere la classe, è più sicuro sigillarla che impegnarsi a supportare il numero potenzialmente illimitato di modi in cui la classe potrebbe essere estesa.
Robert Harvey,


18
Non devi prevedere come verrà sovrascritta la tua classe, devi solo supporre che le persone che estendono la tua classe sappiano cosa stanno facendo. Se estendono la classe e impostano qualcosa su null quando non dovrebbe essere null, allora è colpa loro, non tua. L'unico posto in cui questo argomento ha senso è nelle applicazioni super critiche in cui qualcosa andrà terribilmente storto se un campo è nullo.
TheLQ,

5
@TheLQ: È un presupposto piuttosto grande.
Robert Harvey,

9
@TheLQ Di chi è la colpa è una domanda altamente soggettiva. Se impostano una variabile su un valore apparentemente ragionevole e non hai documentato il fatto che ciò non era consentito e il tuo codice non ha generato ArgumentException (o equivalente), ma provoca l'interruzione del server o dei lead a un exploit di sicurezza, quindi direi che è colpa tua quanto loro. E se hai documentato i valori validi e messo in atto una verifica dell'argomento, hai fatto esattamente il tipo di sforzo di cui sto parlando e devi chiederti se quello sforzo è stato davvero un buon uso del tuo tempo.
Aaron,

24

Rendere tutto privato per impostazione predefinita sembra duro, ma guardalo dall'altra parte: quando tutto è privato per impostazione predefinita, rendere 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 sai esattamente su quali parti della classe puoi fare affidamento e quali sono soggette a modifiche.

L'idea alla base è "accoppiamento libero" (noto anche come "interfacce strette"); il suo valore sta nel mantenere bassa la complessità. Riducendo il numero di modi in cui i componenti possono interagire, si riduce anche la quantità di dipendenza tra loro; e la dipendenza incrociata è uno dei peggiori tipi di complessità quando si tratta di manutenzione e gestione delle modifiche.

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


Questo ha un senso. C'è ancora il problema però quando hai bisogno di qualcosa di molto simile ma con 1 o 2 cose sono cambiate nell'implementazione (meccanismi interni della classe). Faccio anche fatica a capire che se stai scavalcando la classe, non stai già dipingendo pesantemente dalla sua implementazione?
TheLQ

5
+1 per i vantaggi dell'accoppiamento libero. @TheLQ, è l' interfaccia su cui dovresti dipendere fortemente, non l'implementazione.
Karl Bielefeldt,

19

Si suppone che tutto ciò che non sia privato esista 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 probabilmente causa problemi di compatibilità in un secondo momento.

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


4
Questa è la vera risposta. Altre risposte toccano cose importanti ma il vero bit rilevante è questo: tutto ciò che non è finale / o privateè bloccato sul posto e non può mai essere cambiato .
Konrad Rudolph,

1
@Konrad: fino a quando gli sviluppatori non decidono di rinnovare tutto e di non essere retrocompatibili.
JAB,

È vero, ma vale la pena notare che è un requisito molto più debole rispetto ai publicmembri; protectedi membri formano un contratto solo tra la classe che li espone e le sue classi derivate immediate; al contrario, i publicmembri per i contratti con tutti i consumatori per conto di tutte le possibili classi ereditate presenti e future.
supercat

14

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

Il primo è per eredità: prendi una classe e ne derivi. Tuttavia, l'eredità deve essere usata con cura. Dovresti usare l'eredità pubblica principalmente quando hai una relazione isA tra la base e la classe derivata (es. Un rettangolo è una forma). Invece, dovresti evitare l'eredità pubblica per il riutilizzo di un'implementazione esistente. Fondamentalmente, l'eredità pubblica viene utilizzata per assicurarsi che la classe derivata abbia la stessa interfaccia della classe base (o una più grande), in modo da poter applicare il principio di sostituzione di Liskov .

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

Non sono sicuro di quale sia stata l'idea alla base di tutte quelle finali nella biblioteca che stai cercando di usare. Probabilmente i programmatori volevano assicurarsi che non avresti ereditato una nuova classe dalle loro, perché non è il modo migliore per estendere il loro codice.


5
Ottimo punto con composizione contro eredità.
Zsolt Török,

+1, ma non mi è mai piaciuto l'esempio "è una forma" per l'eredità. Un rettangolo e un cerchio potrebbero essere due cose molto diverse. Mi piace di più da usare, un quadrato è un rettangolo vincolato. I rettangoli possono essere usati come forme.
Tylermac,

@tylermac: davvero. Il caso Square / Rectangle è in realtà uno dei controesempi dell'LSP. Probabilmente, dovrei iniziare a pensare a un esempio più efficace.
anno

3
Quadrato / Rettangolo è solo un controesempio se si consente la modifica.
Starblue,

11

La maggior parte delle risposte ha ragione: le classi dovrebbero essere sigillate (final, NotOverridable, ecc.) Quando l'oggetto non è progettato o non è stato progettato per essere esteso.

Tuttavia, non prenderei in considerazione la semplice chiusura di tutto il corretto design del codice. La "O" in SOLID è per "Principio aperto-chiuso", affermando che una classe dovrebbe essere "chiusa" alla modifica, ma "aperta" all'estensione. L'idea è che hai il codice in un oggetto. Funziona benissimo. L'aggiunta di funzionalità non dovrebbe richiedere l'apertura di quel codice e apportare modifiche chirurgiche che potrebbero interrompere il comportamento lavorativo precedente. Invece, si dovrebbe essere in grado di usare l'iniezione di ereditarietà o dipendenza per "estendere" la classe per fare cose aggiuntive.

Ad esempio, una classe "ConsoleWriter" può ottenere testo e inviarlo alla console. Lo fa bene. Tuttavia, in un determinato caso è necessario ANCHE lo stesso output scritto in un file. L'apertura del codice di ConsoleWriter, la modifica della sua interfaccia esterna per aggiungere un parametro "WriteToFile" alle funzioni principali e l'inserimento del codice aggiuntivo per la scrittura di file accanto al codice per la scrittura della console verrebbero generalmente considerati una cosa negativa.

Invece, potresti fare una di queste due cose: potresti derivare da ConsoleWriter per formare ConsoleAndFileWriter ed estendere il metodo Write () per chiamare prima l'implementazione di base, quindi scrivere anche su un file. In alternativa, è possibile estrarre un'interfaccia IWriter da ConsoleWriter e reimplementarla per creare due nuove classi; un FileWriter e un "MultiWriter" a cui possono essere dati altri IWriter e che "trasmettono" qualsiasi chiamata fatta ai suoi metodi a tutti gli autori indicati. Quale scegli dipende da cosa ti servirà; la derivazione semplice è, beh, più semplice, ma se hai una vaga previsione di dover eventualmente inviare il messaggio a un client di rete, tre file, due pipe nominate e la console, vai avanti e prenditi la briga di estrarre l'interfaccia e creare il "Y-adattatore"; E'

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 cattiva posizione in cui trovarsi, e frantuma gli utenti di API di origine chiusa o limitata fino alla fine quando accade. Ma ci sono motivi legittimi per cui Write (), o l'intera classe ConsoleWriter, sono sigillati; potrebbero esserci informazioni sensibili in un campo protetto (sovrascritte a loro volta da una classe base per fornire tali informazioni). La convalida potrebbe non essere sufficiente; quando scrivi un API, devi presumere che i programmatori che consumano il tuo codice non siano più intelligenti o benevoli del "utente finale" medio del sistema finale; in questo modo non sei mai deluso.


Buon punto. L'ereditarietà può essere buona o cattiva, quindi è sbagliato dire che ogni classe dovrebbe essere sigillata. Dipende davvero dal problema attuale.
anno

2

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

Fondamentalmente, se hai intenzione di scrivere in una lingua oo, il tuo progetto deve seguire il paradigma oo, che include l'estensione. Naturalmente, se qualcuno ha scritto una biblioteca in un diverso paradigma, forse non dovresti estenderla comunque.


7
A proposito, OO e procedurale non si escludono a vicenda in modo enfatico. Non puoi avere OO senza procedure. Inoltre, la composizione è tanto un concetto OO quanto l'estensione.
Michael K,

@Michael ovviamente no. Ho affermato che potresti volere un sistema procedurale , cioè uno senza concetti OO. Ho visto persone usare Java per scrivere codice puramente procedurale e non funziona se si tenta di interagire con esso in modo OO.
Spencer Rathbun,

1
Ciò potrebbe essere risolto se Java avesse funzioni di prima classe. Meh.
Michael K,

È difficile insegnare le lingue Java o DOTNet ai nuovi studenti. Di solito insegno procedurale con procedurale Pascal o "Plain C", e successivamente, passare a OO con Object Pascal o "C ++". E, lascia Java per dopo. Insegnare a programmare con una procedura sembra che un singolo oggetto (singleton) funzioni bene.
Umlcat,

@Michael K: In OOP una funzione di prima classe è solo un oggetto con esattamente un metodo.
Giorgio,

2

Perché non sanno niente di meglio.

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

Spero che noterai 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 tipizzazione dinamica. I modificatori di accesso sono facili da usare nella maggior parte (tutte?) Le lingue. I campi C ++ possono essere confusi eseguendo il casting su un altro tipo (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 voglia VERAMENTE.

Sigillare le lezioni e contrassegnare ogni membro come privato viola esplicitamente il principio secondo cui le cose semplici dovrebbero essere semplici e le cose difficili dovrebbero essere possibili. Improvvisamente le cose che dovrebbero essere semplici, non lo sono.

Ti incoraggio a cercare di capire il punto di vista degli autori originali. Gran parte proviene 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 in cui alcuni sviluppatori da qualche parte non avrebbero voluto che funzionasse in modo leggermente diverso e non avessero buone ragioni per cambiarlo. Ci sono due possibilità che potrebbero aver afflitto gli sviluppatori di software originali che hanno sigillato e reso i membri privati.

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

Penso che nel quadro aziendale wold, il numero 2 sia probabilmente il caso. Quei framework C ++, Java e .NET devono essere "fatti" e devono seguire alcune linee guida. Tali linee guida di solito significano tipi sigillati a meno che il tipo non sia stato 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 . L'estrazione di un nuovo tipo sarebbe troppo costosa per supportare, documentare, ecc.

L'idea alla base dei modificatori di accesso è che i programmatori dovrebbero essere protetti da se stessi. "La programmazione in C è cattiva perché ti consente di spararti nel piede." Non è una filosofia con cui sono d'accordo come programmatore.

Preferisco di gran lunga l'approccio mangling al nome di Python. Puoi facilmente (molto più facile della riflessione) sostituire i privati ​​se ne hai bisogno. Un ottimo articolo è disponibile qui: http://bytebaker.com/2009/03/31/python-properties-vs-java-access-modifiers/

Il modificatore privato di Ruby è in realtà più simile a protetto in C # e non ha un modificatore privato come C #. Protetto è un po 'diverso. C'è una grande spiegazione qui: http://www.ruby-lang.org/en/documentation/ruby-from-other-languages/

Ricorda, il tuo linguaggio statico non deve conformarsi agli stili antiquati della scrittura del codice in quel linguaggio passato.


5
"l'incapsulamento non ha mai dimostrato un successo assoluto". Avrei pensato che tutti sarebbero d'accordo sul fatto che la mancanza di incapsulamento ha causato molti problemi.
Joh,

5
-1. Gli sciocchi si precipitano dove gli angeli hanno paura di camminare. Celebrare la possibilità di modificare le variabili private mostra un grave difetto nel tuo stile di codifica.
riwalk

2
Oltre a essere discutibile e probabilmente sbagliato, questo post è anche piuttosto arrogante e offensivo. Ben fatto.
Konrad Rudolph,

1
Esistono due scuole di pensiero i cui sostenitori non sembrano andare d'accordo. Le persone che digitano staticamente vogliono che sia facile ragionare sul software, le persone dinamiche vogliono che sia facile aggiungere funzionalità. Quale è meglio dipende fondamentalmente dalla durata prevista del progetto.
Joh

1

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

Perché: "Classi finali" o "Classi sigillate" mi sembrano strane. Non devo mai contrassegnare una delle mie classi come "finale" ("sigillata"), perché potrei dover ereditare quelle classi, in seguito.

Ho comprato / scaricato librerie di terze parti con classi, (la maggior parte dei controlli visivi) e nessuna di quelle librerie veramente grate ha usato "final", anche se supportato dal linguaggio di programmazione, in cui erano codificate, perché ho finito 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 finito per creare un nuovo "wrapper" di classe contenente 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 classificatori di ambito consentono di "sigillare" alcune funzionalità senza limitare l'ereditarietà.

Quando sono passato da Progr strutturato. a OOP, ho iniziato a utilizzare membri della classe privata , ma, dopo molti progetti, ho finito con l'utilizzo di protetto e, eventualmente, ho promosso quelle proprietà o metodi al pubblico , se necessario.

PS Non mi piace "finale" come parola chiave in C #, piuttosto uso "sigillato" come Java o "sterile", seguendo la metafora dell'ereditarietà. Inoltre, "final" è una parola ambiguos utilizzata in diversi contesti.


-1: Hai dichiarato che ti piacciono i progetti aperti, ma questo non risponde alla domanda, motivo per cui le classi non dovrebbero essere progettate per essere aperte.
Joh,

@Joh Scusa, non mi sono spiegato bene, ho cercato di esprimere l'opinione opposta, non dovevo essere sigillato. Grazie per descrivere perché non sei d'accordo.
Umlcat,
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.