Quali sono i ruoli di singoli, classi astratte e interfacce?


13

Sto studiando OOP in C ++ e, anche se sono consapevole delle definizioni di questi 3 concetti, non riesco davvero a capire quando o come usarlo.

Usiamo questa classe per l'esempio:

class Person{
    private:
             string name;
             int age;
    public:
             Person(string p1, int p2){this->name=p1; this->age=p2;}
             ~Person(){}

             void set_name (string parameter){this->name=parameter;}                 
             void set_age (int parameter){this->age=parameter;}

             string get_name (){return this->name;}
             int get_age (){return this->age;}

             };

1. Singleton

COME funziona la limitazione della classe per avere un solo oggetto?

PUOI progettare una classe che avrebbe SOLO 2 istanze? O forse 3?

QUANDO si utilizza un singleton consigliato / necessario? È buona pratica?

2. Classe astratta

Per quanto ne so, se esiste una sola funzione virtuale pura, la classe diventa astratta. Quindi, aggiungendo

virtual void print ()=0;

lo farebbe, vero?

PERCHÉ avresti bisogno di una classe il cui oggetto non è richiesto?

3.Interface

Se un'interfaccia è una classe astratta in cui tutti i metodi sono pure funzioni virtuali, allora

Qual è la differenza principale tra i 2?

Grazie in anticipo!


2
Singleton è controverso, fai una ricerca su questo sito per ottenere varie opinioni.
Winston Ewert,

2
Vale anche la pena notare che mentre le classi astratte fanno parte del linguaggio, né singleton né interfacce lo sono. Sono modelli che le persone implementano. Singleton in particolare è qualcosa che richiede un po 'di hacking intelligente per funzionare. (Anche se ovviamente puoi creare un singleton solo per convenzione.)
Gort the Robot

1
Uno alla volta, per favore.
JeffO,

Risposte:


17

1. Singleton

Limiti il ​​numero di istanze perché il costruttore sarà privato, il che significa che solo i metodi statici possono creare istanze di quella classe (ci sono altri trucchi sporchi per realizzarlo, ma non lasciamoci trasportare).

La creazione di una classe che avrà solo 2 o 3 istanze è perfettamente fattibile. Dovresti usare singleton ogni volta che senti la necessità di avere solo un'istanza di quella classe nell'intero sistema. Questo di solito accade alle classi che hanno un comportamento da "manager".

Se vuoi saperne di più su Singletons puoi iniziare da Wikipedia e in particolare per C ++ in questo post .

Ci sono sicuramente alcune cose buone e cattive su questo modello, ma questa discussione appartiene altrove.

2. Classi astratte

Sì, è giusto. Solo un singolo metodo virtuale contrassegnerà la classe come astratta.

Utilizzerai questo tipo di classi quando hai una gerarchia di classi più grande in cui le classi migliori non dovrebbero essere istanziate.

Supponiamo che tu stia definendo una classe Mammal e quindi ereditandola da Dog and Cat. Se ci pensate, non ha molto senso avere un'istanza pura di un mammifero, poiché prima dovete sapere che tipo di mammifero è davvero.

Potenzialmente esiste un metodo chiamato MakeSound () che avrà senso solo nelle classi ereditate, ma non esiste un suono comune che tutti i mammiferi possono fare (è solo un esempio che non sta cercando di dare un caso ai suoni dei mammiferi qui).

Quindi questo significa che Mammal dovrebbe essere una classe astratta poiché avrà un comportamento comune implementato per tutti i mammiferi ma non dovrebbe essere istanziato. Questo è il concetto di base dietro le lezioni astratte ma c'è sicuramente molto altro da imparare.

3. Interfacce

Non ci sono interfacce pure in C ++ nello stesso senso che hai in Java o C #. L'unico modo per crearne uno è avere una classe astratta pura che imita la maggior parte del comportamento desiderato da un'interfaccia.

Fondamentalmente il comportamento che stai cercando è quello di definire un contratto in cui altri oggetti possono interagire senza preoccuparsi dell'implementazione sottostante. Quando si rende una classe puramente astratta, significa che tutta l'implementazione appartiene a qualcos'altro, quindi lo scopo di quella classe riguarda solo il contratto che definisce. Questo è un concetto molto potente in OO e dovresti assolutamente approfondire.

