Le classi / i metodi astratti sono obsoleti?


37

Ho usato per creare molte classi / metodi astratti. Quindi ho iniziato a utilizzare le interfacce.

Ora non sono sicuro che le interfacce non rendano obsolete le classi astratte.

Hai bisogno di una lezione completamente astratta? Crea invece un'interfaccia. Hai bisogno di una classe astratta con qualche implementazione? Crea un'interfaccia, crea una classe. Eredita la classe, implementa l'interfaccia. Un ulteriore vantaggio è che alcune classi potrebbero non aver bisogno della classe genitore, ma implementeranno semplicemente l'interfaccia.

Quindi, le classi / i metodi astratti sono obsoleti?


Che ne dici se il tuo linguaggio di programmazione preferito non supporta le interfacce? Mi sembra di ricordare questo è il caso del C ++.
Bernard,

7
@Bernard: in C ++, una classe astratta è un'interfaccia in tutti tranne il nome. Il fatto che possano fare anche più di interfacce "pure" non è uno svantaggio.
gbjbaanb,

@gbjbaanb: suppongo. Non ricordo di averli usati come interfacce, ma piuttosto di fornire implementazioni predefinite.
Bernard,

Le interfacce sono la "valuta" dei riferimenti agli oggetti. In generale, sono il fondamento del comportamento polimorfico. Le classi astratte hanno uno scopo diverso che Deadalnix ha spiegato perfettamente.
jiggy,

4
Non è forse come dire "i modi di trasporto sono obsoleti ora che abbiamo auto?" Sì, la maggior parte delle volte usi un'auto. Ma se hai mai bisogno di qualcosa di diverso da un'auto o no, non sarebbe proprio corretto dire "Non ho bisogno di usare le modalità di trasporto". Un'interfaccia è molto simile a una classe astratta senza alcuna implementazione e con un nome speciale, no?
Jack V.

Risposte:


111

No.

Le interfacce non possono fornire un'implementazione predefinita, classi astratte e metodi possono. Ciò è particolarmente utile per evitare la duplicazione del codice in molti casi.

Questo è anche un modo davvero carino per ridurre l'accoppiamento sequenziale. Senza metodi / classi astratti, non è possibile implementare il modello di metodo del modello. Ti suggerisco di guardare questo articolo di Wikipedia: http://it.wikipedia.org/wiki/Template_method_pattern


3
@deadalnix Lo schema del metodo modello può essere piuttosto pericoloso. Puoi facilmente finire con un codice altamente accoppiato e iniziare a hackerare il codice per estendere la classe astratta modellata per gestire "solo un altro caso".
quant_dev,

9
Sembra più un anti-pattern. Puoi ottenere esattamente la stessa cosa con la composizione rispetto a un'interfaccia. La stessa cosa si applica alle classi parziali con alcuni metodi astratti. Invece di forzare il codice client per sottoclassare e sovrascriverli, è necessario iniettare l'implementazione . Vedi: en.wikipedia.org/wiki/Composition_over_inheritance#Benefits
back2dos

4
Questo non spiega affatto come ridurre l'accoppiamento sequenziale. Sono d'accordo sul fatto che esistono diversi modi per raggiungere questo obiettivo, ma per favore, rispondi al problema di cui stai parlando invece di invocare ciecamente qualche principio. Finirai per fare la programmazione di culto del carico se non riesci a spiegare perché questo è legato al problema reale.
deadalnix,

3
@deadalnix: Dire che l'accoppiamento sequenziale è meglio ridotto con il modello di modello è la programmazione di culto del carico (l'uso del modello di stato / strategia / fabbrica può anche funzionare), così come l'assunto che deve essere sempre ridotto. L'accoppiamento sequenziale è spesso una semplice conseguenza dell'esposizione di un controllo molto fine su qualcosa, il che significa nient'altro che un compromesso. Ciò non significa ancora che non posso scrivere un wrapper che non ha un accoppiamento sequenziale usando la composizione. In effetti, lo rende molto più semplice.
back2dos

