Gestione di un'interfaccia più specializzata senza duplicazione del codice

1

Ho il seguente codice:

struct AInterface {
    XXX some_method(/* some params */) = 0;
};

void foo(const AInterface & a) {
    do_work();

    while (x) {
        do_work_2();
        foo_process_input(a);
    }

    return some_value();
}

void foo_process_input(const AInterface & a) {
    do_inefficient(a);
}

Tuttavia, recentemente mi sono reso conto che alcuni oggetti che implementano AInterface stanno effettivamente seguendo un'interfaccia più specializzata, quindi potrei fare:

struct BInterface : AInterface {
     YYY some_better_method(/* some other params */) = 0;
};

void foo_process_input(const BInterface & b) {
    do_efficient(b);
}

per migliorare le prestazioni del mio codice in alcuni casi. Nota: le firme di some_method e some_better_method non consentono l'utilizzo l'una sull'altra.

Tuttavia, mi chiedo come dovrei modificare foo per poter accogliere questo cambiamento ed evitare di duplicare il codice. Come posso vedere, sembra che ho due opzioni:

  • Templatizza foo per accettare entrambe le interfacce
  • Fai affidamento su dynamic_cast
template <typename Interface>
void foo(const Interface & a_or_b) {
    do_work();

    while (x) {
        do_work_2();
        foo_process_input(a_or_b);
    }

    return some_value();
}

La soluzione templata richiede meno modifiche, ma ha lo svantaggio che il compilatore sta per duplicare tutto il codice do_work che in realtà non dipende dall'input. Inoltre, questo fa errori più brutti, dato che il tipo effettivo di a_or_b è controllato solo chiamando foo_process_input , che essendo un interno di foo potrebbe confondere l'utente.

void foo(const AInterface & a_or_b) {
    do_work();

    auto * binterface = dynamic_cast<BInterface*>(&a_or_b);

    while (x) {
        do_work_2();
        if (binterface) foo_process_input(*binterface);
        else foo_process_input(a_or_b);
    }

    return some_value();
}

La soluzione dynamic_cast dovrebbe funzionare e non costare troppo in termini di prestazioni, ma sono sempre stato diffidente nei confronti del controllo dei tipi di runtime, e mi chiedo se potrei in qualche modo migliorare la definizione dell'interfaccia.

Hai qualche suggerimento su come affrontare questo problema?

    
posta Svalorzen 08.04.2017 - 21:21
fonte

1 risposta

3

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.

    
risposta data 08.04.2017 - 23:04
fonte

Leggi altre domande sui tag