Flexible design despite highly dependent classes

I am working on code that should be extremely flexible in nature, especially very easy to extend later by other people. But now I am faced with a problem that I don’t even know in principle how to deal with:

I have a rather complicated Algorithm , which at some point should converge. But because of its complexity, there are several different criteria for checking convergence, and depending on the circumstances (or input) I would like to have different convergence criteria. It is also easy to create new convergence criteria without having to touch the algorithm itself. Therefore, ideally, I would like to have an abstract class ConvergenceChecker from which I can inherit, and let the algorithm have a vector of these, for example. eg:

 //Algorithm.h (with include guards of course) class Algorithm { //... vector<ConvergenceChecker*> _convChecker; } //Algorithm.cpp void runAlgorithm() { bool converged=false; while(true){ //Algorithm performs a cycle for (unsigned i=0; i<_convChecker.size(); i++) { // Check for convergence with each criterion converged=_convChecker[i]->isConverged(); // If this criterion is not satisfied, forget about the following ones if (!converged) { break; } } // If all are converged, break out of the while loop if (converged) { break; } } } 

The problem is that every ConvergenceChecker needs to know something about the current start of Algorithm , but each of them may need to know completely different things from the algorithm. Say the Algorithm changes _foo _bar and _fooBar during each cycle, but one of the possible ConvergenceChecker should know only _foo , another _foo and _bar , and maybe some day a ConvergenceChecker needing _fooBar . Here are some of the ways that I have already tried to solve:

  • Give the isConverged() function a list of large arguments (containing _foo , _bar and _fooBar ). Disadvantages: most of the variables used as arguments will not be used in most cases, and if the Algorithm extends to another variable (or a similar algorithm inherits from it and adds some variables), then some code should be changed, → perhaps, but ugly
  • Give the isConverged() function to the Algorithm itself (or a pointer to it) as an argument. Problem: circular dependence.
  • Declare isConverged() as a friend function. Problem (among other things): cannot be defined as a member function from different ConvergenceChecker s.
  • Use an array of function pointers. Does not solve the problem at all, and also: where to define them?
  • (Just came up with this when writing this question). Use another class that contains data, e.g. AlgorithmData , having Algorithm as a class other, then select AlgorithmData as a function argument. So, as 2. but perhaps circumvent the circular problems of addiction. (Not tested it yet.)

I would be happy to hear your decisions about this (and the problems you see with 5).

Further notes:

  • Question heading: I know that “highly dependent classes” already say that most likely someone is doing something very bad when developing the code, but I think that many people may run into this problem and would like to hear the possibilities to avoid this, so I would rather keep that ugly expression.
  • Too easy: In fact, the problem I presented here was incomplete. The code that inherits from each other will have many different Algorithm , and ConvergenceChecker should work perfectly in appropriate cases without any further modifications, even if a new Algorithm appears. Feel free to comment on this.
  • Question style: I hope this question is not too abstract and not too special, and I hope that it is not too long and clear. So please also feel free to comment on how I ask this question so that I can improve it.
+4
source share
4 answers

Actually, your solution 5 sounds good.

When there is a danger of introducing circular dependencies, the best way is, as a rule, to extract the part that is required and transfer it to a separate object; just like extracting the data used by the algorithm into a separate class / struct will do in your case!

+3
source

Another solution would be to pass your test to an object that provides the current state of the algorithm in response to parameter names expressed as string names. This allows you to separately compile your conversion strategies, since the interface of this callback interface remains the same even if you add additional parameters to your algorithm:

 struct AbstractAlgorithmState { virtual double getDoubleByName(const string& name) = 0; virtual int getIntByName(const string& name) = 0; }; struct ConvergenceChecker { virtual bool converged(const AbstractAlgorithmState& state) = 0; }; 

This is what all convergence check developers should see: they implement a checker and get state.

Now you can create a class that is closely related to your implementation of the algorithm for implementing AbstractAlgorithmState and get a parameter based on its name. However, this closely related class is private to your implementation: callers only see its interface, which never changes:

 class PrivateAlgorithmState : public AbstractAlgorithmState { private: const Algorithm &algorithm; public: PrivateAlgorithmState(const Algorithm &alg) : algorithm(alg) {} ... // Implement getters here } void runAlgorithm() { PrivateAlgorithmState state(*this); ... converged=_convChecker[i]->converged(state); } 
+1
source

Using a separate data / state structure seems simple enough - just pass it to the checker as a read-only constant link.

 class Algorithm { public: struct State { double foo_; double bar_; double foobar_; }; struct ConvergenceChecker { virtual ~ConvergenceChecker(); virtual bool isConverged(State const &) = 0; } void addChecker(std::unique_ptr<ConvergenceChecker>); private: std::vector<std::unique_ptr<ConvergenceChecker>> checkers_; State state_; bool isConverged() { const State& csr = state_; return std::all_of(checkers_.begin(), checkers_.end(), [csr](std::unique_ptr<ConvergenceChecker> &cc) { return cc->isConverged(csr); }); } }; 
0
source

Perhaps the decorator pattern will help simplify the (unknown) set of convergence checks. Thus, you can prevent the algorithm from being incompatible with which convergence checks may occur, and you do not need a container for all checks.

You would get something in these lines:

 class ConvergenceCheck { private: ConvergenceCheck *check; protected: ConvergenceCheck(ConvergenceCheck *check):check(check){} public: bool converged() const{ if(check && check->converged()) return true; return thisCheck(); } virtual bool thisCheck() const=0; virtual ~ConvergenceCheck(){ delete check; } }; struct Check1 : ConvergenceCheck { public: Check1(ConvergenceCheck* check):ConvergenceCheck(check) {} bool thisCheck() const{ /* whatever logic you like */ } }; 

You can then do arbitrary complex combinations of convergence checks by storing only one member of ConvergenceCheck* in the Algorithm . For example, if you want to check two criteria (implemented in Check1 and Check2 ):

 ConvergenceCheck *complex=new Check2(new Check1(nullptr)); 

The code is not complete, but you get the idea. In addition, if you are a fanatic of execution and are afraid of calling a virtual function ( thisCheck ), you can apply a curiously returning template for the template, eliminate this.


Here is a complete example of decorators for checking int constraints to give an idea of ​​how this works:

 #include <iostream> class Check { private: Check *check_; protected: Check(Check *check):check_(check){} public: bool check(int test) const{ if(check_ && !check_->check(test)) return false; return thisCheck(test); } virtual bool thisCheck(int test) const=0; virtual ~Check(){ delete check_; } }; class LessThan5 : public Check { public: LessThan5():Check(NULL){}; LessThan5(Check* check):Check(check) {}; bool thisCheck(int test) const{ return test < 5; } }; class MoreThan3 : public Check{ public: MoreThan3():Check(NULL){} MoreThan3(Check* check):Check(check) {} bool thisCheck(int test) const{ return test > 3; } }; int main(){ Check *morethan3 = new MoreThan3(); Check *lessthan5 = new LessThan5(); Check *both = new LessThan5(new MoreThan3()); std::cout << morethan3->check(3) << " " << morethan3->check(4) << " " << morethan3->check(5) << std::endl; std::cout << lessthan5->check(3) << " " << lessthan5->check(4) << " " << lessthan5->check(5) << std::endl; std::cout << both->check(3) << " " << both->check(4) << " " << both->check(5); } 

Output:

0 1 1

1 1 0

0 1 0

0
source

Source: https://habr.com/ru/post/1497956/


All Articles