So, I have a class hierarchy that looks like this:
It is an embedded project, so the entire code will be run over and over.
I have several algorithm classes, that compute some result given some inputs. The results are written to some structs (that contain the necessary members).
class Algorithm1 { Algo1Output out; };
class Algorithm2 { Algo2Output out; };
class Algorithm3 { Algo3Output out; };
These algorithm classes are orchestrated by an Algorithms
class. This class gathers the outputs of the algorithms in an aggregated output (that is just references to the individual outputs).
class Algorithms {
Algorithm1 a1{};
Algorithm2 a2{};
Algorithm3 a3{};
AlgosOutput out{ a1.out, a2.out, a3.out };
};
class AlgosOutput {
Algo1Output& a1out;
Algo2Output& a2out;
Algo3Output& a3out;
};
The Algorithms
class is in turn managed by a Manager
class, that also manages a state machine and has an output of its own that is just a reference to the outputs of Algorithms
and StateMachine
:
class Manager {
StateMachine sm;
Algorithms a;
ManagerOutput out{ sm.out, a.out };
};
class StateMachine {
SmOutput out;
};
All of this hierarchy gets its inputs from a common source, that is simply passed down at initialization. The constructors basically all look almost the same:
Manager(InputData& inData) : sm(inData), a(inData) {}
StateMachine(InputData& inData) : inData(inData) {} // holds a reference to inData for actual use
Algorithms(InputData& inData) : a1(inData), a2(inData), a3(inData) {}
Algorithm1(InputData& inData) : inData(inData) {}
Algorithm2(InputData& inData) : inData(inData) {}
Algorithm3(InputData& inData) : inData(inData) {}
So all the components have a reference to inData
(read-only access, but I tried to keep code fragments brief), and inData
is updated with new info every run cycle. And each class in the hierarchy has access to the output of the classes immediately below it (which in turn contain the outptus of the classes immediately below them etc.).
When execution is done, the state machine and the algorithms are all executed using inData
. After execution, according to the new state that StateMachine
is in, the result of one of the 3 algorithm classes will be used. This logic is in Manager
:
void Algorithms::execute() {
a1.execute();
a2.execute();
a3.execute();
}
void Manager::execute() {
sm.execute();
a.execute();
switch (out.stateMachineOut.currentState) {
// select one of the 3 algo results for further use
}
}
Problem is that now, Algorithm2
needs the current state of the state machine to function. So I need a way to pass along that info. Ideally an elegant way that does not involve a complex design pattern like Mediator.
Option 1: One way would be to basically make inData
non-const in the state machine and add a member to that, since everyone has a reference to inData
, but inData
only contains input data from outside this system so I have a feeling it would be bad practice to pollute it with temporary values that pertain to the internals of the system.
Option 2: Another way would be to have Algorithms
also get (as a ctor argument) a reference to sm.out
and pass that along to Algorhithm 1/2/3
so now the algorithm classes maintain a reference to sm.out
and have instant read-only access to the output of state machine. However because of how C++ initialization works, this would mean that inside Manager
, StateMachine
should always remain declared above Algorithms
so it is initilized before Algorithms
, meaning I would have to put a big comment with "LEAVE THE DECLARATION OF STATE MACHINE ABOVE ALGORITHMS" and hope other developers bother to read it.
Option 3: And another way would be to pass the current state of StateMachine
as an argument to Algorithms::execute()
(which will in turn pass it to the 3 algorithm classes). This method also gives me a feeling that itmay be bad practice since all classes basically only use inData
as input, and if I were to add one single argument to them, the situation of the Algorithm
classes would change to the input being split, with about 95% of the input values inside inData
and one input value being passed as an argument.
Which way would be the "best practice" here and why? Is there something I did not think of?
My gut says to go with option 2, but that would create a new piece of coupling between StateMachine
and Algorithms
, and quite a big piece of coupling at that. Or is a degree of coupling inevitable since I basically need to bring the output of StateMachine
into Algorithms
anyways?
1 Answer 1
You wrote
Option 3: [..] This method also gives me a feeling that itmay be bad practice since all classes basically only use inData as input, and if I were to add one single argument to them, the situation of the Algorithm classes would change to the input being split, with about 95% of the input values inside inData and one input value being passed as an argument.
Well, based on how you described your requirements, this is exactly what you want to happen: Algorithm2
taking 95% of it's input values from inData
(which is available at the time of construction), plus 5% - some information from the state machine - at the time when it is available - after execution of algo 1.
So if that is what you want to happen, a clean implementation will reflect exactly that. "Bad practice" would be to hide that asymmetry behind a symmetrically looking design, just for the sake of keeping things symmetric. But if you have asymmetric requirements, there is nothing wrong in designing an asymmetric API for it.
Said that, there are different options how one might implement the parameter passing. Passing the state machine or its output through the execute method is one option. But it also possible to pass all input for each algorithm in one place. For example, one can implement the algorithm variables a1, a2, a3 as unique_ptr
members, so construction can be deferred to the point in time where all required data is available, like
void Algorithms::execute(InputData& inData, StateMachine &sm) {
a1 = unique_ptr<Algorithm1>(new Algorithm1(inData));
a1->execute();
a2 = unique_ptr<Algorithm2>(new Algorithm2(inData,sm))
a2->execute();
a3 = unique_ptr<Algorithm3>(new Algorithm3(inData));
a3->execute();
}
Or, you design the constructors without any parameters and pass the whole input through the execute methods:
void Algorithms::execute(InputData& inData, StateMachine &sm) {
a1.execute(inData);
a2.execute(inData,sm);
a3.execute(inData);
}
Which one you prefer may be just a matter of taste, or it may depend on some things in your real code not mentioned in your question, which is hard to tell without seeing "the real thing".
-
1"Bad practice" would be to hide that asymmetry behind a symmetrically looking design, just for the sake of keeping things symmetric. --- that statement is the key thing to realize. Hiding that 5% can lead to bugs or behavior that become real head scratchers months after you make this design decision.Greg Burghardt– Greg Burghardt2025年03月07日 14:58:45 +00:00Commented Mar 7 at 14:58
execute()
), then I would actually prefer option 3, except that I'd only pass the state (a copy of it) to algorithm 2. This communicates that this specific algorithm requires additional, just-in-time information in order to run. If that's not feasible, then I'd consider other options.inData
. I'd be more inclined towards algorithm-specific inputs with less generic type names, extracted frominData
by, say, your Manager class. Think of ctor params as configuring the object in general, and of method params as pertaining to a particular call, then decide which inputs to pass to the ctor vs theexecute
method. But I don't know enough about the project, so maybe there's good justification for the way it's currently designed.