4
Java ora ha implementazioni predefinite per le interfacce. Questo cambia la risposta?
raptortech97,

15

Esiste un metodo astratto in modo che tu possa chiamarlo dalla tua classe base ma implementarlo in una classe derivata. Quindi la tua classe base sa:

public void DoTask()
{
    doSetup();
    DoWork();
    doCleanup();
}

protected abstract void DoWork();

È un modo ragionevolmente piacevole per implementare un buco nel modello centrale senza che la classe derivata sia a conoscenza delle attività di installazione e pulizia. Senza metodi astratti, dovresti fare affidamento sulla classe derivata che implementa DoTaske ricorda di chiamare base.DoSetup()e base.DoCleanup()sempre.

modificare

Inoltre, grazie a deadalnix per la pubblicazione di un link al modello Metodo Pattern , che è quello che ho descritto sopra senza conoscere effettivamente il nome. :)


2
+1, preferisco la tua risposta a deadalnix '. Si noti che è possibile implementare un metodo modello in modo più diretto usando i delegati (in C #):public void DoTask(Action doWork)
Joh

1
@Joh - vero, ma non è necessariamente bello, a seconda della tua API. Se la classe base è Fruite la vostra classe derivata è Apple, si desidera chiamare myApple.Eat(), non è myApple.Eat((a) => howToEatApple(a)). Inoltre, non devi Applechiamare base.Eat(() => this.howToEatMe()). Penso che sia più pulito solo scavalcare un metodo astratto.
Scott Whitlock,

1
@Scott Whitlock: il tuo esempio usando mele e frutta è un po 'troppo distaccato dalla realtà per giudicare se andare in eredità sia preferibile ai delegati in generale. Ovviamente, la risposta è "dipende", quindi lascia molto spazio ai dibattiti ... Ad ogni modo, trovo che i metodi di modello si verificano molto durante il refactoring, ad esempio quando si rimuove il codice duplicato. In questi casi, di solito non voglio fare confusione con la gerarchia dei tipi e preferisco stare lontano dall'eredità. Questo tipo di chirurgia è più facile da fare con le lambda.
Joh,

Questa domanda illustra più di una semplice applicazione del modello Metodo modello: l'aspetto pubblico / non pubblico è noto nella comunità C ++ come modello di interfaccia non virtuale . Avendo una funzione pubblica non virtuale avvolgere quella virtuale - anche se inizialmente la prima non fa altro che chiamare la seconda - si lascia un punto di personalizzazione per installazione / pulizia, registrazione, creazione di profili, controlli di sicurezza ecc. Che potrebbero essere necessari in seguito.
Tony,

10

No, non sono obsoleti.

In effetti, esiste una differenza oscura ma fondamentale tra classi / metodi astratti e interfacce.

se l'insieme di classi in cui una di queste deve essere utilizzata ha un comportamento comune che condividono (classi correlate, intendo), allora scegli Classi / metodi astratti.

Esempio: impiegato, funzionario, direttore - tutte queste classi hanno CalculateSalary () in comune, usano classi base astratte. CalculateSalary () può essere implementato diversamente ma ci sono alcune cose come GetAttendance () per esempio che ha una definizione comune nella classe base.

Se le tue classi non hanno nulla in comune (classi non correlate, nel contesto scelto) tra loro ma ha un'azione che è molto diversa nell'implementazione, allora scegli Interfaccia.

Esempio: mucca, panchina, auto, classi non correlate al telesope ma Isortable può essere lì per ordinarle in un array.

Questa differenza viene generalmente ignorata quando affrontata da una prospettiva polimorfica. Ma personalmente sento che ci sono situazioni in cui l'una è adatta all'altra per il motivo spiegato sopra.


6

Oltre alle altre buone risposte, esiste una differenza fondamentale tra interfacce e classi astratte che nessuno ha menzionato in modo specifico, vale a dire che le interfacce sono molto meno affidabili e quindi impongono un onere di prova molto maggiore rispetto alle classi astratte. Ad esempio, considera questo codice C #:

