Interfaccia ed ereditarietà: il meglio dei due mondi?


10

Ho "scoperto" le interfacce e ho iniziato ad amarle. La bellezza di un'interfaccia è che si tratta di un contratto e qualsiasi oggetto che adempie a quel contratto può essere utilizzato ovunque sia richiesta tale interfaccia.

Il problema con un'interfaccia è che non può avere un'implementazione predefinita, il che è una seccatura per proprietà banali e sconfigge DRY. Anche questo è positivo, perché mantiene disaccoppiate l'implementazione e il sistema. L'ereditarietà, invece, mantiene un accoppiamento più stretto e ha il potenziale di rompere l'incapsulamento.

Caso 1 (Eredità con membri privati, buona incapsulamento, strettamente accoppiata)

class Employee
{
int money_earned;
string name;

public:
 void do_work(){money_earned++;};
 string get_name(return name;);
};


class Nurse : public Employee: 
{
   public:
   void do_work(/*do work. Oops, can't update money_earned. Unaware I have to call superclass' do_work()*/);

};

void HireNurse(Nurse *n)
{
   nurse->do_work();
)

Caso 2 (solo un'interfaccia)

class IEmployee
{
     virtual void do_work()=0;
     virtual string get_name()=0;
};

//class Nurse implements IEmployee.
//But now, for each employee, must repeat the get_name() implementation,
//and add a name member string, which breaks DRY.

Caso 3: (il migliore dei due mondi?)

Simile al caso 1 . Tuttavia, immagina che (ipoteticamente) il C ++ non consentisse metodi di esclusione ad eccezione di quei metodi che sono puramente virtuali .

Quindi, nel caso 1 , l'override di do_work () causerebbe un errore di compilazione. Per risolvere questo problema, impostiamo do_work () come puro virtuale e aggiungiamo un metodo separato increment_money_earned (). Come esempio:

class Employee
{
int money_earned;
string name;

public:
 virtual void do_work()=0;
 void increment_money_earned(money_earned++;);
 string get_name(return name;);
};


class Nurse : public Employee: 
{
   public:
   void do_work(/*do work*/ increment_money_earned(); ); .
};

Ma anche questo ha problemi. E se tra 3 mesi, Joe Coder crea un impiegato medico, ma si dimentica di chiamare increment_money_earned () in do_work ()?


La domanda:

  • Il caso 3 è superiore al caso 1 ? È perché "incapsulamento migliore" o "accoppiamento più libero", o qualche altra ragione?

  • Il caso 3 è superiore al caso 2 perché è conforme al DRY?


2
... stai reinventando le lezioni astratte o cosa?
ZJR,

Risposte:


10

Un modo per risolvere il problema dell'oblio della superclasse è quello di restituire il controllo alla superclasse! Ho reimpostato il tuo primo esempio per mostrare come (e fatto compilare;)). Oh, presumo anche che do_work()in Employeedovesse essere il virtualtuo primo esempio.

#include <string>

using namespace std;

class Employee
{
    int money_earned;
    string name;
    virtual void on_do_work() {}

    public:
        void do_work() { money_earned++; on_do_work(); }
        string get_name() { return name; }
};

class Nurse : public Employee
{
    void on_do_work() { /* do more work. Oh, and I don't have to call do_work()! */ }
};

void HireNurse(Nurse* nurse)
{
    nurse->do_work();
}

Ora do_work()non può essere ignorato. Se vuoi estenderlo devi farlo tramite il on_do_work()quale do_work()ha il controllo.

Questo, ovviamente, può essere usato con l'interfaccia del tuo secondo esempio e se lo Employeeestende. Quindi, se ti capisco correttamente, penso che questo sia il caso 3, ma senza dover usare l'ipotetico C ++! È ASCIUTTO e ha un forte incapsulamento.


3
E questo è il modello di progettazione noto come "metodo modello" ( en.wikipedia.org/wiki/Template_method_pattern ).
Joris Timmermans,

Sì, questo è conforme al caso 3. Sembra promettente. Esamineremo in dettaglio. Inoltre, si tratta di una sorta di sistema di eventi. C'è un nome per questo 'modello'?
MustafaM,

@MadKeithV sei sicuro che questo sia il "metodo modello"?
MustafaM,

@illmath - sì, è un metodo pubblico non virtuale che delega parti dei suoi dettagli di implementazione a metodi virtuali protetti / privati.
Joris Timmermans,

@illmath Non ci avevo mai pensato prima come metodo di template ma credo che ne sia un esempio base. Ho appena trovato questo articolo che potresti voler leggere laddove l'autore crede che meriti il ​​suo nome: Idiom di interfaccia non virtuale
Gyan aka Gary Buyn,

1

Il problema con un'interfaccia è che non può avere un'implementazione predefinita, il che è una seccatura per proprietà banali e sconfigge DRY.

A mio avviso, le interfacce dovrebbero avere solo metodi puri, senza un'implementazione predefinita. Non infrange in alcun modo il principio DRY, perché le interfacce mostrano come accedere a qualche entità. Solo per riferimento, sto guardando la spiegazione DRY qui :
"Ogni pezzo di conoscenza deve avere una rappresentazione unica, inequivocabile e autorevole all'interno di un sistema".

D'altra parte, il SOLID ti dice che ogni classe dovrebbe avere un'interfaccia.

Il caso 3 è superiore al caso 1? È perché "incapsulamento migliore" o "accoppiamento più libero", o qualche altra ragione?

No, il caso 3 non è superiore al caso 1. Devi prendere una decisione. Se si desidera avere un'implementazione predefinita, farlo. Se vuoi un metodo puro, seguilo.

E se tra 3 mesi, Joe Coder crea un impiegato medico, ma si dimentica di chiamare increment_money_earned () in do_work ()?

Quindi Joe Coder dovrebbe ottenere ciò che merita per aver ignorato i test unitari falliti. Ha testato questa classe, vero? :)

