Il modo migliore per progettare un'interfaccia di classe passata a libreria / plugin

0

Ho un'applicazione comprendente un eseguibile bootstrapper, una libreria di base e diversi plugin (librerie condivise). La libreria principale è implicitamente collegata a tutti i componenti, i plugin vengono collegati / caricati esplicitamente dalla libreria principale. Quando vengono richiamate le funzioni dei plugin, passo diversi oggetti definiti nella libreria principale. Ora mi chiedo quale sia il modo migliore per progettare l'interfaccia di queste classi.

Ci sono tre modi in cui posso pensare.

  1. Progetta una classe semplice e passala. Questo è l'approccio attuale, ma l'interfaccia è disordinata perché il core ha bisogno di funzioni addizionali su questo oggetto, attualmente realizzate con relazioni di amici e modificatori di accesso privati in modo che i client / plug-in non possano accedervi. Penso che questo sia l'approccio peggiore dal momento che l'interfaccia client è davvero brutta.
  2. Crea un ABC (interfaccia) e aggiungi le funzionalità principali nella sottoclasse concreta e passa l'oggetto come ABC ai plug-in. Questo sembra buono, ma in qualche modo non sono sicuro che questo sia un buon design dal momento che da un lato le interfacce non sono realmente un concetto C ++ e c'è solo una classe che eredita l'ABC (mi sembra un odore per me) e d'altra parte il il compilatore si lamenta dei deboli vtables emessi in tutte le TU. (Ho provato ma non capisco questo)
  3. Crea una classe mediatore / facciata e nascondi l'interfaccia richiesta dalla libreria principale. La truffa in questo approccio è l'extraindirizzamento.

Sarebbe bello se qualcuno potesse integrare questa lista ed elaborare i pro e i contro degli approcci. Infine se è possibile apprezzerei una raccomandazione. Grazie.

    
posta ManuelSchneid3r 01.08.2017 - 14:39
fonte

1 risposta

1

Di questi tre, il numero 2 è la strada da percorrere: introdurre un'interfaccia. Le classi con funzioni virtuali pure sono un concetto C ++ legittimo. La lingua supporta le interfacce anche se non ha una sintassi speciale per loro.

Il problema con il tuo primo suggerimento (progettare una classe semplice) è che qualsiasi modifica alla dichiarazione della classe rompa la compatibilità binaria, quindi tutti i plugin dovrebbero essere ricompilati con la nuova versione. In particolare, aggiungere, rimuovere o riordinare i membri potrebbe rompere la compatibilità binaria, anche per i membri privati. Ci sono pochissime modifiche che puoi apportare tranquillamente a un file .h . È quindi importante mantenere le informazioni non necessarie fuori dalle intestazioni.

Una di queste tecniche consiste nell'utilizzare il polimorfismo: definire un'interfaccia che espone un insieme di metodi virtuali e un'implementazione che eredita dall'interfaccia. Poiché i client dipendono solo dall'interfaccia, eventuali modifiche all'implementazione sono sicure. Questa è fondamentalmente la soluzione numero 2 che hai delineato. Lo svantaggio è che gli oggetti polimorfici necessitano di semantica di riferimento: devi passarli per riferimento o per puntatore, e tali tipi non sono copiabili e non possono essere copiati.

Una soluzione simile è l'uso di tipi incompleti (linguaggio pImpl). In un file di intestazione, una classe viene dichiarata con vari metodi. Ha solo un singolo membro, che è un puntatore a un tipo incompleto (di solito con std::unique_ptr<…> ). In un file .cpp, il tipo incompleto è dichiarato e definito. Questa dichiarazione non è accessibile al di fuori del file cpp. Questo tipo privato contiene tutti i dettagli di implementazione, in particolare tutti i campi membri. Anche all'interno di questo file, deleghiamo tutti i metodi pubblici all'implementazione privata. Poiché il tipo privato è noto, tali tipi possono anche essere resi costruibili in copia e assegnabili in copia.

In entrambi i casi, abbiamo invertito le nostre dipendenze in modo che il codice client non dipenda dai dettagli di implementazione. Poiché l'interfaccia di solito sarà molto stabile, il codice del client dovrà essere ricompilato solo quando l'interfaccia cambia effettivamente.

                 +-----------+
           .---> | Interface | <---.
depends on |     +-----------+     | depends on
           |                       |
   +----------------+         +--------+
   | Implementation |         | Client |
   +----------------+         +--------+

Per il polimorfismo, l'interfaccia è una classe base astratta con metodi virtuali puri, l'implementazione di un'altra classe. Per pImpl, l'interfaccia è un file .h con un tipo incompleto e l'implementazione è un'unità di compilazione contenente la definizione del tipo incompleto.

In generale, l'uso dell'idioma pImpl è più semplice e flessibile dell'utilizzo dell'ereditarietà.

Ma per quanto riguarda il tuo obiettivo di rendere visibili più parti dell'implementazione al tuo codice interno rispetto ai client, un'interfaccia chiara sarà probabilmente meno confusa.

    
risposta data 06.08.2017 - 17:31
fonte

Leggi altre domande sui tag