Гибкий дизайн несмотря на сильно зависимые классы

Я работаю над кодом, который должен быть чрезвычайно гибким по своей природе, то есть его очень легко потом расширять и другие люди. Но сейчас я сталкиваюсь с проблемой, о которой я даже не знаю в принципе, как правильно решать:

У меня довольно сложный 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, но каждый может знать совершенно разные вещи из алгоритма. Скажи Algorithm изменения _foo _bar а также _fooBar в течение каждого цикла, но один возможный ConvergenceChecker нужно только знать _foo, другой _foo а также _barи, возможно, когда-нибудь ConvergenceChecker нуждаясь _fooBar будет осуществляться. Вот несколько способов, которые я уже пытался решить:

  1. Дать функцию isConverged() большой список аргументов (содержащий _foo, _bar, а также _fooBar). Недостатки: большинство переменных, используемых в качестве аргументов, не будут использоваться в большинстве случаев, и если Algorithm будет расширен другой переменной (или аналогичный алгоритм наследует ее и добавляет некоторые переменные), довольно большой код должен быть изменен. -> возможно, но безобразно
  2. Дать функцию isConverged() Algorithm сам (или указатель на него) в качестве аргумента. Проблема: круговая зависимость.
  3. декларировать isConverged() как функция друга. Проблема (среди прочих): не может быть определена как функция-член различных ConvergenceCheckers.
  4. Используйте массив указателей на функции. Совсем не решает проблему, а также: где их определить?
  5. (Просто придумал это, когда писал этот вопрос). Используйте другой класс, который содержит данные, скажем, AlgorithmData имеющий Algorithm в качестве друга, затем укажите AlgorithmData в качестве аргумента функции. Так, как 2. но, возможно, обойти проблемы круговой зависимости. (Еще не проверял.)

Я был бы рад услышать ваши решения по этому поводу (и проблемы, которые вы видите с 5.).

Дальнейшие заметки:

  • Заголовок вопроса: я знаю, что «сильно зависимые классы» уже говорят, что, скорее всего, кто-то делает что-то очень неправильное при разработке кода, тем не менее, я думаю, что многие люди могут столкнуться с такой проблемой и хотели бы услышать возможности избегайте этого, так что я бы предпочел сохранить это уродливое выражение.
  • Слишком просто? На самом деле проблема, которую я здесь представил, не была полной. Там будет много разных Algorithms в коде, наследующем друг от друга, и ConvergenceCheckerКонечно, в идеале, в идеальном случае, они должны работать без каких-либо изменений, даже если новые Algorithmс поднимись. Не стесняйтесь комментировать это.
  • Стиль вопроса: я надеюсь, что вопрос не слишком абстрактный и не слишком особенный, и я надеюсь, что он не стал слишком длинным и понятным. Поэтому, пожалуйста, также не стесняйтесь комментировать, как я задаю этот вопрос, чтобы я мог улучшить это.

4

Решение

На самом деле, ваше решение 5 звучит хорошо.

Когда существует опасность введения циклических зависимостей, лучшее средство обычно состоит в том, чтобы извлечь ту часть, которая как требуется, так и перенести ее в отдельную сущность; Точно так же, как извлечение данных, используемых алгоритмом, в отдельный класс / структуру в вашем случае!

3

Другие решения

Другим решением было бы передать вашему контролеру объект, который предоставляет текущее состояние алгоритма в ответ на имена параметров, выраженные в виде строковых имен. Это позволяет отдельно компилировать ваши стратегии преобразования, потому что интерфейс этого интерфейса «обратного вызова» остается прежним, даже если вы добавляете больше параметров в свой алгоритм:

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

Использование отдельной структуры данных / состояний кажется достаточно простым — просто передайте ее в средство проверки в качестве постоянной ссылки для доступа только для чтения.

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

Может быть шаблон декоратора может помочь в упрощении (неизвестного) набора проверок сходимости. Таким образом, вы можете не зависеть от самого алгоритма от того, какие проверки могут выполняться, и вам не требуется контейнер для всех проверок.

Вы бы получили что-то вроде этого:

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

Затем вы можете создавать произвольные сложные комбинации проверок сходимости, сохраняя только одну ConvergenceCheck* член в Algorithm, Например, если вы хотите проверить два критерия (реализовано в Check1 а также Check2):

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

0