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:
- 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.
- 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.
- 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: -)