public abstract class Frobber
{
    private Frobber() {}
    public abstract void Frob(Frotz frotz);
    private class GreenFrobber : Frobber
    { ... }
    private class RedFrobber : Frobber
    { ... }
    public static Frobber GetFrobber(bool b) { ... } // return a green or red frobber
}

public sealed class Frotz
{
    public void Frobbit(Frobber frobber)
    {
         ...
         frobber.Frob(this);
         ...
    }
}

Sono sicuro che ci sono solo due percorsi di codice che devo testare. L'autore di Frobbit può fare affidamento sul fatto che il frobber è rosso o verde.

Se invece diciamo:

public interface IFrobber
{
    void Frob(Frotz frotz);
}
public class GreenFrobber : IFrobber
{ ... }
public class RedFrobber : Frobber
{ ... }

public sealed class Frotz
{
    public void Frobbit(IFrobber frobber)
    {
         ...
         frobber.Frob(this);
         ...
    }
}

Ora non so assolutamente nulla degli effetti di quella chiamata a Frob lì. Devo essere sicuro che tutto il codice di Frobbit sia solido contro ogni possibile implementazione di IFrobber , anche implementazioni di persone che sono incompetenti (cattive) o attivamente ostili a me o ai miei utenti (molto peggio).

Le classi astratte ti consentono di evitare tutti questi problemi; usali!


1
Il problema di cui parli dovrebbe essere risolto usando tipi di dati algebrici, piuttosto che piegare le classi a un punto in cui violano il principio aperto / chiuso.
back2dos,

1
In primo luogo, non ho mai detto che fosse buono o morale . Me lo hai messo in bocca. In secondo luogo (essendo il mio punto reale): non è altro che una scusa scusa per una caratteristica della lingua mancante. Infine, esiste una soluzione senza classi astratte: pastebin.com/DxEh8Qfz . Al contrario, il tuo approccio utilizza classi nidificate e astratte, gettando tutto tranne il codice che richiede la sicurezza in una grande palla di fango. Non vi è alcuna buona ragione per cui RedFrobber o GreenFrobber debbano essere legati ai vincoli che si desidera applicare. Aumenta l'accoppiamento e si blocca in molte decisioni senza alcun vantaggio.
back2dos

1
Dire che i metodi astratti sono obsoleti è sbagliato senza dubbio, ma affermare d'altro canto che dovrebbero essere preferiti rispetto alle interfacce è fuorviante. Sono semplicemente uno strumento per risolvere problemi diversi rispetto alle interfacce.
Groo,

2
"esiste una differenza fondamentale [...] le interfacce sono molto meno [...] affidabili delle classi astratte". Non sono d'accordo, la differenza che hai illustrato nel tuo codice si basa su restrizioni di accesso, che non credo abbiano alcun motivo per differire in modo significativo tra interfacce e classi astratte. Potrebbe essere così in C #, ma la domanda è indipendente dalla lingua.
Joh,

1
@ back2dos: Vorrei solo sottolineare che la soluzione di Eric utilizza, infatti, un tipo di dati algebrico: una classe astratta è un tipo di somma. La chiamata di un metodo astratto è la stessa della corrispondenza del modello su una serie di varianti.
Rodrick Chapman,

4

Lo dici tu stesso:

Hai bisogno di una classe astratta con qualche implementazione? Crea un'interfaccia, crea una classe. Eredita la classe, implementa l'interfaccia

sembra molto lavoro rispetto a "ereditare la classe astratta". Puoi lavorare da solo avvicinandoti al codice da una visione "purista", ma trovo che ho già abbastanza da fare senza cercare di aggiungere al mio carico di lavoro senza alcun vantaggio pratico.


Per non parlare del fatto che se la classe non eredita l'interfaccia (che non è stata menzionata dovrebbe), devi scrivere le funzioni di inoltro in alcune lingue.
Sjoerd,

Umm, l'ulteriore "sacco di lavoro" che hai citato è che hai creato un'interfaccia e le classi lo hanno implementato - sembra davvero molto lavoro? Inoltre, l'interfaccia offre la possibilità di implementare l'interfaccia da una nuova gerarchia che non funzionerebbe con una classe base astratta.
Bill K,

