Interface and Inheritance: il meglio di entrambi i mondi?

9

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

Il problema con un'interfaccia è che non può avere un'implementazione predefinita, il che è un problema per le proprietà banali e le sconfitte. Anche questo è positivo, perché mantiene l'implementazione e il sistema disaccoppiati. L'ereditarietà, sulla mano, mantiene un accoppiamento più stretto e ha il potenziale di rompere l'incapsulamento.

Caso 1 (Ereditarietà con membri privati, buon incapsulamento, strettamente accoppiati)

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 meglio dei due mondi?)

Simile a Caso 1 . Tuttavia, immagina che (ipoteticamente) C ++ non abbia permesso l'override dei metodi tranne quei metodi che sono pure virtual .

Quindi, in Caso 1 , l'override di do_work () causerebbe un errore in fase di compilazione. Per risolvere questo problema, impostiamo do_work () come puro virtuale e aggiungiamo un metodo separato increment_money_earned (). Ad 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 fra 3 mesi, Joe Coder crea un Dipendente Dottore, ma dimentica di chiamare increment_money_earned () in do_work ()?

La domanda:

  • È Caso 3 superiore a Caso 1 ? È dovuto al "migliore incapsulamento" o "più sciolto", o qualche altro motivo?

  • È Caso 3 superiore a Caso 2 perché è conforme a DRY?

posta MustafaM 17.02.2012 - 07:56
fonte

5 risposte

10

Un modo per risolvere il problema di dimenticare la call-the-superclasse è restituire il controllo alla superclasse! Ho re-jiggerato il tuo primo esempio per mostrare come (e reso compilativo;)). Oh, suppongo anche che do_work() in Employee avrebbe dovuto essere virtual nel tuo 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 sovrascritto. Se vuoi estenderlo devi farlo tramite on_do_work() su cui do_work() ha il controllo.

Questo, ovviamente, può essere utilizzato con l'interfaccia dal secondo esempio anche se Employee lo estende. Quindi, se ho capito bene, penso che questo Caso 3 lo faccia ma senza usare l'ipotetico C ++! È ASCIUTTO e ha un strong incapsulamento.

    
risposta data 17.02.2012 - 09:04
fonte
1

The problem with an interface is that it can't have a default implementation, which is a pain for mundane properties and defeats DRY.

Secondo la mia opinione, 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 ad alcune entità. Solo per i riferimenti, sto osservando la spiegazione di DRY qui :
 "Ogni pezzo di conoscenza deve avere una singola, non ambigua, autorevole rappresentazione all'interno di un sistema."

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

Is Case 3 superior to Case 1? Is it because it of 'better encapsulation' or 'more loosely coupled', or some other reason?

No, il caso 3 non è superiore al caso 1. Devi prendere una decisione. Se vuoi avere un'implementazione predefinita, fallo. Se vuoi un metodo puro, fallo con esso.

What if 3 months from now, Joe Coder creates a Doctor Employee, but he forgets to call increment_money_earned() in do_work()?

Quindi Joe Coder dovrebbe ottenere ciò che merita per aver ignorato i fallimenti dei test unitari. Ha fatto il test di questa classe, no? :)

Which Case is the best for a software project which might have 40,000 lines of code?

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

Forse dovresti imparare alcuni design pattern invece di provare a inventarne alcuni tuoi .

Ho appena realizzato che stai cercando interfaccia non virtuale modello di progettazione, perché questo è ciò che assomiglia alla classe 3 del tuo caso.

    
risposta data 17.02.2012 - 08:39
fonte
0

Le interfacce possono avere implementazioni predefinite in C ++. Non c'è nulla che dica che un'implementazione predefinita di una funzione non dipende esclusivamente da altri membri virtuali (e argomenti), quindi non aumenta alcun tipo di accoppiamento.

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

In effetti, le interfacce run-time sono generalmente considerate inferiori ai loro equivalenti in fase di compilazione. Nel caso in fase di compilazione, puoi avere entrambi caso 1 e caso 2 nello stesso pacchetto, per non parlare dei numerosi altri vantaggi. O anche in fase di esecuzione, puoi semplicemente fare Employee : public IEmployee per ottenere effettivamente lo stesso vantaggio. Esistono numerosi modi per gestire queste cose.

Case 3: (best of both worlds?)

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

Ho smesso di leggere. YAGNI. C ++ è ciò che C ++ è, e il comitato degli standard non ha mai e poi mai intenzione di implementare tale cambiamento, per ragioni eccellenti.

    
risposta data 17.02.2012 - 08:34
fonte
0

Is Case 3 superior to Case 1? Is it because it of 'better encapsulation' or 'more loosely coupled', or some other reason?

Da quanto vedo nella tua implementazione, la tua implementazione Case 3 richiede una classe astratta in grado di implementare metodi virtuali puri che possono successivamente essere modificati nella classe derivata. Caso 3 sarebbe migliore in quanto la classe derivata può modificare l'implementazione di do_work come e quando richiesto e tutte le istanze derivate sarebbero fondamentalmente di proprietà del tipo astratto di base.

Which Case is the best for a software project which might have 40,000 lines of code.

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

Modifica sulla domanda

What if 3 months from now, Joe Coder creates a Doctor Employee, but he forgets to call increment_money_earned() in do_work()?

È possibile eseguire test unitari per verificare se ciascuna classe conferma il comportamento previsto. Quindi, se vengono applicati i test unitari appropriati, è possibile prevenire i bug quando Joe Coder implementa la nuova classe.

    
risposta data 17.02.2012 - 08:26
fonte
0

L'uso delle interfacce interrompe ESSENZIALMENTE se ogni implementazione è duplicata l'una rispetto all'altra. Puoi risolvere questo dilemma applicando l'ereditarietà dell'interfaccia e , tuttavia ci sono alcuni casi in cui potresti voler implementare la stessa interfaccia su un certo numero di classi, ma variare il comportamento in ciascuna delle classi, e questo continuerà a rispettare il principio di ASCIUTTO. Indipendentemente dal fatto che tu scelga di utilizzare uno dei 3 approcci descritti, le scelte che devi fare per applicare la migliore tecnica per abbinare una determinata situazione. D'altra parte, probabilmente lo troverai nel tempo, usi di più le interfacce e applichi l'ereditarietà solo dove desideri rimuovere la ripetizione. Questo non vuol dire che questa sia la ragione dell'ereditarietà di solo , ma che sia meglio minimizzare l'uso dell'ereditarietà per consentire di mantenere aperte le opzioni se si scopre che il design deve essere modificato in seguito e se desideri minimizzare l'impatto sulle classi discendenti dagli effetti che una modifica introdurrebbe in una classe genitore.

    
risposta data 17.02.2012 - 12:59
fonte

Leggi altre domande sui tag