I am in a design dilemma. I have a set of data that can be interpreted in numerous ways, but I cannot really decide how finely grained should it be. To illustrate it with some simple code:
class Base {
Datamembers data;
virtual void doStuff() = 0;
};
class T1 : public Base {
void doStuff() {
// do T1 stuff
}
};
class T2 : public Base {
void doStuff() {
// do T2 stuff
}
};
class ...
// ---------------------------
class Master {
enum Role {
T1, T2 ...
};
Role r;
Datamembers data;
void doStuff() {
switch (r) {
case T1:
// do T1 stuff
return;
case T2:
// do T2 stuff
return;
case ...
}
}
};
In reality the number of responsibilities is much higher than 2, currently still in the two digit range, but may grow further. Also, it is not one, but several doStuff()
s at play.
In the first case, the data is encapsulated in a base class, and doStuff()
is a pure virtual, which each derived class implements. Each responsibility is encapsulated in its own, dedicated type.
In the second case doStuff()
is not a virtual function, and the responsibility is stored as an extra member in a Role, and the method switches the role and executes the appropriate code.
The two designs achieve the same goal in a different way, but what are the consequences?
Code maintenance:
The first approach seems cleaner, but even though usually clean implies maintainable, I feel it is kind of fragmented, which may end up reducing the ease of maintenance.
In the second approach all the code for the different responsibilities is crammed in the same place, which may sound dirty, but in many cases having it all in the same place may actually be easier, since all you have to do is scroll rather than jump around different files. But then again... it is still a big wall of code, not obvious in the example code above, but in practice it will be 4-5 digit lines of code. Although there is an easy remedy for that, just implement a function for each case that takes this
as a parameter to access the data and force inlining to eliminate the overhead of extra function calls (even if not virtual).
Performance:
In my case, I deal with massive trees of objects, where doStuff()
will be executed in tight loops, traversing that tree.
In approach one, the method is virtual, this means I get penalty for the indirection. Also, the code is spread across multiple types, which may end up fragmenting it in memory, reducing cache efficiency.
In the second approach the method is not virtual, and all code is essentially in a single function, which means it will be tighter in memory, and potentially more cache friendly. There is the switching penalty vs the indirection and fragmentation of the first approach.
Memory usage:
Although it is missing from the example code above, the hierarchy does have a constructor with plenty of arguments. Meaning that in the first approach each derived class will have its own constructor with the same parameters, redirecting those to the base class constructor. This, aside from tedious to implement for each type, also adds some code bloat. And there is the overhead of the v-tables. Both the extra constructors and v-tables are avoided in the second approach.
Obviously, I've made some considerations, and had this been a smaller undertaking, I'd do both and compare. But it is actually a lot of work and it is not really possible, so I'd appreciate some experienced opinions - are my considerations valid, am I missing something, is there another, better way to do it?
As it seems the scale of the problem isn't entirely clear, I will just summarize it. I am talking about an object tree, potentially tens of millions of nodes big, a responsibility hierarchy of currently 30 roles, expected to grow to over 100. Right now I have approximately about 200 lines of code per role, although the number varies. Parallelization is not an option, since node result depends on parent or children nodes results, so I am limited to a single thread traversing the tree, currently processing 200-300k nodes a second.