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


29

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

  • classi astratte : contratto non destinato ad essere istanziato così com'è.
  • classi finali / sigillate : implementazione non destinata a servire come classe base per qualcos'altro.

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

Perché una classe dovrebbe essere diversa da "astratta" o "finale / sigillata"?

MODIFICARE

Questo fantastico articolo spiega le mie preoccupazioni molto meglio di me.


21
Perché si chiama il Open/Closed principle, non ilClosed Principle.
StuperUser

2
Che tipo di cose scrivi professionalmente? Potrebbe benissimo influenzare la tua opinione in merito.

Bene, conosco alcuni sviluppatori di piattaforme che sigillano tutto, perché vogliono minimizzare l'interfaccia di ereditarietà.
K ..

4
@StuperUser: non è un motivo, è una banalità. L'OP non sta chiedendo quale sia la banalità, sta chiedendo perché . Se non c'è motivo dietro un principio, non c'è motivo di prenderlo in considerazione.
Michael Shaw,

1
Mi chiedo quanti framework dell'interfaccia utente si rompano cambiando la classe Window in sigillata.
Reactgular,

Risposte:


46

Ironia della sorte, trovo il contrario: l'uso di classi astratte è l'eccezione piuttosto che la regola e tendo ad aggrottare le sopracciglia / classi sigillate.

Le interfacce sono un meccanismo di progettazione per contratto più tipico perché non si specifica alcun interno, non si è preoccupati per loro. Permette che ogni esecuzione di quel contratto sia indipendente. Questa è la chiave in molti domini. Ad esempio, se stavi costruendo un ORM sarebbe molto importante che tu possa 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 per cablare i componenti che possono o meno essere applicati a tutte le implementazioni.

Per le classi finali / sigillate, l'unica scusa che posso mai vedere per usarle è quando è effettivamente pericoloso consentire l'override - forse un algoritmo di crittografia o qualcosa del genere. A parte questo, non sai mai quando potresti voler estendere la funzionalità per motivi locali. Sigillare una classe limita le opzioni per guadagni inesistenti nella maggior parte degli scenari. È molto più flessibile scrivere le tue lezioni in modo che possano essere estese in seguito.

Quest'ultima visione è stata cementata per me lavorando con componenti di terze parti che hanno sigillato le classi impedendo in tal modo un'integrazione che avrebbe reso la vita molto più semplice.


17
+1 per l'ultimo paragrafo. Spesso ho trovato troppa incapsulamento in librerie di terze parti (o persino classi di biblioteche standard!) Per essere una causa di dolore più grande di incapsulamento troppo piccolo.
Mason Wheeler,

5
Il solito argomento per sigillare le classi è che non puoi prevedere i molti modi in cui un cliente potrebbe sovrascrivere la tua classe, e quindi non puoi dare alcuna garanzia sul suo comportamento. Vedi blogs.msdn.com/b/ericlippert/archive/2004/01/22/…
Robert Harvey,

12
@RobertHarvey Conosco l'argomento e in teoria suona alla grande. Come progettista di oggetti non puoi prevedere in che modo le persone possono estendere le tue classi - questo è esattamente il motivo per cui non dovrebbero essere sigillate. Non puoi supportare tutto, va bene. Non farlo. Ma non togliere nemmeno le opzioni.
Michael,

6
@RobertHarvey non è che solo le persone che infrangono LSP ottengono ciò che meritano?
Stuper:

2
Non sono nemmeno un grande fan delle classi sigillate, ma potrei capire perché alcune aziende come Microsoft (che spesso devono supportare cose che non si sono spezzate) le trovano interessanti. Gli utenti di un framework non devono necessariamente avere conoscenza degli interni di una classe.
Robert Harvey,

11

