Allow extension of class by injection of user-made subclass, while preserving accessibility

96 views Asked by At

I'd like to make a class Car extendable by allowing injection of a user-made subclass of Engine.

So one user might want a diesel car:

DieselEngine *de = new DieselEngine;
de->setGlowplugTemperature(1200); // something specific for a Diesel
car->setEngine(de);
car->drive();

While the other wants something else:

FluxCapacitorEngine *fce = new FluxCapacitorEngine;
fce->setDestinationYear(1985);
car->setEngine(fce);
car->drive();

Internally, Car calls (pure) virtual methods of its instance of Engine in order to do its business. The issue now is, if at a later time the user wishes to do some more configuring on the engine, he would either have to keep a pointer e.g. of type DieselEngine* externally in order to access it, or use a dynamic cast:

if (DieselEngine *de = dynamic_cast<DieselEngine*>(car->engine()))
  de->setMixRatio(2.1);

I don't find either variants particularily nice. Are there alternatives to achieve this kind of customizability/extendability?

A solution that I find lacking (current state): one could leave the implementation of the engine part inside Car, and make the user subclass the whole thing like class Delorean: public Car, so he could directly call the specific methods:

delorean->setDestinationYear(1985); // introduced method with the Delorean class
delorean->drive(); // inherited method of Car

However (and this is where the analogy becomes shaky, bear with me) I want to preserve the option of hot-swapping the engine while the Autobahn and the InsuranceCompany hold pointers to the car. This wouldn't be possible if we subclassed Car because we can't transform a car to a delorean instance without changing its pointer.

Another complication: The current implementation of my Car doesn't have this extensible, externally settable engine. Instead it's like in the previous paragraph: all the engine parts are in the Car implementation in the form how 80% of my users need their engine. The configuration setters of the engine part of the car are directly and easily accessible via the car's public interface.

So if I now switch to the external engine concept, I'll be upsetting 80% of my users who are happy with the default engine, because instead of

car->setSparkVoltage(1000);

they would then need to write

if (DefaultGasolineEngine *dge = dynamic_cast<DefaultGasolineEngine*>(car->engine()))
  dge->setSparkVoltage(1000);

ugh. They don't care that it's a DefaultGasolineEngine, they just want to set their familiar spark voltage.

In Summary: Are there alternatives to achieve this kind of customizability/extendability while maintaining a nice interface for the user to his custom class as well as to the default implementation which will be used by the majority of users?

1

There are 1 answers

0
Nicholas Smith On

I feel your pain on this. In my experience, your best option is to add support for a more dynamic way of setting properties: by strings. You can continue to support your existing Engine class interface, but add to it some generic property setters (and getters if you like) that will be implemented by each engine. The same property setters can be exposed by the Car class for convenience, which just call the same on its engine object.

////// base class declarations

class Engine
{
public:
    // your existing API here

    // support as many value types as you need
    virtual bool setProperty(const std::string & name, const std::string & value) = 0;
    virtual bool setProperty(const std::string & name, int value) = 0;
    virtual bool setProperty(const std::string & name, float value) = 0;
};

class Car
{
public:
    // your existing API here

    // optional for convenience... replicate from Engine API
    bool setEngineProperty(const std::string & name, const std::string & value);
    bool setEngineProperty(const std::string & name, int value);
    bool setEngineProperty(const std::string & name, float value);
};

////// DieselEngine implementation

// these should be declared in the DieselEngine as public static const, and defined here
const std::string DieselEngine::PROP_SPARK_VOLTAGE = "SparkVoltage";
const std::string DieselEngine::PROP_MIX_RATIO = "MixRatio";

bool DieselEngine::setProperty(const std::string & name, const std::string & value)
{
    // definitely do some input validation first!

    if (name == PROP_SPARK_VOLTAGE)
    {
        this->setSparkVoltage(atol(value.c_str()));
        return true;
    }
    else if (name == PROP_MIX_RATIO)
    {
        this->setMixRatio(atof(value.c_str()));
        return true;
    }

    return false;
}


////// Car implementation

bool Car::setEngineProperty(const std::string & name, const std::string & value)
{
    return this->engine->setProperty(name, value);
}


////// Example usage

Car car;
car.setEngine(new DieselEngine());
car.setEngineProperty(DieselEngine::PROP_SPARK_VOLTAGE, "1000");
// OR
car.getEngine().setProperty(DieselEngine::PROP_SPARK_VOLTAGE, "1000");

Another added benefit of this approach is that you can easily begin configuring your engines from a configuration file, since you now support loading properties from human readable strings. A simple name / value pair or JSON file can be loaded in and the property setters can be called with the values.

The reason I am returning bool from the setters is so that you can know if the property was recognized. You should obviously do some input validation on the value itself as well. You could return an int error code instead to indicate various types of errors, or use exceptions if you prefer.

It would also be acceptable to put the property name fields (e.g. DieselEngine::PROP_SPARK_VOLTAGE) in a single namespace (e.g. EngineProps::SPARK_VOLTAGE) and then clearly document which engine types support which engine properties. In that way, your example code would look like:

////// Example usage

Car car;
car.setEngine(new DieselEngine());
car.setEngineProperty(EngineProps::SPARK_VOLTAGE, "1000");
// OR
car.getEngine().setProperty(EngineProps::SPARK_VOLTAGE, "1000");