1

My colleagues and I are struggling with a design problem. I'll try and explain the situation:

We have a number of different components (let's call them plugins), that take an input, do something with it, and give an output back. Each plugin takes a different number (and different types of) inputs, and gives back a different number (and different types of) outputs. Some of the inputs and outputs overlap, meaning that one output can be calculated by several plugins and one input may be needed by several plugins. On top of that, we have some "meta-plugins" that combine similar inputs or outputs of several plugins.

And we have a class that is tasked with calling a plugin, let's call that a handler, but it doesn't know which one, only that it is a plugin. Unfortunately, the inputs and outputs have almost nothing to do with each other, and can't really be treated polymorphically.

What we are doing right now, is putting the in-and outputs in heterogeneous maps, keyed with an enum. The handler calls PluginBase::Evaluate(inputmap, outputmap) on the derived plugin object, and the object goes to work. 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.

Ideally, the plugins would each have a sane interface for the work, e.g.

double PluginA::GetHam(double x);

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

but then the handler would not be able to work with them.

I've created a compileable example of what we're doing right now, but beware, it's exactly as horrible as you think it is.

Edit: Maybe this is a better way to think of it: The handler has various data, your input data. So let's say you want a behaviour that, if request eOutput::HAM, it gives you three times eInput::TWO. You implement a plugin Thrice, and give it the required in- and output maps. You can change how you get from TWO (or any other combination of eInputs) to HAM by implementing your own plugin, but you don't have the input data to get to HAM without the handler.

  • Maybe I'm missing something but you have a method InputMap GetInputs(OutputMap& outputs). Usually we call the stuff you pass into a method 'the input' and the stuff it returns 'the output'. This method declaration implies it takes outputs as input and returns inputs as output. I find this Jeopardy-style interface a little confusing. Can you elaborate on why it's like that? – JimmyJames Nov 2 '16 at 15:26
  • The handler has an Output Enum it wants to show using the plugin. But it doesn't know yet which inputs the plugin needs to calculate that output. Therefore, each plugin has a GetInputs(outputs) method to tell the handler it needs to allocate the particular inputs the plugin needs. – Psirus Nov 2 '16 at 15:28
  • OK, why doesn't the plugin just say: "here's what I need as inputs and here's what I provide as outputs" i.e. have two methods: getInputs() and getOutputs() Why is something else passing in outputs? – JimmyJames Nov 2 '16 at 15:34
  • What kind of data types your maps need to carry? – COME FROM Nov 2 '16 at 15:34
  • @JimmyJames Take for example PluginB. If you don't want output EGGS, you don't need input THREE. The maps carry all kinds of stuff, really. Mostly numbers, but also custom classes, enums, etc. – Psirus Nov 2 '16 at 15:37
1

The first thing that jumps out at me is that you have repeated code in the Evaluate and GetInputs methods to handle unsupported requests. That suggests you need a CanSupport (or whatever you want to call it) method that simply returns whether a given output can be handled. This could be called from either of those other methods but if you expose this then the first step is for the handler to find which plugins are relevant and in the absence of bugs, the other methods should never receive requests they can't handle.

Ideally, each plugin would produce exactly one set of outputs and list all the inputs required and what inputs are optional. Then instead of one plugin supporting 1 or 2 or three different sets of outputs, you'd have one or two or three different plugins (perhaps sharing underlying code.)

Then you essentially take a desired set of outputs and loop over the plugins and get the set of plugins that support those outputs. Each plugin provides it's parameters and you go from there.

  • I agree with the first paragraph, good idea. On the second part, it's not so much that some of the inputs are optional, it's that you need different inputs to calculate different outputs. And in general, the handler gets a plugin and works with it. It doesn't need to loop over plugins. Whether the output can be calculated the plugin will tell it. – Psirus Nov 2 '16 at 16:10
  • Right I understand that's where you are now but I'm suggesting that if you go to a more binary approach you get the same capability but the code is less complicated. I imagine you want to have a single plugin to be able to calculate different outputs because the underlying implementation for those various options is shared. You can still accomplish that in various ways e.g. nested classes and have each plugin support one thing. The reason it's helpful to do this is that you eliminate the need for switches or other conditionals in Evaluate and GetInputs and let polymorphism do that for you. – JimmyJames Nov 2 '16 at 16:28
  • The implementation is not shared. You basically say I want to have output HAM from the behaviour implemented by PluginX. So you sort of can choose what output you want, and by which behaviour to arrive at it, and the handler supplies the input data to get this output. – Psirus Nov 2 '16 at 18:04
  • I'm not clear what you are looking for here. You posted the question and a link to code and say it's horrible but when I try to give some advice to clean up the design, you don't seem to want to change it. You have a lot of switches calling code with switches. This is a pretty bad code smell. You can get rid of a lot of it in the way I just explained. Perhaps you should clarify what exactly you think is wrong with what you have in the question. – JimmyJames Nov 2 '16 at 18:40
  • I misunderstood. I thought you wanted to combine multiple plugins into one, while now I read it as e.g. splitting PluginB into PluginBHam and PluginBEggs. Is this what you meant? – Psirus Nov 2 '16 at 22:19
1

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

The correct solution here is to stop trying.

Consider these two types of code:

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);

In the first example, you are writing boilerplate code to create a generic input, then call the plugin, then more boilerplate code to extract the input from the generic API, then do the work, then wrap it in overly generic code, then return it.

In the second, you give up on the overly generic plugin framework (and no longer have to maintain it).

The first example attempts to bridge the gap between different operations and treat them in a generic way. The second, is a particular case.

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

Consider :

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

    Fishes catchFishes(Input i);
};

If you have n heterogenous plugin types, you will have n heterogenous Input classes.

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

Instead, write the code that uses the work class directly, in a non-generic way, then put it in an API, then call that API (no generic plugin framework needed).

If you need to treat your objects in a generic way (for example, a requirement like "I need to have all workers in a registry and show them to the user") , create instead a framework outside your work classes (outside of SomePlugin and it's base classes) that does what you need:

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.

Casting (especially when there's a lot of it) is a symptom of not enough / wrong abstractions.

0

You are doing a lot of dynamic type checking without letting the compiler help you. My C++ is rusty, so don't take this as exactly syntactically correct, but with C++11's variadic templates, you should be able to simplify your plugin code down to something like the following, at the expense of some neckbeard-level PluginBase and Handler code. The trade off is those are in one central place instead of repeating hard-to-maintain code in every 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>;
    }
};

The tricky part is converting a dynamic map like you have now to an Input<ONE,THREE> at runtime, but those types are available to you, and you can iterate through them recursively. There are examples of how to do so scattered throughout the web.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.