Perché una classe dovrebbe essere qualcosa di diverso da "astratto" o "finale / sigillato"?

29

Dopo oltre 10 anni di programmazione di java / c #, mi trovo a creare:

  • classi astratte : contratto che non deve essere istanziato così com'è.
  • classi final / sealed : implementazione non intesa come classe base per qualcos'altro.

Non riesco a pensare a nessuna situazione in cui una semplice "classe" (cioè né astratta né finale / sigillata) sarebbe "programmazione saggia".

Perché una classe dovrebbe essere qualcosa di diverso da "astratto" o "finale / sigillato"?

Modifica

Questo ottimo articolo spiega le mie preoccupazioni meglio di me.

    
posta Nicolas Repiquet 21.11.2012 - 15:58
fonte

12 risposte

45

Ironia della sorte, trovo il contrario: l'uso delle classi astratte è l'eccezione piuttosto che la regola e tendo a disapprovare le classi finali / sigillate.

Le interfacce sono un meccanismo di design-by-contract più tipico perché non si specifica alcun interno: non si è preoccupati di ciò. Permette ad ogni implementazione di quel contratto di essere indipendente. Questo è fondamentale in molti domini. Ad esempio, se si stesse costruendo un ORM sarebbe molto importante passare una query al database in modo uniforme, ma le implementazioni possono essere molto diverse. Se si utilizzano classi astratte per questo scopo, si finisce con il cablaggio fisso in componenti che potrebbero essere o meno applicabili a tutte le implementazioni.

Per le classi finali / sigillate, l'unica scusa che riesco a vedere per usarle è quando è effettivamente pericoloso consentire l'override - forse un algoritmo di crittografia o qualcosa del genere. Oltre a questo, non si sa mai quando si potrebbe desiderare di estendere la funzionalità per ragioni locali. La sigillatura di una classe limita le opzioni per guadagni inesistenti nella maggior parte degli scenari. È molto più flessibile scrivere le tue classi in un modo che possa essere esteso più tardi lungo la linea.

Quest'ultimo punto di vista mi è stato cementato lavorando con componenti di terze parti che hanno sigillato le classi impedendo in tal modo un'integrazione che avrebbe reso la vita molto più facile.

    
risposta data 21.11.2012 - 16:08
fonte
11

Che è un grande articolo di Eric Lippert, ma non credo che supporti il tuo punto di vista.

Sostiene che tutte le classi esposte per l'uso da parte di altri dovrebbero essere sigillate, o altrimenti rese non estendibili.

La tua premessa è che tutte le classi dovrebbero essere astratte o sigillate.

Grande differenza.

L'articolo di EL non dice nulla delle (presumibilmente molte) classi prodotte dalla sua squadra di cui tu e io non sappiamo nulla. In generale, le classi esposte pubblicamente in un framework sono solo un sottoinsieme di tutte le classi coinvolte nell'implementazione di quel framework.

    
risposta data 21.11.2012 - 17:18
fonte
5

Da una prospettiva java penso che le classi finali non siano così intelligenti come sembra.

Molti strumenti (in particolare AOP, JPA, ecc.) funzionano con il tempo di caricamento in modo che debbano estendere le tue clases. L'altro modo sarebbe quello di creare delegati (non quelli .NET) e delegare tutto alla classe originale, che sarebbe molto più disordinata rispetto all'estensione delle classi degli utenti.

    
risposta data 21.11.2012 - 16:49
fonte
5

