Я работаю над кодом, который должен быть чрезвычайно гибким по своей природе, то есть его очень легко потом расширять и другие люди. Но сейчас я сталкиваюсь с проблемой, о которой я даже не знаю в принципе, как правильно решать:
У меня довольно сложный 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
будет осуществляться. Вот несколько способов, которые я уже пытался решить:
isConverged()
большой список аргументов (содержащий _foo
, _bar
, а также _fooBar
). Недостатки: большинство переменных, используемых в качестве аргументов, не будут использоваться в большинстве случаев, и если Algorithm
будет расширен другой переменной (или аналогичный алгоритм наследует ее и добавляет некоторые переменные), довольно большой код должен быть изменен. -> возможно, но безобразноisConverged()
Algorithm
сам (или указатель на него) в качестве аргумента. Проблема: круговая зависимость.isConverged()
как функция друга. Проблема (среди прочих): не может быть определена как функция-член различных ConvergenceChecker
s.AlgorithmData
имеющий Algorithm
в качестве друга, затем укажите AlgorithmData
в качестве аргумента функции. Так, как 2. но, возможно, обойти проблемы круговой зависимости. (Еще не проверял.)Я был бы рад услышать ваши решения по этому поводу (и проблемы, которые вы видите с 5.).
Дальнейшие заметки:
Algorithm
s в коде, наследующем друг от друга, и ConvergenceChecker
Конечно, в идеале, в идеальном случае, они должны работать без каких-либо изменений, даже если новые Algorithm
с поднимись. Не стесняйтесь комментировать это.На самом деле, ваше решение 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);
});
}
};
Может быть шаблон декоратора может помочь в упрощении (неизвестного) набора проверок сходимости. Таким образом, вы можете не зависеть от самого алгоритма от того, какие проверки могут выполняться, и вам не требуется контейнер для всех проверок.
Вы бы получили что-то вроде этого:
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