Questo è un ottimo 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 sulle (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'attuazione di quel framework.


Ma puoi applicare lo stesso argomento alle classi che non fanno parte di un'interfaccia pubblica. Uno dei motivi principali per rendere definitiva una classe è che agisce come misura di sicurezza per prevenire il codice sciatto. Tale argomento è valido per qualsiasi base di codice condivisa da diversi sviluppatori nel tempo, o anche se sei l'unico sviluppatore. Protegge solo la semantica del codice.
DPM,

Penso che l'idea che le classi debbano essere astratte o sigillate faccia parte di un principio più ampio, vale a dire che si dovrebbe evitare di usare variabili di tipi di classe istantanei. Tale evitamento consentirà di creare un tipo che può essere utilizzato dai consumatori di un determinato tipo, senza che sia necessario ereditare tutti i suoi membri privati. Sfortunatamente, tali progetti non funzionano davvero con la sintassi del costruttore pubblico. Invece, il codice dovrebbe sostituire new List<Foo>()con qualcosa di simile List<Foo>.CreateMutable().
supercat

5

Dal punto di vista java, penso che le lezioni finali non siano così intelligenti come sembra.

Molti strumenti (in particolare AOP, JPA ecc.) Funzionano in modo da ridurre i tempi di caricamento, quindi devono estendere i tuoi clasi. L'altro modo sarebbe quello di creare delegati (non quelli .NET) delegare tutto alla classe originale, il che sarebbe molto più complicato che estendere le classi degli utenti.


5

Due casi comuni in cui avrai bisogno di lezioni di vaniglia, non sigillate:

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

  2. Principio: a volte è desiderabile scrivere classi esplicitamente progettate per essere estese in modo sicuro . (Ciò accade molto quando si scrive un'API come Eric Lippert o quando si lavora in gruppo su un grande progetto). A volte vuoi scrivere una classe che funzioni bene da sola, ma è progettata pensando all'estensibilità.

I pensieri di Eric Lippert su marche di tenuta senso, ma ammette anche che essi fanno di progettazione per l'estensibilità, lasciando la classe "open".

Sì, molte classi sono sigillate nel BCL, ma un numero enorme di classi non lo sono e possono essere estese in tutti i modi meravigliosi. Un esempio che viene in mente è in Windows Form, dove è possibile aggiungere dati o comportamenti a quasi tutti Controltramite ereditarietà. Certo, questo avrebbe potuto essere fatto in altri modi (motivo decorativo, vari tipi di composizione, ecc.), Ma anche l'eredità funziona molto bene.

Due note specifiche .NET:

  1. Nella maggior parte dei casi, le classi di tenuta spesso non sono fondamentali per la sicurezza, poiché gli eredi non possono interferire con le non virtualfunzionalità, ad eccezione delle implementazioni esplicite dell'interfaccia.
  2. A volte un'alternativa adatta è quella di rendere il Costruttore internalinvece di sigillare la classe, il che gli consente di essere ereditato all'interno della tua base di codice, ma non al di fuori di esso.

4

Credo che la vera ragione per cui molte persone ritengono che le classi debbano essere final/ sealedè che la maggior parte delle classi estendibili non astratte non siano adeguatamente documentate .

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

Il fatto è che i programmatori adorano riutilizzare il codice. Anche quando non è una buona idea. E l'ereditarietà è uno strumento chiave in questa "ripresa" del 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. Tutte le solite cose.

Tuttavia, quando si documenta correttamente una classe estensibile, è necessario includere almeno quanto segue :

  • Dipendenze tra metodi (quale metodo chiama quale, ecc.)
  • Dipendenze da variabili locali
  • Contratti interni che la classe allargante dovrebbe onorare
  • Chiama la convenzione per ciascun metodo (ad es. Quando lo sostituisci, chiami l' superimplementazione? La chiami all'inizio del metodo o alla fine? Pensa a costruttore contro distruttore)
  • ...

Questi sono appena in cima alla mia testa. E posso fornirti un esempio del motivo per cui ognuno di questi è importante e saltarlo rovinerà una classe in espansione.

Ora, considera quanto sforzo di documentazione dovrebbe essere impiegato per documentare correttamente ognuna di queste cose. Credo che una classe con 7-8 metodi (che potrebbe essere troppo in un mondo idealizzato, ma è troppo poco nella realtà) potrebbe anche avere una documentazione di 5 pagine, solo testo. Quindi, invece, ci allontaniamo a metà strada e non sigilliamo la classe, in modo che altre persone possano usarla, ma non documentarla nemmeno correttamente, dal momento che ci vorrà una quantità enorme di tempo (e, sai, potrebbe mai essere esteso comunque, quindi perché preoccuparsi?).

Se stai progettando una classe, potresti provare la tentazione di sigillarla in modo che le persone non possano usarla in un modo che non hai previsto (e preparato per). D'altra parte 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, mi è costato solo 30 minuti per cercare una soluzione alternativa".

Penso che alcuni degli elementi di una soluzione siano:

  • Per prima cosa assicurati che l' estensione sia una buona idea quando sei un cliente del codice e favorire davvero la composizione rispetto all'eredità.
  • Secondo per leggere il manuale nella sua interezza (di nuovo come client) per essere sicuro di non trascurare qualcosa che è menzionato.
  • In terzo luogo, quando si scrive un pezzo di codice che il client utilizzerà, scrivere la documentazione corretta per il codice (sì, la strada lunga). Come esempio positivo, posso fornire i documenti iOS di Apple. Non sono sufficienti per consentire all'utente di estendere sempre correttamente le proprie classi, ma almeno includono alcune informazioni sull'ereditarietà. Il 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 dell'inclusione di molti campioni e test nelle API e quando si esegue un test, è possibile testare anche le catene di eredità: dopo tutto fanno parte del contratto!
  • In quinto luogo, in situazioni in cui sei in dubbio, indica che la classe non è pensata per essere estesa e che farlo è una cattiva idea (tm). Indica che non dovresti essere ritenuto responsabile per tale uso non intenzionale, ma comunque non sigillare la classe. Ovviamente, questo non copre i casi in cui la classe dovrebbe essere sigillata al 100%.
  • Infine, quando si sigilla una classe, fornire un'interfaccia come hook intermedio, in modo che il client possa "riscrivere" la propria versione modificata della classe e aggirare la classe "sigillata". In questo modo, può sostituire la classe sigillata con la sua implementazione. Ora, questo dovrebbe essere ovvio, dal momento che è un accoppiamento libero nella sua forma più semplice, ma vale comunque la pena menzionarlo.

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


3

Una classe non deve essere né finale / sigillata né astratta se:

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

Ad esempio, prendi la ObservableCollection<T>classe in C # . Deve solo aggiungere l'innalzamento di eventi alle normali operazioni di a Collection<T>, motivo per cui si suddivide in sottoclassi Collection<T>. Collection<T>è una classe praticabile da sola, e così ObservableCollection<T>.


Se ne fossi responsabile, probabilmente lo farò CollectionBase(astratto), Collection : CollectionBase(sigillato), ObservableCollection : CollectionBase(sigillato). Se osservi attentamente la Collezione <T> , vedrai che è una classe astratta a metà.
Nicolas Repiquet,

2
Che vantaggio ottieni dall'avere tre classi anziché solo due? Inoltre, come è Collection<T>una "classe astratta a metà"?
FishBasketGordo

Collection<T>espone molte viscere attraverso metodi e proprietà protette ed è chiaramente inteso come una classe base per la raccolta specializzata. Ma non è chiaro dove dovresti inserire il codice, poiché non esistono metodi astratti da implementare. ObservableCollectioneredita Collectione non è sigillato, quindi puoi ereditare di nuovo da esso. E puoi accedere alla Itemsproprietà protetta, consentendoti di aggiungere elementi nella raccolta senza generare eventi ... Bello.
Nicolas Repiquet,

@NicolasRepiquet: in molti linguaggi e framework, i normali mezzi idiomatici per creare un oggetto richiedono che il tipo di variabile che conterrà l'oggetto sia lo stesso del tipo dell'istanza creata. In molti casi, l'uso ideale sarebbe quello di passare i riferimenti a un tipo astratto, ma ciò costringerebbe molto codice a usare un tipo per variabili e parametri e un diverso tipo concreto quando si chiamano costruttori. Difficilmente impossibile, ma un po 'imbarazzante.
supercat

3

Il problema con le classi finali / sigillate è 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 attuale, il che rende difficile sostenere la sua utilità.

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

Nel corso degli anni ho trovato incapsulamento, callback ed eventi molto più flessibili / utili delle lezioni astratte. Vedo molto codice con una grande gerarchia di classi in cui l'incapsulamento e gli eventi avrebbero semplificato la vita dello sviluppatore.


1
Il software sigillato in modo definitivo risolve il problema di "Sento la necessità di imporre la mia volontà ai futuri manutentori di questo codice".
Kaz,

Non è stato aggiunto / sigillato qualcosa di aggiunto a OOP, perché non ricordo che fosse in giro quando ero più giovane. Sembra una sorta di caratteristica pensata dopo.
Reactgular,

La finalizzazione esisteva come procedura di toolchain nei sistemi OOP prima che OOP venisse smorzato con linguaggi come C ++ e Java. I programmatori lavorano in Smalltalk, Lisp con la massima flessibilità: qualsiasi cosa può essere estesa, nuovi metodi aggiunti in ogni momento. Quindi, l'immagine compilata del sistema è soggetta a un'ottimizzazione: si presume che il sistema non verrà esteso dagli utenti finali, quindi ciò significa che l'invio del metodo può essere ottimizzato sulla base del bilancio di quali metodi e le classi esistono ora .
Kaz,

Non penso sia la stessa cosa, perché questa è solo una funzione di ottimizzazione. Non ricordo di essere stato sigillato in tutte le versioni di Java, ma potrei sbagliarmi perché non lo uso molto.
Reactgular,

Solo perché si manifesta come alcune dichiarazioni che devi inserire nel codice non significa che non sia la stessa cosa.
Kaz,

2

La "sigillatura" o la "finalizzazione" nei sistemi a oggetti consentono determinate ottimizzazioni, poiché è noto il grafico completo della spedizione.

Vale a dire, rendiamo difficile la trasformazione del sistema in qualcos'altro, come compromesso per le prestazioni. (Questa è l'essenza della maggior parte dell'ottimizzazione.)

Sotto 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 estenderla arbitrariamente.

Oggi non otteniamo alcuna nuova funzionalità adottando misure per prevenire future estensioni.

Quindi, se lo facciamo per motivi di prevenzione, ciò 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, accidenti!"


1

Le lezioni di prova sembrano venire in mente. Queste sono classi che vengono chiamate in modo automatizzato o "a volontà" in base a ciò che il programmatore / tester sta cercando di realizzare. Non sono sicuro di aver mai visto o sentito parlare di una classe finita di test che è privata.


Di cosa stai parlando? In che modo le classi di prova non devono essere definitive / sigillate / astratte?

1

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

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

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

Ho una classe Export che è la classe base di ogni esportatore. Sa come connettersi al database, scoprire dove era arrivato l'ultima volta che è stato eseguito e creare archivi dei file di dati che genera. Fornisce inoltre la gestione dei file delle proprietà, la registrazione e una semplice gestione delle eccezioni.

Inoltre, ho un esportatore diverso che lavora con ogni tipo di dati, forse c'è attività dell'utente, dati transazionali, dati sulla gestione della liquidità ecc.

In cima a questo stack ho messo un livello specifico del 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 dello schema del database che dovrebbero essere comunque propagate a tutti i clienti. L'unico lavoro che devo fare per ogni cliente è quella parte del codice specifica per quel cliente. Un mondo perfetto!

Quindi la struttura appare come:

Base
 Function1
  Customer1
  Customer2
 Function2
 ...

Il mio punto principale è che progettando il codice in questo modo posso usare l'eredità principalmente per il riutilizzo del codice.

Devo dire che non riesco a pensare a nessun motivo per superare i tre livelli.

Ho usato più volte due livelli, ad esempio per avere una Tableclasse comune che implementa le query della tabella del database mentre le sottoclassi di Tableimplementano i dettagli specifici di ogni tabella usando un enumper definire i campi. Consentire all'attrezzo enumun'interfaccia definita nella Tableclasse ha senso.


1

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


1
Questo è un buon commento, ma non risponde direttamente alla domanda. Per favore, considera di espandere i tuoi pensieri qui.

1

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

Il motivo principale di ciò è garantire che tutte le istanze delle tue classi rispettino l'interfaccia che hai originariamente progettato e documentato. Altrimenti uno sviluppatore che usa le tue classi può potenzialmente creare codice fragile e incoerente e, a sua volta, trasmetterlo ad altri sviluppatori / progetti, rendendo gli oggetti delle tue classi non affidabili.

Da una prospettiva più pratica, c'è davvero un aspetto negativo in questo, dal momento che i client dell'interfaccia della tua libreria non saranno in grado di apportare modifiche e utilizzare le tue classi in modi più flessibili a cui inizialmente hai pensato.

Personalmente, per tutto il codice di cattiva qualità che esiste (e dal momento che ne stiamo discutendo 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 di più facile da mantenere il codice.

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.