Gestire diversi "plug-in" pur rimanendo equilibrato

1

I miei colleghi e io stiamo lottando con un problema di progettazione. Proverò a spiegare la situazione:

Abbiamo un numero di componenti diversi (chiamiamoli plugin), che prendono un input, fare qualcosa con esso e restituire un output. Ogni plugin richiede a input diversi (e diversi tipi di) e restituisce un altro numero (e diversi tipi di) uscite. Alcuni degli input e output si sovrappongono, il che significa che un output può essere calcolato da diversi plugin e uno l'input può essere richiesto da diversi plugin. Oltre a questo, ne abbiamo alcuni "meta-plugins" che combinano input o output simili di diversi plugin.

E abbiamo una classe che ha il compito di chiamare un plugin, chiamiamolo un gestore, ma non lo sa quale, solo che è un plugin. Sfortunatamente, gli input e gli output hanno quasi nulla a che fare con l'altro, e non può davvero essere trattato polimorfico.

Ciò che stiamo facendo in questo momento, è mettere l'in-e output in modo eterogeneo mappe, con chiave enum. Il gestore chiama PluginBase :: Evaluate (inputmap, outputmap) sull'oggetto plugin derivato e l'oggetto va a lavorare. Ma ora i plugin devono scorrere sulla mappa, vedere quale input e output può davvero lavorare con. Inoltre, ci sono molti casting coinvolti, come potresti immagina adesso.

Idealmente, i plugin avrebbero ciascuno un'interfaccia sana per il lavoro, ad es.

double PluginA::GetHam(double x);

std::string PluginB::GetSpam(int x, int y);

ma il gestore non sarebbe in grado di lavorare con loro.

Ho creato un esempio compilabile di quello che stiamo facendo in questo momento, ma attenzione, è esattamente orribile come pensi è.

Modifica: Forse questo è un modo migliore per pensarci: il gestore ha vari dati, i tuoi dati di input. Diciamo che vuoi un comportamento che, se richiesta eOutput :: HAM, ti dà tre volte eInput :: TWO. Implementa un plugin Thrice e dagli le necessarie mappe di input e output. Puoi cambiare il modo in cui ottieni da TWO (o qualsiasi altra combinazione di eInput) a HAM implementando il tuo plugin, ma non hai i dati di input per arrivare a HAM senza il gestore.

    
posta Psirus 02.11.2016 - 16:11
fonte

3 risposte

1

La prima cosa che mi viene in mente è che hai ripetuto il codice nei metodi Evaluate e GetInputs per gestire le richieste non supportate. Ciò suggerisce che è necessario un metodo CanSupport (o qualunque cosa si voglia chiamare) che restituisce semplicemente se un dato output può essere gestito. Questo potrebbe essere chiamato da uno di questi altri metodi, ma se lo esponi allora il primo passo è che il gestore trovi quali plugin sono rilevanti e in assenza di bug, gli altri metodi non dovrebbero mai ricevere richieste che non possono gestire.

Idealmente, ogni plugin produrrebbe esattamente un set di uscite ed elencerà tutti gli input richiesti e quali input sono opzionali. Poi, invece di un plugin che supporta 1 o 2 o tre set di output diversi, avresti uno o due o tre plug-in diversi (magari condividendo il codice sottostante).

Quindi essenzialmente prendi un set di output desiderato e fai un loop sui plug-in e ottieni il set di plug-in che supportano tali output. Ogni plugin fornisce i suoi parametri e tu vai da lì.

    
risposta data 02.11.2016 - 16:59
fonte
1

Unfortunately, the inputs and outputs have almost nothing to do with each other, and can't really be treated polymorphically.

La soluzione corretta qui è smettere di provare.

Considera questi due tipi di codice:

Output OverlyGenericApi(OverlyGenericInput in)
{
    // very very ugly code here that breaks encapsulation here on the
    // Input class, to extract the data
    auto specificInput = UglyCodeToGetSpecificInputFrom(in);
    auto fishes = CatchFishes(specificInput); // actual work internal API
    auto overlyGenericOutput(fishes);
    return overlyGenericOutput;
}

vs

auto fishes = CatchFishes(specificInput);

Nel primo esempio, stai scrivendo il codice boilerplate per creare un input generico, quindi chiama il plug-in, quindi un codice boilerplate per estrarre l'input dall'API generica, quindi esegui il lavoro, quindi lo racchiudono in un codice troppo generico, quindi restituirlo.

Nel secondo, si rinuncia al framework dei plugin troppo generico (e non è più necessario mantenerlo).

Il primo esempio tenta di colmare il divario tra le diverse operazioni e trattarle in modo generico. Il secondo, è un caso particolare.

What we are doing right now, is putting the in-and outputs in heterogeneous maps, keyed with an enum.

Considera:

class Fisher // not a plugin
{
    class Inputs { ... }; // not generic code, just something that applies to
                          // this plugin only

    Fishes catchFishes(Input i);
};

Se hai n tipi di plugin eterogenei, avrai n classi di input eterogenee.

The handler calls PluginBase::Evaluate(inputmap, outputmap) on the derived plugin object, and the object goes to work.

Invece, scrivi il codice che usa direttamente la classe di lavoro, in un modo non generico, quindi inseriscilo in un'API, quindi chiama quell'API (non è necessario alcun framework di plugin generico).

Se devi trattare i tuoi oggetti in modo generico (ad esempio, un requisito come "Ho bisogno di avere tutti i lavoratori in un registro e mostrarli all'utente"), crea invece un framework al di fuori delle tue classi di lavoro (all'esterno di SomePlugin e le sue classi di base) che fa ciò di cui hai bisogno:

class Registry { ... };

class RegistryEntry { ... };

void makeRegistryEntry(Fisher & f); // make this polymporphic by argument type.

But now the plugins needs to iterate over the map, see which of the in- and outputs it can actually work with. Also, there is a lot of casting involved, as you might imagine by now.

Il cast (specialmente quando ce n'è molto) è un sintomo di astrazioni non sufficienti / errate.

    
risposta data 04.11.2016 - 16:35
fonte
0

Stai facendo un sacco di controllo dinamico dei tipi senza che il compilatore ti aiuti. Il mio C ++ è arrugginito, quindi non prendi questo esattamente come sintatticamente corretto, ma con i modelli variadici di C ++ 11, dovresti essere in grado di semplificare il codice del tuo plugin fino a qualcosa del tipo seguente, a scapito di alcuni% di livello di barra del colloPluginBase e Handler code. Il compromesso è che ci sono in un posto centrale invece di ripetere il codice difficile da mantenere in ogni plugin.

class PluginA : public PluginBase<Input<ONE,THREE>,Output<SPAM>>
{
public:
    void Evaluate(Input<ONE,THREE>& in, Output<SPAM>& out) override
    {
        out.get<0> = "Hello " + in.get<0> + in.get<1>;
    }
};

La parte complicata è la conversione di una mappa dinamica come quella che hai ora in Input<ONE,THREE> in fase di runtime, ma quei tipi sono disponibili per te, e puoi scorrere in modo ricorsivo. Ci sono esempi su come farlo sparsi sul web.

    
risposta data 02.11.2016 - 22:55
fonte

Leggi altre domande sui tag