Quale caso è il migliore per un progetto software che potrebbe avere 40.000 righe di codice?

Una taglia non va bene per tutti. È impossibile dire quale è meglio. Ci sono alcuni casi in cui uno si adatterebbe meglio dell'altro.

Forse dovresti imparare alcuni schemi di progettazione invece di provare a inventarne alcuni.


Mi sono appena reso conto che stai cercando un modello di progettazione dell'interfaccia non virtuale , perché è così che appare la tua classe case 3.


Grazie per il commento. Ho aggiornato il caso 3 per rendere più chiaro il mio intento.
MustafaM,

1
Ti devo -1 qui. Non c'è alcun motivo per dire che tutte le interfacce dovrebbero essere pure, o che tutte le classi dovrebbero ereditare da un'interfaccia.
DeadMG,

@DeadMG ISP
BЈовић,

@VJovic: c'è una grande differenza tra SOLID e "Tutto deve ereditare da un'interfaccia".
DeadMG,

"Una taglia non va bene per tutti" e "impara alcuni schemi di progettazione" sono corretti - il resto della tua risposta viola il tuo suggerimento che una taglia non vada bene per tutti.
Joris Timmermans,

0

Le interfacce possono avere implementazioni predefinite in C ++. Non si può dire che un'implementazione predefinita di una funzione non dipenda esclusivamente da altri membri virtuali (e argomenti), quindi non aumenta alcun tipo di accoppiamento.

Per il caso 2, DRY sostituisce qui. L'incapsulamento esiste per proteggere il tuo programma dal cambiamento, da diverse implementazioni, ma in questo caso non hai implementazioni diverse. Quindi incapsulamento YAGNI.

In effetti, le interfacce di runtime sono generalmente considerate inferiori ai loro equivalenti in fase di compilazione. Nel caso del tempo di compilazione, puoi avere sia il caso 1 che il caso 2 nello stesso pacchetto, per non parlare dei numerosi altri vantaggi. O anche in fase di esecuzione, puoi semplicemente fare Employee : public IEmployeelo stesso vantaggio in modo efficace. Esistono numerosi modi per affrontare tali cose.