4

Come ho commentato sul post di @deadnix: le implementazioni parziali sono un anti-modello, nonostante il fatto che il modello di modello le formalizzi.

Una soluzione pulita per questo esempio di Wikipedia del modello di modello :

interface Game {
    void initialize(int playersCount);
    void makePlay(int player);
    boolean done();
    void finished();
    void printWinner();
}
class GameRunner {
    public void playOneGame(int playersCount, Game game) {
        game.initialize(playersCount);
        int j = 0;
        for (int i = 0; !game.finished(); i++)
             game.makePlay(i % playersCount);
        game.printWinner();
    }
} 
class Monopoly implements Game {
     //... implementation
}

Questa soluzione è migliore, perché utilizza la composizione anziché l'eredità . Il modello di modello introduce una dipendenza tra l'implementazione delle regole del monopolio e l'implementazione delle modalità di esecuzione dei giochi. Tuttavia, si tratta di due responsabilità completamente diverse e non c'è motivo di accoppiarle.


+1. Come nota a margine: "Le implementazioni parziali sono un anti-modello, nonostante il fatto che il modello di modello le formalizzi". La descrizione su Wikipedia definisce chiaramente il modello, solo l'esempio di codice è "sbagliato" (nel senso che usa l'ereditarietà quando non è necessario ed esiste un'alternativa più semplice, come illustrato sopra). In altre parole, non penso che la colpa sia il modello stesso, ma solo il modo in cui le persone tendono a implementarlo.
Joh,

2

No. Anche la tua alternativa proposta include l'uso di classi astratte. Inoltre, poiché non hai specificato la lingua, vado avanti e dirò che il codice generico è l'opzione migliore dell'eredità fragile comunque. Le classi astratte presentano vantaggi significativi rispetto alle interfacce.


-1. Non capisco cosa c'entri il "codice generico contro l'eredità" con la domanda. Dovresti illustrare o giustificare il motivo per cui "le classi astratte presentano vantaggi significativi rispetto alle interfacce".
Joh,

1

Le classi astratte non sono interfacce. Sono classi che non possono essere istanziate.

Hai bisogno di una lezione completamente astratta? Crea invece un'interfaccia. Hai bisogno di una classe astratta con qualche implementazione? Crea un'interfaccia, crea una classe. Eredita la classe, implementa l'interfaccia. Un ulteriore vantaggio è che alcune classi potrebbero non aver bisogno della classe genitore, ma implementeranno semplicemente l'interfaccia.

Ma poi avresti una classe inutile non astratta. Sono necessari metodi astratti per riempire il foro di funzionalità nella classe base.

Ad esempio, data questa classe

public abstract class Frobber {
    public abstract void Frob();

    public abstract boolean IsFrobbingNeeded { get; }

    public void FrobUntilFinished() {
        while (IsFrobbingNeeded) {
            Frob();
        }
    }
}

Come implementereste questa funzionalità di base in una classe che non ha né Frob()IsFrobbingNeeded?


1
Neanche un'interfaccia può essere istanziata.
Sjoerd,

@Sjoerd: Ma hai bisogno di una classe base con l'implementazione condivisa; quella non può essere un'interfaccia.
configuratore

1

Sono un creatore di servlet framework in cui le classi astratte svolgono un ruolo essenziale. Direi di più, ho bisogno di metodi semi astratti, quando un metodo deve essere ignorato nel 50% dei casi e vorrei vedere l'avvertimento del compilatore su quel metodo non era ignorato. Risolvo il problema aggiungendo annotazioni. Tornando alla tua domanda, ci sono due diversi casi d'uso di classi e interfacce astratte, e finora nessuno è obsoleto.


0

Non penso che siano obsoleti dalle interfacce, ma potrebbero essere obsoleti dal modello di strategia.

L'uso principale di una classe astratta è di rimandare una parte dell'implementazione; per dire "questa parte dell'implementazione della classe può essere diversa".

