Come hai notato, l'utilizzo di dynamic_cast
è abbastanza fragile e poco elegante.
Se some_method()
e some_better_method()
hanno la stessa interfaccia (ad esempio, soddisfa il principio di sottosistema di Liskov), non vi è alcun motivo particolare per cui dovresti usare metodi separati. Invece, sovrascrivere il metodo virtuale quando una migliore implementazione è disponibile sembrerebbe più desiderabile.
L'invio virtuale consente all'oggetto di destinazione di selezionare l'implementazione appropriata. Questo è uno dei punti principali di OOP. Usando un cast dinamico e quindi selezionando te stesso l'implementazione, rinunci alla maggior parte dei vantaggi del polimorfismo.
Come dici, i due metodi hanno firme incompatibili, anche se chiaramente devono fare qualcosa di simile. In molti casi, è possibile definire un'interfaccia che consenta di eseguire lo stesso comportamento ma senza menzionare tipi concreti. È quindi possibile utilizzare il modello dell'adattatore dell'oggetto per adattare le interfacce originali all'interfaccia comune. Questo è anche noto come tipo di cancellazione e ad es. è l'approccio utilizzato da std::function<…>
per adattare qualsiasi oggetto simile a funzione o puntatore a un'interfaccia comune.
Nel tuo esempio specifico, ciò significherebbe che foo_process_input()
non dovrebbe chiamare do_inefficient(a)
o do_efficient(b)
, ma adapter.do_whatever_the_adapter_decides()
. Avresti quindi un'interfaccia DoStuffAdapter
con un InefficientDoStuffAdapter
che può eseguire il wrapping di AInterface
istanze e un EfficientDoStuffAdapter
che include% istanze diBInterface
.
Il problema diventa quindi selezionando l'adattatore corretto per un dato oggetto. In un certo senso, questo semplicemente spinge il problema verso il basso di un livello, e sarebbe allettante usare di nuovo un cast dinamico. Invece:
-
Se il chiamante di foo()
conosce il tipo reale del tuo oggetto, può includerlo lì, ad es. foo(InefficientDoStuffAdapter(real_object))
o foo(EfficientDoStuffAdapter(real_object))
.
Questo è possibile ogni volta che è possibile un template<…> foo()
, poiché la soluzione del modello suggerita funzionerà solo se il tipo reale dell'oggetto di destinazione è staticamente noto al compilatore.
-
Se una dipendenza dalle istanze AInterface
agli adattatori è OK nel tuo modello di dominio, ogni istanza di AInterface
dovrebbe essere in grado di adattarsi all'interfaccia DoStuffAdapter
. Questo significherebbe un metodo come questo, che può essere sovrascritto se è disponibile un adattatore migliore:
virtual std::unique_ptr<DoStuffAdapter> adapt_to_do_stuff() const {
return make_unique<InefficientDoStuffAdapter>(*this);
}
-
Se tale dipendenza non è OK, almeno rendere AInterface
visitabile, in modo che il pattern visitor possa essere utilizzato per selezionare l'implementazione appropriata. Questo condivide alcuni problemi di un cast dinamico (il modello di visitatore è essenzialmente un cast dinamico "sicuro"). In particolare, è necessario conoscere tutti i tipi rilevanti in fase di progettazione e non è possibile aggiungere dinamicamente nuovi casi. Ma questo schema alla fine lascia la decisione di selezionare l'implementazione appropriata nella mano dell'oggetto target, non nelle mani del codice dipendente. Schema della soluzione:
struct Visitor;
struct AInterface {
virtual XXX some_method(/* some params */) = 0;
virtual void accept_visitor(Visitor& v) const;
};
struct BInterface : Ainterface {
virtual YYY some_better_method(/* some other params */) = 0;
virtual void accept_visitor(Visitor& v) const override;
};
struct Visitor {
virtual void visit_a(AInterface const& a) = 0;
virtual void visit_b(BInterface const& b) = 0;
};
void AInterface::accept_visitor(Visitor& v) const {
v.visit_a(*this);
}
void BInterface::accept_visitor(Visitor& v) const {
v.visit_b(*this);
}
Quindi l'adattatore corretto può essere selezionato come:
struct SelectAdapterVisitor : Visitor {
std::unique_ptr<DoStuffAdapter> adapter = nullptr;
void visit_a(AInterface const& a) {
adapter = make_unique<InefficientDoStuffAdapter>(a);
}
void visit_b(BInterface const& b) {
adapter = make_unique<EfficientDoStuffAdapter>(a);
}
};
std::unique_ptr<DoStuffAdapter> select_adapter(AInterface const& a_or_b) {
SelectAdapterVisitor v;
a_or_b.accept_visitor(v);
return std::move(v.adapter);
}
Alcuni problemi possono essere risolti sia con i modelli che con le tecniche di cancellazione dei tipi. Il tipo di cancellazione è leggermente non ovvio da implementare e scomodo da usare, quindi una semplice soluzione di template è solitamente molto migliore. Il suggerimento basato su modello nella tua domanda è una soluzione così semplice.
Se tuttavia la soluzione modello non può essere semplice, ad es. perché è necessaria una metaprogrammazione più complicata con SFINAE ecc., quindi un po 'di cancellazione del tipo è molto più facile da capire rispetto a molti modelli. Un altro scenario per preferire la cancellazione dei tipi è in un grande progetto con tempi di compilazione lunghi. Mentre i modelli richiedono che il codice di modello sia presente nei file di intestazione e pertanto devono ricompilare lo stesso codice sorgente più e più volte, le tecniche basate sul polimorfismo come la cancellazione dei tipi isolano le diverse unità di compilazione l'una dall'altra.