Case 3: (best of both worlds?)

Similar to Case 1. However, imagine that (hypothetically)

Ho smesso di leggere. YAGNI. Il C ++ è ciò che è il C ++ e il comitato Standards non implementerà mai e poi un tale cambiamento, per ragioni eccellenti.


Dici "non hai implementazioni diverse". Ma faccio. Ho l'implementazione dell'infermiera di Employee, e potrei avere altre implementazioni in seguito (un dottore, un bidello, ecc.). Ho aggiornato il caso 3 per chiarire cosa intendevo dire.
MustafaM,

@illmath: Ma non hai altre implementazioni di get_name. Tutte le implementazioni proposte condivideranno la stessa implementazione di get_name. Inoltre, come ho detto, non c'è motivo di scegliere, puoi avere entrambi. Inoltre, il caso 3 è assolutamente inutile. Puoi ignorare i virtual non puri, quindi dimenticati di un design dove non puoi.
DeadMG,

Le interfacce non solo possono avere implementazioni predefinite in C ++, ma possono avere implementazioni predefinite ed essere comunque astratte! cioè void virtuale IMethod () = 0 {std :: cout << "Ni!" << std :: endl; }
Joris Timmermans,

@MadKeithV: non credo che tu possa definirli in linea, ma il punto è sempre lo stesso.
DeadMG,

@MadKeith: Come se Visual Studio fosse mai stata una rappresentazione particolarmente accurata dello standard C ++.
DeadMG,

0

Il caso 3 è superiore al caso 1? È perché "incapsulamento migliore" o "accoppiamento più libero", o qualche altra ragione?

Da quello che vedo nella tua implementazione, l'implementazione del tuo caso 3 richiede una classe astratta in grado di implementare metodi virtuali puri che possono essere successivamente modificati nella classe derivata. Il caso 3 sarebbe migliore in quanto la classe derivata può cambiare l'implementazione di do_work come e quando richiesto e tutte le istanze derivate appartengono sostanzialmente al tipo astratto di base.

Quale caso è il migliore per un progetto software che potrebbe avere 40.000 righe di codice.

Direi che dipende esclusivamente dalla progettazione dell'implementazione e dall'obiettivo che si desidera raggiungere. La classe astratta e le interfacce sono implementate in base al problema che deve essere risolto.

Modifica su domanda

E se tra 3 mesi, Joe Coder crea un impiegato medico, ma si dimentica di chiamare increment_money_earned () in do_work ()?

È possibile eseguire unit test per verificare se ciascuna classe conferma il comportamento previsto. Quindi, se vengono applicati test unitari adeguati, i bug possono essere prevenuti quando Joe Coder implementa la nuova classe.


0

L'uso delle interfacce rompe il DRY solo se ogni implementazione è una copia dell'altra. Puoi risolvere questo dilemma applicando sia l'interfaccia che l' ereditarietà, ma ci sono alcuni casi in cui potresti voler implementare la stessa interfaccia su un numero di classi, ma variare il comportamento in ciascuna delle classi, e questo rimarrà comunque al principio di SECCO. Il fatto che tu scelga di utilizzare uno dei 3 approcci che hai descritto dipende dalle scelte che devi fare per applicare la tecnica migliore per soddisfare una determinata situazione. D'altra parte, probabilmente scoprirai che nel tempo usi più interfacce e applichi l'ereditarietà solo dove desideri rimuovere la ripetizione. Questo non vuol dire che questo è l' unico motivo dell'ereditarietà, ma è meglio ridurre al minimo l'uso dell'ereditarietà per consentire di mantenere aperte le opzioni se si ritiene che il progetto debba cambiare in seguito e se si desidera ridurre al minimo l'impatto sulle classi discendenti dagli effetti che una modifica introdurrebbe in una classe genitore.

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.