尽管存在强依赖类,但设计灵活

Flexible design despite strongly dependent classes

本文关键字:存在 依赖      更新时间:2023-10-16

我正在编写一个代码,它本质上需要非常灵活,也就是说,以后很容易被其他人扩展。但我现在面临着一个问题,我甚至不知道如何正确处理这个问题:

我有一个相当复杂的Algorithm,它在某个点应该收敛。但由于其复杂性,有几个不同的标准来检查收敛性,根据情况(或输入),我希望激活不同的收敛标准。此外,应该可以很容易地创建新的收敛标准,而不必接触算法本身。因此,理想情况下,我希望有一个抽象的ConvergenceChecker类,我可以从中继承,并让算法有一个向量,例如:

//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; }
  }
}

这样做的问题是,每个ConvergenceChecker都需要了解有关当前运行的Algorithm的一些信息,但每个CCD_4可能需要了解与算法完全不同的信息。假设Algorithm在每个周期中改变_foo_bar_fooBar,但一个可能的ConvergenceChecker只需要知道_foo,另一个_foo_bar,可能有一天会实现需要_fooBarConvergenceChecker。以下是我已经尝试过的解决方法:

  1. 给函数isConverged()一个大参数列表(包含_foo_bar_fooBar)。缺点:在大多数情况下,大多数用作参数的变量都不会被使用,如果Algorithm将由另一个变量扩展(或类似的算法从中继承并添加一些变量),则必须修改相当多的代码。->可能,但丑陋
  2. 将函数isConverged()本身(或指向它的指针)作为参数。问题:循环依赖关系
  3. isConverged()声明为友元函数。问题(以及其他问题):无法定义为不同ConvergenceChecker s的成员函数
  4. 使用函数指针数组。根本不能解决问题,而且:在哪里定义它们
  5. (刚在写这个问题时想到的)使用另一个保存数据的类,比如AlgorithmData,将Algorithm作为友元类,然后提供AlgorithmData作为函数参数。所以,就像2。但也许可以解决循环依赖问题。(尚未对此进行测试。)

我很高兴听到你对此的解决方案(以及你在5中看到的问题)

进一步说明:

  • 问题标题:我知道"强依赖类"已经表明,很可能有人在设计代码时做错了什么,但我想很多人最终可能会遇到这个问题,并希望听到避免它的可能性,所以我宁愿保留这个丑陋的表达
  • 太容易了?:事实上,我在这里提出的问题并不完整。代码中会有很多不同的Algorithm,它们相互继承,当然,即使出现新的AlgorithmConvergenceChecker也应该在适当的情况下工作,而不需要任何进一步的修改。欢迎对此发表评论
  • 提问风格:我希望这个问题不要太抽象也不要太特别,我希望它不要太长,可以理解。因此,请不要犹豫,对我提出这个问题的方式发表评论,以便我能够改进

实际上,您的解决方案5听起来不错。

当面临引入循环依赖关系的危险时,最好的补救措施通常是提取两者都需要的部分,并将其移动到一个单独的实体;就像将算法使用的数据提取到单独的类/结构中一样!

另一个解决方案是向检查器传递一个对象,该对象提供当前算法状态,以响应以字符串名称表示的参数名称。这使得单独编译转换策略成为可能,因为即使在算法中添加更多参数,这个"回调"接口的接口也保持不变:

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;
};

这就是收敛检查器的所有实现者需要看到的:他们实现检查器,并获得状态。

现在,您可以构建一个与算法实现紧密耦合的类来实现AbstractAlgorithmState,并根据其名称获取参数。这个紧密耦合的类对您的实现是私有的:调用者只看到它的接口,它永远不会改变:

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);
}

使用单独的数据/状态结构似乎很容易——只需将其作为只读访问的常量引用传递给检查器即可。

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);
                       });
  }
};

也许decorator模式可以帮助简化一组(未知)收敛检查。通过这种方式,您可以使算法本身与可能发生的收敛检查无关,并且不需要用于所有检查的容器。

你会得到这样的东西:

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 */ }
};

然后,您可以进行收敛检查的任意复杂组合,同时在Algorithm中只保留一个ConvergenceCheck*成员。例如,如果要检查两个条件(在Check1Check2中实现):

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

代码并不完整,但你已经明白了。此外,如果您是一个性能狂热者,并且害怕虚拟函数调用(thisCheck),则可以应用奇怪的返回模板模式来消除这种情况。


以下是一个完整的装饰器示例,用于检查int上的约束,以了解其工作原理:

#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);
    
}

输出:

0 1 1

1 1 0

0 1 0