Puoi leggere le specifiche dell'interfaccia per C # in MSDN per avere un'idea migliore:

http://msdn.microsoft.com/en-us/library/ms173156.aspx

Il C ++ fornirà lo stesso tipo di comportamento avendo una classe astratta pura.


2
Una classe base astratta pura ti dà tutto ciò che un'interfaccia fa. Esistono interfacce in Java (e C #) perché i progettisti del linguaggio volevano prevenire l'ereditarietà multipla (a causa dei mal di testa che crea) ma hanno riconosciuto un uso molto comune dell'ereditarietà multipla che non è problematico.
Gort the Robot

@StevenBurnap: ma non in C ++, che è il contesto della domanda.
DeadMG

3
Sta chiedendo informazioni sul C ++ e sulle interfacce. "Interface" non è una caratteristica del linguaggio di C ++, ma le persone creano certamente interfacce in C ++ che funzionano esattamente come le interfacce Java, usando classi di base astratte. Lo hanno fatto prima ancora che esistesse Java.
Gort the Robot,


1
Lo stesso vale per Singletons. In C ++, entrambi sono modelli di progettazione, non caratteristiche del linguaggio. Ciò non significa che le persone non parlino di interfacce in C ++ e di cosa servano. Il concetto di "interfaccia" è emerso da sistemi componenti come Corba e COM, entrambi originariamente sviluppati per essere utilizzati in puro C. In C ++, le interfacce sono in genere implementate con classi di base astratte in cui tutti i metodi sono virtuali. La funzionalità di questo è identica a quella di un'interfaccia Java. Pertanto, il concetto di un'interfaccia Java è intenzionalmente un sottoinsieme di classi astratte C ++.
Gort the Robot

8

La maggior parte delle persone ha già spiegato cosa sono i singoli / le classi astratte. Spero di fornire una prospettiva leggermente diversa e di fornire alcuni esempi pratici.

Singletons: quando vuoi che tutto il codice chiamante usi una singola istanza di variabili, per qualunque motivo, hai le seguenti opzioni:

  • Variabili globali - ovviamente nessun incapsulamento, la maggior parte del codice accoppiato ai globali ... male
  • Una classe con tutte le funzioni statiche - un po 'meglio dei semplici globi, ma questa decisione di progettazione ti porta ancora verso un percorso in cui il codice si basa su dati globali e potrebbe essere molto difficile cambiare in seguito. Inoltre non puoi trarre vantaggio da cose OO come il polimorfismo se tutto ciò che hai sono funzioni statiche
  • Singleton - Anche se esiste solo un'istanza della classe, l'implementazione effettiva della classe non deve sapere nulla del fatto che è globale. Quindi oggi puoi avere una classe che è un singleton, domani puoi semplicemente rendere pubblico il suo costruttore e consentire ai clienti di creare un'istanza di più copie. La maggior parte del codice client che fa riferimento al singleton non dovrebbe cambiare e l'implementazione del singleton stesso non dovrà cambiare. L'unica modifica è come il codice client acquisisce il riferimento singleton in primo luogo.

Di tutte le cattive e cattive opzioni là fuori, se hai bisogno di dati globali, singleton è un approccio MOLTO migliore rispetto a uno dei due precedenti. Ti consente anche di mantenere aperte le tue opzioni se domani cambi idea e decidi di utilizzare l' inversione del controllo invece di disporre di dati globali.