Sfortunatamente, una classe astratta costringe il cliente a farlo tramite eredità . Considerando che il modello di strategia ti permetterà di ottenere lo stesso risultato senza alcuna eredità. Il client può creare istanze della tua classe piuttosto che definirne sempre la propria e l '"implementazione" (comportamento) della classe può variare in modo dinamico. Il modello di strategia ha l'ulteriore vantaggio che il comportamento può essere modificato in fase di esecuzione, non solo in fase di progettazione, e ha anche un accoppiamento significativamente più debole tra i tipi coinvolti.


0

In genere ho affrontato problemi di manutenzione molto, molto più associati alle interfacce pure rispetto agli ABC, anche agli ABC usati con ereditarietà multipla. YMMV - non so, forse il nostro team li ha usati in modo inadeguato.

Detto questo, se usiamo un'analogia del mondo reale, quanto è utile per le interfacce pure completamente prive di funzionalità e stato? Se uso USB come esempio, questa è un'interfaccia ragionevolmente stabile (penso che ora siamo su USB 3.2, ma ha anche mantenuto la compatibilità con le versioni precedenti).

Eppure non è un'interfaccia senza stato. Non è privo di funzionalità. È più simile a una classe base astratta che a un'interfaccia pura. In realtà è più vicino a una classe concreta con requisiti funzionali e di stato molto specifici, con l'unica astrazione che è quella che si inserisce nella porta come unica parte sostituibile.

Altrimenti sarebbe solo un "buco" nel tuo computer con un fattore di forma standardizzato e requisiti funzionali molto più ampi che non farebbero nulla da solo fino a quando ogni produttore non si inventasse il proprio hardware per fare quel buco fare qualcosa, a quel punto diventa uno standard molto più debole e nient'altro che un "buco" e una specifica di cosa dovrebbe fare, ma nessuna disposizione centrale per come farlo. Nel frattempo potremmo finire con 200 modi diversi di farlo dopo che tutti i produttori di hardware hanno cercato di escogitare i propri modi per collegare funzionalità e affermare quel "buco".

E a quel punto potremmo avere alcuni produttori che presentano problemi diversi rispetto ad altri. Se avessimo bisogno di aggiornare le specifiche, potremmo avere 200 diverse implementazioni di porte USB concrete con modi totalmente diversi di affrontare le specifiche che devono essere aggiornate e testate. Alcuni produttori potrebbero sviluppare implementazioni standard di fatto che condividono tra loro (la tua classe di base analogica che implementa tale interfaccia), ma non tutti. Alcune versioni potrebbero essere più lente di altre. Alcuni potrebbero avere un rendimento migliore ma una latenza peggiore o viceversa. Alcuni potrebbero utilizzare più energia della batteria rispetto ad altri. Alcuni potrebbero sfaldarsi e non funzionare con tutto l'hardware che dovrebbe funzionare con le porte USB. Alcuni potrebbero richiedere che un reattore nucleare sia collegato per funzionare, che tende a provocare avvelenamenti da radiazioni.

Ed è quello che ho trovato, personalmente, con interfacce pure. Potrebbero esserci alcuni casi in cui hanno senso, come solo modellare il fattore di forma di una scheda madre rispetto a un case della CPU. Le analogie dei fattori di forma sono, in effetti, praticamente apolidi e prive di funzionalità, come nel caso del "buco" analogico. Ma spesso considero un enorme errore per i team ritenerlo in qualche modo superiore in tutti i casi, nemmeno vicino.

Al contrario, penso che molti più casi sarebbero risolti meglio dagli ABC che dalle interfacce se queste fossero le due scelte a meno che il tuo team non sia così gigantesco che in realtà è desiderabile avere l'equivalente sopra equivalente di 200 implementazioni USB concorrenti piuttosto che uno standard centrale per mantenere. In un ex team in cui mi trovavo, in realtà ho dovuto lottare duramente solo per allentare lo standard di codifica per consentire ABC e eredità multipla, e principalmente in risposta a questi problemi di manutenzione descritti sopra.

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.