Sta usando l'ereditarietà protetta per nascondere (ma sfruttare) l'interfaccia implementata dal pubblico ok?

6

Recentemente, abbiamo discusso del codice usando l'ereditarietà protetta per nascondere il fatto (al codice client) che una classe eredita da una specifica classe base ma per sfruttare questo fatto nell'implementazione.

Il seguente codice lo dimostra. Si compila con le versioni recenti di GCC e clang ++ (utilizza le funzionalità di C ++ 11):

#include <vector>
#include <iostream>

class IObserver
{
public:
    virtual void update() = 0;
};

class Model
{
    std::vector<IObserver*> m_observers;
    int m_number = 0;
public:
    void addObserver(IObserver& observer) {
        m_observers.push_back(&observer);
    }
    void setNumber(int value) {
        m_number = value;
        notifyObservers();
    }
    int number() const {
        return m_number;
    }
protected:
    void notifyObservers() {
        for (auto pObserver : m_observers)
            pObserver->update();
    }
};

// We want to hide the fact class 'View' has 'IObserver' interface.
class View : protected IObserver
{
    Model* m_pModel;
public:
    View(Model& model) : m_pModel(&model) {
        model.addObserver(*this); // Exploit the fact we are an 'IObserver'.
    }
protected:
    void update() override {
        std::cout << m_pModel->number() << std::endl;
    }
};

int main(int argc, char *argv[])
{
    Model model;
    View view(model);

    //view.update(); // ERROR: 'update' is a protected member of 'View'.

    model.setNumber(1);
    model.setNumber(2);
}

La classe 'View' eredita l'interfaccia 'IObserver' ma usa il modificatore 'protected'. In modo corrispondente, il metodo pubblico "aggiornamento" ereditato da tale interfaccia è protetto. Nel costruttore, la classe 'Visualizza' si aggiunge come osservatore all'istanza 'Modello' passata come parametro. L'esecuzione dell'eseguibile genererà "1" e "2" in due righe separate, quindi il codice verrà eseguito come previsto.

Ora questa soluzione è stata discussa intensamente nel nostro team, ma alla fine ci sono state alcune domande a cui non è stato possibile rispondere con un accordo comune:

  1. Questo codice è effettivamente legale w.r.t. allo standard C ++? Il problema qui è che, nel costruttore, la classe 'View' passa un riferimento a se stesso al metodo 'Model :: addOberver' che si aspetta un riferimento a un'istanza 'IObserver' con un pubblico metodo 'aggiornamento'. Tuttavia, poiché la classe "View" utilizza l'ereditarietà protetta, questo metodo ora è protetto. Pertanto, quando il metodo 'Model :: notifyObservers' viene chiamato all'interno del metodo 'Model :: setNumber', il metodo 'update' della vista verrà chiamato "dall'esterno" nonostante sia effettivamente protetto. ( Domande frequenti su C ++ Lite afferma: "[eredità protetta ] consente alle classi derivate della classe derivata protetta di sfruttare la relazione con la classe di base protetta ". Quindi sembra che il codice sopra sia un caso d'uso corrispondente.
  2. Supposto che sia un codice legale, è anche un buon design? Il fatto che abbiamo discusso molto e non abbiamo raggiunto un accordo comune potrebbe essere un suggerimento che non lo è. Alcuni dei nostri colleghi erano dell'opinione che l'interfaccia di una classe fosse definita esclusivamente dai suoi metodi pubblici. E se la classe 'View' vuole passare un riferimento a se stessa a un metodo che si aspetta un'istanza 'IObserver' deve usare l'ereditarietà pubblica. Alcuni altri (incluso me stesso) non erano d'accordo con questa definizione rigorosa. L'implementazione sa che ha (e può fornire) un'interfaccia 'IObserver', quindi perché non dovrebbe sfruttare questa conoscenza ed esporre l'interfaccia protetta al codice che vuole? In C ++ siamo in grado di definire sia un'interfaccia pubblica per il codice client "normale" sia un'interfaccia protetta per il codice client che vuole specializzare / estendere la classe. Quindi da questo punto di vista, un'interfaccia di classe è definita dal suo pubblico e la sua interfaccia protetta. Dipende solo da quale tipo di codice client "guarda" un'interfaccia di classe.
  3. Se l'obiettivo è quello di nascondere l'interfaccia 'IObserver' dal pubblico, utilizza pImpl idiom solo per questo scopo ne vale la pena? Poiché non siamo stati in grado di giungere a una conclusione comune riguardo alle prime due domande, abbiamo deciso di" risolverle "utilizzando l'idioma pImpl e il pubblico eredità nella classe di implementazione. Quindi ora abbiamo una 'vista' di classe che non eredita affatto dalla classe 'IObserver'. Invece, una classe 'ViewImpl' ora eredita da 'IObserver' pubblicamente. Mentre abbiamo concordato che questo è un "buon design", ha reso la nostra implementazione più complessa, abbiamo una sola indiretta in più, e ora dobbiamo mantenere quasi il doppio del codice. Inoltre, abbiamo bisogno di mantenere le gerarchie di ereditarietà per entrambi, il pubblico e le classi di implementazione. (Naturalmente, questi sono i ben noti svantaggi derivanti dall'uso dell'idioma pImpl.)

Apprezzo molto le tue opinioni riguardo a queste tre domande !!! Grazie mille in anticipo: -)

    
posta Jonny Dee 12.08.2013 - 23:39
fonte

1 risposta

6
  1. Possiamo stabilire che è legale (cioè valido) C ++ perché compila. Lo hai usato su GCC e clang e ho fatto esattamente la stessa cosa con MSVC ++, quindi copre la maggior parte del mercato dei compilatori.

  2. Ecco perché è stato introdotto il modificatore protetto / privato (btw, vorrei usare privato). Permette alla tua classe di implementare le interfacce di callback che sono necessarie per le altre classi che usa internamente, ma a) il resto del mondo non ha bisogno di sapere che stai usando quelle altre classi eb) il resto del mondo non dovrebbe essere in grado di chiamare anche una qualsiasi di queste funzioni perché sono pensate per essere nascoste e utilizzate solo dagli interni.

    Quando ho passato da C ++ a C #, mi sentivo strano che non potessi avere ereditarietà dell'interfaccia privata. Se qualcosa è un dettaglio di implementazione e non è destinato a essere visto / utilizzato dal pubblico, dovrebbe essere tenuto nascosto e penso che il metodo che hai descritto sia un modo semplice e fantastico per farlo.

  3. L'uso di pImpl genera solo un sovraccarico. Quando si assegnano gli oggetti (ovviamente assumendo l'allocazione dinamica), si finisce per andare all'heap due volte quante volte. Riduci anche la località del tuo codice perché ora View e ViewImpl potrebbero trovarsi in pagine diverse in memoria e causeranno più errori di cache. E hai aumentato la quantità di codice della piastra della caldaia che devi scrivere perché ora stai inoltrando tutte le chiamate all'interfaccia pubblica dalla vista a ViewImpl.

Questa è solo la mia opinione, ma alla fine non riesco a pensare a nessuna ragione per non avere ereditarietà dell'interfaccia privata. Ho scritto codice come questo per oltre 6 anni e non ho mai avuto problemi di leggibilità / manutenibilità a causa di ciò.

    
risposta data 13.08.2013 - 04:04
fonte