Quindi dove useresti un singleton? Ecco alcuni esempi:

  • Registrazione: se si desidera che l'intero processo abbia un unico registro, è possibile creare un oggetto registro e passarlo ovunque. Ma cosa succede se si dispone di 100.000.000 di righe di codice dell'applicazione legacy? modificarli tutti? Oppure potresti semplicemente introdurre quanto segue e iniziare ad usarlo ovunque tu voglia:

    CLog::GetInstance().write( "my log message goes here" );
  • Cache di connessione al server - Questo era qualcosa che dovevo introdurre nella nostra applicazione. La nostra base di codice, e ce n'era molto, si collegava ai server ogni volta che lo desiderava. Il più delle volte questo andava bene, a meno che non ci fosse alcun tipo di latenza nella rete. Avevamo bisogno di una soluzione e la riprogettazione di un'applicazione di 10 anni non era davvero sul tavolo. Ho scritto un CServerConnectionManager singleton. Quindi ho cercato il codice e ho sostituito le chiamate CoCreateInstanceWithAuth con identica chiamata di firma che ha invocato la mia classe. Ora, dopo il primo tentativo, la connessione è stata memorizzata nella cache e il resto del tempo i tentativi di "connessione" sono stati istantanei. Alcuni sostengono che i singoli siano malvagi. Dico che mi hanno salvato il culo.

  • Per il debug, troviamo spesso una tabella di oggetti in esecuzione globale molto utile. Abbiamo alcune lezioni di cui vorremmo tenere traccia. Derivano tutti dalla stessa classe base. Durante l'istanza, chiamano singleton la tabella oggetti e si registrano. Quando vengono distrutti, annullano la registrazione. Posso raggiungere qualsiasi macchina, collegarmi a un processo e creare un elenco di oggetti in esecuzione. Sono stato nel prodotto per oltre mezzo decennio e non ho mai pensato di aver mai avuto bisogno di 2 tabelle di oggetti "globali".

  • Abbiamo alcune classi di utilità di parser di stringhe relativamente complesse che si basano su espressioni regolari. Le classi di espressioni regolari devono essere inizializzate prima di poter eseguire le corrispondenze. L'inizializzazione è piuttosto costosa perché è quando viene generato un FSM basato sulla stringa di analisi. Tuttavia, dopo ciò, è possibile accedere in sicurezza alla classe delle espressioni regolari da 100 thread perché una volta creato, FSM non cambia mai. Queste classi di parser usano internamente i singleton per assicurarsi che l'inizializzazione avvenga una sola volta. Ciò ha notevolmente migliorato le prestazioni e non ha mai causato problemi a causa di "singoli malvagi".

Detto questo, è necessario tenere presente quando e dove utilizzare i singoli. 9 su 10 volte c'è una soluzione migliore e in ogni caso dovresti usarla. Tuttavia, ci sono momenti in cui singleton è assolutamente la scelta giusta per il design.

Argomento successivo ... interfacce e classi astratte. Innanzitutto, come altri hanno già detto, l'interfaccia È una classe astratta ma va oltre, imponendo che non abbia assolutamente alcuna implementazione. In alcune lingue l'interfaccia della parola chiave fa parte della lingua. In C ++ usiamo semplicemente classi astratte. Microsoft VC ++ ha fatto un passo per definirlo da qualche parte internamente:

typedef struct interface;

... quindi puoi ancora usare la parola chiave dell'interfaccia (verrà anche evidenziata come una parola chiave "reale"), ma per quanto riguarda il compilatore reale, è solo una struttura.

Quindi dove lo useresti? Torniamo al mio esempio di una tabella di oggetti in esecuzione. Diciamo che la classe base ha ...

virtual void print () = 0;

C'è la tua classe astratta. Le classi che utilizzano la tabella degli oggetti di runtime deriveranno tutte dalla stessa classe di base. La classe base contiene un codice comune per la registrazione / annullamento della registrazione. Ma non sarà mai istanziato da solo. Ora posso avere delle classi derivate (es. Richieste, ascoltatori, oggetti di connessione client ...), ognuno implementerà print () in modo che quando si attacca al processo e lo chiede, cosa è in esecuzione, ogni oggetto riporterà il proprio stato.

Gli esempi di classi / interfacce astratte sono innumerevoli e le usi sicuramente (o dovresti usarle) molto, molto più frequentemente che utilizzeresti singleton. In breve, ti consentono di scrivere codice che funziona con tipi di base e non è legato all'implementazione effettiva. Ciò consente di modificare l'implementazione in un secondo momento senza dover modificare troppo codice.

Ecco un altro esempio. Diciamo che ho una classe che implementa un logger, CLog. Questa classe scrive nel file sul disco locale. Comincio a usare questa classe nella mia eredità 100.000 righe di codice. Dappertutto. La vita è bella fino a quando qualcuno dice, ehi scriviamo nel database invece che in un file. Ora creo una nuova classe, chiamiamola CDbLog e scriviamo nel database. Riesci a immaginare la seccatura di passare attraverso 100.000 linee e cambiare tutto da CLog a CDbLog? In alternativa, avrei potuto:

