I'm working on a piece of software which generates configuration data for certain hardware and currently needs to be adapted each time the hardware is released in a new version by an external company (this happens around twice a year). My current task is to introduce a plugin interface so the whole application doesn't have to be recompiled and re-released each time.
Currently, injecting new functionality - or replacing default functionality by adapted one - is already possible at runtime. However, there is one central enum
which has all the different versions in it and prevents large parts of code from being separated from the executable.
The enum looks somewhat like this:
enum class HardwareVersion
{
Stage1_Base, // corresponds to version 5.11.37
Stage1_Extensions, // corresponds to version 5.12.01
Stage2_Base, // corresponds to version 8.00.00
Stage2_ABC_Extensions, // corresponds to version 8.03.20
Stage2_DEF_Extensions // corresponds to version 9.01.00
}
Note: The names like "Stage 1 Base" are terms which are usually used when discussing with project management or the customer, hence they were used in code as well.
The users of the software need to view and modifiy data for every possible stage and generate the respective configuration data for the hardware version of interest (They are using various hardware versions simultaneously). Because of this, the software has several code parts similar to the following (theoretical code so you get the idea):
// This would be in a plugin
void addContent( HardwareVersion currentStage ) /* override */
{
if (currentStage < Stage2_Base) return;
// add stage 2 content
...
}
and
// This would be in a plugin as well
void initialize() /* override */
{
_someFactory->register( HardwareVersion::Stage2_Base, new Stage2SubcontrolFactory() );
}
// This would be somewhere in the core application
std::shared_ptr<Subcontrol> createSubcontrol( HardwareVersion currentStage )
{
return getFactory( currentStage )->createSubcontrol();
}
Now we could simply replace the enum
by a typedef int
and lose all type safety etc. We could also use GUIDs and lose all readability and make debugging really hard.
Both solutions feel really bad. This (finally) leads to the question:
What should you replace an enumeration with if values are to be provided by plugins?
[Edit:] To make this less subjective: The two major critera for solutions are high maintainability (this kinda implies readability for me) and high stability when adding new plugins.
-
Is it possible for you to create a central, managed registry for the codes? For instance, Ethernet addresses have to be registered by vendors with an assigned numbers authority. So the number codes are bound early in the life cycle.BobDalgleish– BobDalgleish09/13/2016 16:49:33Commented Sep 13, 2016 at 16:49
-
I don't think the enum could be split up.Gohan– Gohan09/13/2016 17:05:42Commented Sep 13, 2016 at 17:05
-
Is it possible using xml/json file to configure the HardwareVersion, to make the version_id unique? in your plugin code, you can generate .h file from xml/json, which can make your code readability?Gohan– Gohan09/13/2016 17:11:27Commented Sep 13, 2016 at 17:11
-
Who owns the list of enums, and how does it get extended?Erik Eidt– Erik Eidt09/13/2016 21:26:09Commented Sep 13, 2016 at 21:26
-
@ErikEidt The values are currently owned by the executable. In future, the executable shall only use, but not define the list, and the list will maybe have to be assembled dynamicallyTim Meyer– Tim Meyer09/14/2016 06:36:09Commented Sep 14, 2016 at 6:36
2 Answers 2
After sleeping over the question for a night and thinking about the various comments so far, I found the following solution to fit best for our needs:
Create a HardwareVersion
C-style POD
(*) struct like this:
struct IMPORT_EXPORT HardwareVersion
{
public:
/// The first part of the version
unsigned int Major;
/// The second part of the version
unsigned int Minor;
/// The third part of the version
unsigned int Release;
/// A readable name mainly used for logging and debugging
char* ReadableName;
}
Additionally, A Cpp Wrapper with functions like comparing, hash support, version tuple string ("XX.YY.ZZ"
where XX = Major, YY = Minor and ZZ = Release; Note that by definition, none of the version parts can exceed 99 in this case) will be added, but not exposed through the plugin interface.
This has the following advantages:
- The executable can be implemented to not allow plugins which provide
HardwareVersion
objects which are equal to already known ones OR it can be implemented to always use the newest one, so patches can be realized through plugins as well (This will most likely not be decided by me). - The plugins can specify which plugins they depend on through the version number (e.g. if
Stage2_ABC_Extensions
has a factory which provides a subclass of an object ofStage2_Base
, the plugins could be initialized according to their dependency chain). - Code which shall be executed only above or below a certain stage is still readable
- The application can be extended by only adding a plugin, without additional configuration required in the user environment
(*) POD to increase chances of being able to write a plugin with a different compiler (though I am aware this is not guaranteed)
-
2You can avoid overloading all of those operators by
#include
ing the<utility>
header. The Utility header includes generic overloads foroperator!=
,operator>
,operator>=
andoperator<=
. :-)Karl Nicoll– Karl Nicoll09/14/2016 08:58:41Commented Sep 14, 2016 at 8:58 -
@KarlNicoll As I'm planning to expose the class through a C Style interface: Will including
utility
break this somehow?Tim Meyer– Tim Meyer09/15/2016 09:10:18Commented Sep 15, 2016 at 9:10 -
1Yes is probably will, but C doesn't support operator overloading, so you'll need to extract the operators methods as non-member functions anyway :)Karl Nicoll– Karl Nicoll09/15/2016 09:13:47Commented Sep 15, 2016 at 9:13
-
@KarlNicoll Thanks Guess I'm gonna make use of
<uitlity>
in the CppWrapper I've created anyway (edited the answer a couple of minutes ago) and move the compare functions there (version comparing is done in C++ code anyway).Tim Meyer– Tim Meyer09/15/2016 09:18:43Commented Sep 15, 2016 at 9:18
I would say that rather than use an actual enumeration, it would be advisable to have each plugin use a set of offsets from some base value. The base value for each plugin can be supplied when the plugin is loaded.
This makes it fairly easy to maintain a unique set of values across the entire system, while still allowing each plugin to be developed in isolation, without any development-time dependency.
Explore related questions
See similar questions with these tags.