Due casi comuni in cui hai bisogno di vaniglia, classi non sigillate:

  1. Tecnico: se hai una gerarchia che è più di due livelli di profondità e vuoi essere in grado di creare un'istanza di qualcosa nel mezzo.

  2. Principio: A volte è preferibile scrivere classi che sono progettate per essere estese in modo sicuro . (Questo accade molto quando scrivi un'API come Eric Lippert o quando lavori in un team su un grande progetto). A volte vuoi scrivere una classe che funzioni bene da sola, ma è progettata pensando all'estendibilità.

Le idee di Eric Lippert sulla sigillatura hanno senso, ma ammette anche che progettano fanno per l'estensibilità lasciando la classe "aperta".

Sì, molte classi sono sigillate nel BCL, ma un numero enorme di classi non lo sono e può essere esteso in tutti i modi meravigliosi. Un esempio che viene in mente è in Windows Form, in cui è possibile aggiungere dati o comportamenti a quasi tutti Control tramite ereditarietà. Certo, questo potrebbe essere stato fatto in altri modi (schema decoratore, vari tipi di composizione, ecc.), Ma anche l'ereditarietà funziona molto bene.

Due note specifiche .NET:

  1. Nella maggior parte dei casi, le classi di tenuta spesso non sono critiche per la sicurezza, perché gli eredi non possono interferire con la tua funzionalità non- virtual , ad eccezione delle implementazioni esplicite dell'interfaccia.
  2. A volte un'alternativa adatta è di rendere il Costruttore internal invece di sigillare la classe, il che consente di ereditarlo all'interno della tua base di codice, ma non al di fuori di essa.
risposta data 21.11.2012 - 18:06
fonte
4

Credo che la vera ragione per cui molte persone sentono che le classi dovrebbero essere final / sealed è che la maggior parte delle classi estendibili non astratte sono non correttamente documentate .

Lasciami elaborare. A partire da lontano, c'è la visione di alcuni programmatori che l'ereditarietà come strumento in OOP è ampiamente sfruttata e abusata. Abbiamo letto tutti il principio di sostituzione di Liskov, anche se questo non ci ha impedito di violarne centinaia (forse anche migliaia) di volte.

Il fatto è che i programmatori amano riutilizzare il codice. Anche quando non è una buona idea. E l'ereditarietà è uno strumento chiave in questo "reabusing" di codice. Torna alla domanda finale / sigillata.

La documentazione corretta per una classe finale / sigillata è relativamente piccola: descrivi cosa fa ogni metodo, quali sono gli argomenti, il valore di ritorno, ecc. Tutto il solito.

Tuttavia, quando hai correttamente che documenta una classe estendibile devi includere almeno il seguente :

  • Dipendenze tra i metodi (che metodo chiama quali, ecc.)
  • Dipendenze dalle variabili locali
  • Contratti interni che la classe estendente dovrebbe onorare
  • Convenzione di chiamata per ogni metodo (ad es. quando lo sostituisci, chiami l'implementazione super ? La chiami all'inizio del metodo o alla fine? Pensa al costruttore e al distruttore)
  • ...

Questi sono appena in cima alla mia testa. E posso fornirti un esempio del perché ciascuno di questi è importante e saltarlo rovinerà una classe che si estende.

Ora, considera la quantità di documentazione necessaria per documentare correttamente ciascuna di queste cose. Credo che una classe con 7-8 metodi (che potrebbe essere troppo in un mondo idealizzato, ma troppo poco nel reale) potrebbe anche avere una documentazione di 5 pagine, solo testo. Quindi, invece, ci nascondiamo a metà strada e non sigilliamo la classe, in modo che altre persone possano usarla, ma non documentarla neanche correttamente, poiché ci vorrà una quantità enorme di tempo (e, sai, potrebbe mai essere esteso comunque, quindi perché preoccuparsi?).

Se stai progettando un corso, potresti provare la tentazione di sigillarlo in modo che le persone non possano usarlo in un modo che non hai previsto (e preparato). D'altro canto, quando si utilizza il codice di qualcun altro, a volte dall'API pubblica non vi è alcun motivo visibile per cui la classe sia definitiva, e si potrebbe pensare "Accidenti, questo mi è costato solo 30 minuti in cerca di una soluzione alternativa".

Penso che alcuni degli elementi di una soluzione siano:

  • Innanzitutto assicurati che estendere sia una buona idea quando sei un cliente del codice e davvero preferisci la composizione all'ereditarietà.
  • In secondo luogo a leggi il manuale nella sua interezza (sempre come client) per assicurarti di non trascurare ciò che viene menzionato.
  • In terzo luogo, quando si sta scrivendo una parte di codice che il client utilizzerà, scrivere una documentazione adeguata per il codice (sì, la lunga strada). Come esempio positivo, posso fornire i documenti iOS di Apple. Non sono sufficienti per l'utente per estendere sempre correttamente le loro classi, ma includono almeno alcune informazioni sull'ereditarietà. Che è più di quello che posso dire per la maggior parte delle API.
  • In quarto luogo, in realtà cerca di estendere la tua classe, per assicurarti che funzioni. Sono un grande sostenitore di includere molti campioni e test nelle API, e quando stai facendo un test, potresti anche testare le catene ereditarie: sono una parte del tuo contratto, dopo tutto!
  • Quinto, in situazioni in cui sei in dubbio, indica che la classe non è destinata ad essere estesa e che farlo è una cattiva idea (tm). Indica che non devi essere ritenuto responsabile per tale uso non intenzionale, ma non sigillare la classe. Ovviamente, questo non copre i casi in cui la classe dovrebbe essere sigillata al 100%.
  • Infine, sigillando una classe, fornisci un'interfaccia come un hook intermedio, in modo che il client possa "riscrivere" la sua versione modificata della classe e aggirare la tua classe "sigillata". In questo modo, può sostituire la classe sigillata con la sua implementazione. Ora, questo dovrebbe essere ovvio, poiché è un accoppiamento lento nella sua forma più semplice, ma vale comunque la pena menzionarlo.

Vale anche la pena di menzionare la seguente domanda "filosofica": è se una classe è sealed / final parte del contratto per la classe, o un dettaglio di implementazione? Ora non voglio calpestare lì, ma una risposta a questo dovrebbe anche influenzare la tua decisione se sigillare una classe o meno.

    
risposta data 22.11.2012 - 01:32
fonte
3

Una classe non dovrebbe essere né definitiva / sigillata né astratta se:

  • È utile da solo, cioè è utile avere istanze di quella classe.
  • È vantaggioso che quella classe sia la sottoclasse / la classe base di altre classi.

Ad esempio, prendi la classe ObservableCollection<T> in C # . Deve solo aggiungere la generazione di eventi alle normali operazioni di Collection<T> , motivo per cui sottoclasse Collection<T> . Collection<T> è una classe valida a sé stante e quindi ObservableCollection<T> .

    
risposta data 21.11.2012 - 16:04
fonte
3

Il problema con le classi final / sealed è che stanno cercando di risolvere un problema che non si è ancora verificato. È utile solo quando il problema esiste, ma è frustrante perché una terza parte ha imposto una restrizione. Raramente la classe di tenuta risolve un problema corrente, il che rende difficile argomentare la sua utilità.

Ci sono casi in cui una classe dovrebbe essere sigillata. Per esempio; La classe gestisce risorse / memoria allocate in modo tale da non poter prevedere in che modo le modifiche future potrebbero alterare tale gestione.

Nel corso degli anni ho riscontrato che l'incapsulamento, i callback e gli eventi erano di gran lunga più flessibili / utili, quindi classi astratte. Vedo molto più codice con una grande gerarchia di classi in cui l'incapsulamento e gli eventi avrebbero reso la vita più semplice per lo sviluppatore.

    
risposta data 21.11.2012 - 17:45
fonte
2

"Sigillare" o "finalizzare" nei sistemi oggetto consente determinate ottimizzazioni, perché è noto il grafico completo della spedizione.

Cioè, rendiamo difficile trasformare il sistema in qualcos'altro, come un compromesso per le prestazioni. (Questa è l'essenza della maggior parte dell'ottimizzazione.)

In tutti gli altri aspetti, è una perdita. I sistemi dovrebbero essere aperti ed estensibili per impostazione predefinita. Dovrebbe essere facile aggiungere nuovi metodi a tutte le classi ed estendere arbitrariamente.

Oggi non acquisiamo nuove funzionalità adottando misure per impedire future estensioni.

Quindi, se lo facciamo per la prevenzione stessa, allora quello che stiamo facendo è cercare di controllare la vita dei futuri manutentori. Riguarda l'ego. "Anche quando non lavoro più qui, questo codice verrà mantenuto a modo mio, dannazione!"

    
risposta data 21.11.2012 - 19:50
fonte
1

Le classi di test sembrano venire in mente. Queste sono classi chiamate in modo automatico o "a piacimento" in base a ciò che il programmatore / tester sta tentando di realizzare. Non sono sicuro di aver mai visto o sentito parlare di una classe di test conclusa che è privata.

    
risposta data 21.11.2012 - 16:05
fonte
1

Il momento in cui è necessario considerare una classe che è destinata ad essere estesa è quando si sta facendo un po 'di pianificazione reale per il futuro. Lascia che ti dia un esempio di vita reale dal mio lavoro.

Trascorro una buona parte del mio tempo a scrivere strumenti di interfaccia tra il nostro prodotto principale e i sistemi esterni. Quando creiamo una nuova vendita, un grande componente è un insieme di esportatori progettati per essere eseguiti a intervalli regolari che generano file di dati che descrivono in dettaglio gli eventi accaduti quel giorno. Questi file di dati vengono quindi utilizzati dal sistema del cliente.

Questa è un'ottima opportunità per estendere le lezioni.

Ho una classe di esportazione che è la classe base di ogni esportatore. È in grado di connettersi al database, scoprire dove è stata l'ultima volta che è stato eseguito e creare archivi dei file di dati che genera. Fornisce inoltre la gestione dei file di proprietà, la registrazione e alcune semplici operazioni di gestione delle eccezioni.

Oltre a questo ho un esportatore diverso per lavorare con ogni tipo di dati, forse c'è l'attività dell'utente, i dati transazionali, i dati di gestione della liquidità ecc.

In cima allo stack inserisco un livello specifico per il cliente che implementa la struttura del file di dati di cui il cliente ha bisogno.

In questo modo, l'esportatore di base cambia molto raramente. Gli esportatori del tipo di dati di base a volte cambiano, ma raramente, e di solito solo per gestire le modifiche allo schema del database che dovrebbero comunque essere propagate a tutti i clienti. L'unico lavoro che devo fare per ogni cliente è quella parte del codice che è specifica per quel cliente. Un mondo perfetto!

Quindi la struttura assomiglia a:

Base
 Function1
  Customer1
  Customer2
 Function2
 ...

Il mio punto principale è che architettando il codice in questo modo posso utilizzare l'ereditarietà principalmente per riutilizzare il codice.

Devo dire che non posso pensare ad alcun motivo per superare tre livelli.

Ho usato due livelli molte volte, ad esempio per avere una comune classe Table che implementa le query della tabella di database mentre le sottoclassi di Table implementano i dettagli specifici di ogni tabella utilizzando un enum per definire il campi. Lasciare che enum implementa un'interfaccia definita nella classe Table rende ogni sorta di senso.

    
risposta data 21.11.2012 - 22:48
fonte
1

Ho trovato le classi astratte utili ma non sempre necessarie e le classi sigillate diventano un problema quando si applicano i test unitari. Non puoi prendere in giro o bloccare una classe sigillata, a meno che tu non usi qualcosa come Teleriks justmock.

    
risposta data 22.11.2012 - 01:56
fonte
1

Sono d'accordo con la tua opinione. Penso che in Java, per impostazione predefinita, le classi dovrebbero essere dichiarate "finali". Se non lo rendi definitivo, allora preparalo e documentalo appositamente per l'estensione.

La ragione principale per questo è garantire che qualsiasi istanza delle tue classi si atterrà all'interfaccia che hai progettato e documentato in origine. Altrimenti uno sviluppatore che utilizza le tue classi può potenzialmente creare codice fragile e incoerente e, a sua volta, passarlo ad altri sviluppatori / progetti, rendendo gli oggetti delle tue classi non affidabili.

Da un punto di vista più pratico c'è davvero un aspetto negativo a questo, dal momento che i client dell'interfaccia della tua libreria non saranno in grado di fare alcun tweaking e usare le tue classi in modi più flessibili a cui pensavi in origine.

Personalmente, per tutto il codice di cattiva qualità che esiste (e visto che stiamo discutendo di questo a un livello più pratico, direi che lo sviluppo di Java è più incline a questo) Penso che questa rigidità sia un piccolo prezzo da pagare nella nostra ricerca per più facile manutenzione del codice.

    
risposta data 30.11.2012 - 18:06
fonte

Leggi altre domande sui tag