interface ILogger {
    virtual void write( const char* format, ... ) = 0;
};

class CLog : public ILogger { ... };

class CDbLog : public ILogger { ... };

class CLogFactory {
    ILogger* GetLog();
};

Se tutto il codice stesse utilizzando l'interfaccia ILogger, tutto ciò che dovrei cambiare è l'implementazione interna di CLogFactory :: GetLog (). Il resto del codice funzionerebbe in modo automatico senza che io dovessi alzare un dito.

Per ulteriori informazioni sulle interfacce e una buona progettazione OO, consiglio vivamente i principi, i modelli e le pratiche agili dello zio Bob in C # . Il libro è pieno di esempi che usano le astrazioni e fornisce spiegazioni in linguaggio semplice di tutto.


4

QUANDO si utilizza un singleton consigliato / necessario? È buona pratica?

Mai. Peggio ancora, sono una cagna assoluta da eliminare, quindi fare questo errore una volta può perseguitarti per molti, molti anni.

La differenza tra classi astratte e interfacce non è assolutamente nulla in C ++. In genere si dispone di interfacce per specificare alcuni comportamenti della classe derivata, ma senza dover specificare tutto. Questo rende il tuo codice più flessibile, perché puoi scambiare qualsiasi classe che soddisfi le specifiche più limitate. Le interfacce di runtime vengono utilizzate quando è necessaria un'astrazione di runtime.


Le interfacce sono un sottoinsieme di classi astratte. Un'interfaccia è una classe astratta senza metodi definiti. (Una classe astratta senza codice).
Gort the Robot

1
@StevenBurnap: forse in un'altra lingua.
DeadMG

4
"Interface" è solo una convenzione in C ++. Quando l'ho visto usato, è una classe astratta con solo metodi virtuali puri e senza proprietà. Ovviamente puoi scrivere qualsiasi vecchia classe e schiaffeggiare un "I" davanti al nome.
Gort the Robot,

Questo è il modo in cui mi aspettavo che le persone rispondessero a questo post. Una domanda alla volta. Comunque, grazie ragazzi per aver condiviso le vostre conoscenze. In questa comunità vale la pena investire tempo.
Appoll

3

Singleton è utile quando non vuoi più copie di un particolare oggetto, deve esserci solo un'istanza di quella classe - è usata per oggetti che mantengono lo stato globale, devono gestire in qualche modo codici non rientranti, ecc.

Un singleton che ha un numero fisso di 2 o più istanze è un multitono , pensa al pool di connessioni al database ecc.

L'interfaccia specifica un'API ben definita che aiuta a modellare l'interazione tra oggetti. In alcuni casi, potresti avere un gruppo di classi che hanno alcune funzionalità comuni - in tal caso, invece di duplicarlo nelle implementazioni, puoi aggiungere definizioni di metodo all'interfaccia trasformandola in una classe astratta .

Puoi anche avere una classe astratta in cui tutti i metodi sono implementati, ma la contrassegni come astratta per indicare che non dovrebbe essere usata così com'è senza la sottoclasse.

Nota: l' interfaccia e la classe astratta non sono molto diverse nel mondo C ++ con eredità multipla ecc., Ma hanno significati diversi in Java et al.


Davvero ben detto! +1
jmort253

3

Se ti fermi a pensarci, è tutto sul polimorfismo. Vuoi essere in grado di scrivere un pezzo di codice una volta che può fare più di un pensiero a seconda di cosa lo passi.

Supponiamo che abbiamo una funzione come il seguente codice Python:

function foo(objs):
    for obj in objs:
        obj.printToScreen()

class HappyWidget:
    def printToScreen(self):
        print "I am a happy widget"

class SadWidget:
    def printToScreen(self):
        print "I am a sad widget"

L'aspetto positivo di questa funzione è che sarà in grado di gestire qualsiasi elenco di oggetti, purché tali oggetti implementino un metodo "printToScreen". Puoi passargli un elenco di widget felici, un elenco di widget tristi o anche un elenco che ne ha un mix e la funzione foo sarà comunque in grado di fare correttamente le sue cose.

Ci riferiamo a questo tipo di limitazione della necessità di implementare una serie di metodi (in questo caso, printToScreen) come interfaccia e si dice che gli oggetti che implementano tutti i metodi implementino l'interfaccia.

Se stessimo parlando di un linguaggio dinamico e tipicamente da papera come Python, alla fine saremmo praticamente finiti. Tuttavia, il sistema di tipo statico di C ++ richiede che diamo una classe agli oggetti nella nostra funzione e sarà in grado di lavorare solo con sottoclassi di quella classe iniziale.

void foo( Printable *objs[], int n){ //Please correctme if I messed up on the type signature
    for(int i=0; i<n; i++){
        objs[i]->printToScreen();
    }
}

Nel nostro caso, l'unico motivo per cui esiste la classe Printable è quello di dare spazio al metodo printToScreen. Poiché non esiste un'implementazione condivisa tra le classi che implementano il metodo printToScreen, ha senso rendere Stampabile in una classe astratta che viene utilizzata solo come modo per raggruppare classi simili in una gerarchia comune.

In C ++ i concetti di classe e interfaccia astratti sono un po 'sfocati. Se vuoi definirli meglio, le classi astratte sono ciò a cui stai pensando, mentre le interfacce di solito significano l'idea più generale, in più lingue, dell'insieme di metodi visibili esposti da un oggetto. (Anche se alcuni linguaggi, come Java, usano il termine interfaccia per fare riferimento a qualcosa di più direttamente simile a una classe base astratta)

Fondamentalmente, le classi concrete specificano come vengono implementati gli oggetti, mentre le classi astratte specificano come si interfacciano con il resto del codice. Per rendere le tue funzioni più polimorfiche, dovresti provare a ricevere un puntatore alla superclasse astratta ogni volta che avrebbe senso farlo.


Per quanto riguarda i Singleton, sono davvero abbastanza inutili, dal momento che spesso possono essere sostituiti solo da un gruppo di metodi statici o semplici vecchie funzioni. Tuttavia, a volte hai una sorta di restrizione che ti costringe a usare un oggetto, anche se non vorresti davvero usarne uno, quindi il picchetto singleton è appropriato.


A proposito, alcune persone potrebbero aver commentato che la parola "interfaccia" ha un significato particolare nel linguaggio Java. Penso che per ora sia meglio attenersi alla definizione più generale.


1

interfacce

È difficile capire lo scopo di uno strumento che risolve un problema che non hai mai avuto. Non ho capito le interfacce per un po 'dopo aver iniziato a programmare. Capiremo cosa hanno fatto, ma non sapevo perché avresti voluto usarne uno.

Ecco il problema: sai cosa vuoi fare, ma hai diversi modi per farlo o puoi cambiare il modo in cui lo fai in seguito. Sarebbe bello se potessi interpretare il ruolo del manager privo di sensi: abbaiare alcuni ordini e ottenere i risultati desiderati senza preoccuparsi di come è fatto.

Supponi di avere un piccolo sito Web e di salvare tutte le informazioni dei tuoi utenti in un file CSV. Non è la soluzione più sofisticata, ma funziona abbastanza bene per memorizzare i dettagli dell'utente di tua madre. Successivamente, il tuo sito decolla e hai 10.000 utenti. Forse è il momento di utilizzare un database adeguato.

Se fossi stato intelligente all'inizio, l'avresti visto arrivare e non avrebbe fatto le chiamate per salvare direttamente su CSV. Invece penseresti a cosa ti serviva, indipendentemente da come fosse implementato. Diciamo store()e retrieve(). Si crea Persisterun'interfaccia con metodi astratti per store()e retrieve()e si crea una CsvPersistersottoclasse che implementa effettivamente tali metodi.

Successivamente, puoi creare un oggetto DbPersisterche implementa l'archiviazione e il recupero effettivi dei dati in modo completamente diverso da come è stata eseguita la tua classe CSV.

La cosa grandiosa è che tutto ciò che devi fare ora è cambiare

Persister* prst = new CsvPersister();

per

Persister* prst = new DbPersister();

e poi hai finito. Le tue chiamate verso prst.store()e prst.retrieve()continueranno a funzionare, sono gestite in modo diverso "dietro le quinte".

Ora, dovevi ancora creare le implementazioni cvs e db, quindi non hai ancora provato il lusso di essere il capo. I vantaggi reali sono evidenti quando si utilizzano interfacce create da qualcun altro. Se qualcun altro è stato così gentile da creare un CsvPersister()e DbPersister()già, allora devi solo sceglierne uno e chiamare i metodi necessari. Se decidi di utilizzare l'altro in un secondo momento o in un altro progetto, sai già come funziona.

Sono davvero arrugginito sul mio C ++, quindi userò solo alcuni esempi di programmazione generici. I contenitori sono un ottimo esempio di come le interfacce ti semplificano la vita.

Si possono avere Array, LinkedList, BinaryTree, ecc tutte le sottoclassi di Containerche ha metodi come insert(), find(), delete().

Ora quando aggiungi qualcosa al centro di un elenco collegato, non devi nemmeno sapere cosa sia un elenco collegato. Basta chiamare myLinkedList->insert(4)e scorre magicamente l'elenco e lo inserisce. Anche se sai come funziona un elenco collegato (cosa che dovresti davvero), non devi cercare le sue funzioni specifiche, perché probabilmente sai già che cosa derivano dall'utilizzare un altro in Containerprecedenza.

Classi astratte

Le classi astratte sono abbastanza simili alle interfacce (anche tecnicamente le interfacce sono classi astratte, ma qui intendo le classi di base che hanno alcuni dei loro metodi perfezionati.

Supponiamo che tu stia creando un gioco e che devi rilevare quando i nemici si trovano a breve distanza dal giocatore. È possibile creare una classe di base Enemyche ha un metodo inRange(). Sebbene ci siano molte cose sui nemici che sono diverse, il metodo utilizzato per verificare la loro portata è coerente. Pertanto la tua Enemyclasse avrà un metodo potenziato per controllare il raggio, ma metodi virtuali puri per altre cose che non condividono somiglianze tra i tipi nemici.

La cosa bella di questo è che se sbagli il codice di rilevamento della portata o vuoi modificarlo, devi solo cambiarlo in un posto.

Naturalmente ci sono molte altre ragioni per interfacce e classi base astratte, ma queste sono alcune delle ragioni per cui potresti usarle.

Singletons

Li uso occasionalmente e non sono mai stato bruciato da loro. Questo non vuol dire che a un certo punto non rovineranno la mia vita, sulla base delle esperienze di altre persone.

Ecco una buona discussione sullo stato globale da parte di alcune persone più esperte e diffidenti: perché lo stato globale è così malvagio?


1

Nel regno animale ci sono vari animali che sono mammiferi. Qui il mammifero è una classe di base e da essa derivano vari animali.

Hai mai visto un mammifero che camminava? Sì, molte volte ne sono sicuro - comunque erano tutti i tipi di mammifero, vero?

Non hai mai visto qualcosa che fosse letteralmente solo un mammifero. Erano tutti i tipi di mammiferi.

Il mammifero di classe è tenuto a definire varie caratteristiche e gruppi ma non esiste come entità fisica.

Pertanto è una classe base astratta.

Come si muovono i mammiferi? Camminano, nuotano, volano ecc?

Non c'è modo di sapere a livello dei mammiferi, ma tutti i mammiferi devono muoversi in qualche modo (diciamo che questa è una legge biologica per facilitare l'esempio).

Pertanto MoveAround () è una funzione virtuale in quanto ogni mammifero che deriva da questa classe deve essere in grado di implementarlo in modo diverso.

Tuttavia, essendo come ogni mammifero DEVE definire MoveAround perché tutti i mammiferi devono muoversi ed è impossibile farlo a livello di mammifero. Deve essere implementato da tutte le classi figlio ma non ha alcun significato nella classe base.

Pertanto MoveAround è una pura funzione virtuale.

Se hai un'intera classe che consente l'attività ma non è in grado di definire al massimo livello come dovrebbe essere fatto, allora tutte le funzioni sono pure virtuali e questa è un'interfaccia.
Ad esempio, se abbiamo un gioco in cui codificherai un robot e me lo invierai per combattere in un campo di battaglia, devo conoscere i nomi delle funzioni e i prototipi da chiamare. Non mi interessa come lo implementi dalla tua parte finché "l'interfaccia" è chiara. Pertanto posso fornirti una classe di interfaccia da cui deriverai per scrivere il tuo robot killer.

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.