Требуются интенсивные расчеты, которые разделяются между двумя моделями поведения в сочетании стратегии. Где это должно быть сделано и проведено - PullRequest
1 голос
/ 03 марта 2020

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

[snippet1]

class BehaviourInterface
{
public:
    BehaviourInterface() {}
    virtual double func() = 0;
};

class Model
{
public:
    std::vector<std::shared_ptr<BehaviourInterface>> behaviours_;
};

class BehaviourA : public BehaviourInterface
{
public:
    BehaviourA(double a) : BehaviourInterface(), a_(a), c_(0) {}
    double func() { return a_; }
private:
    double a_;
};

class BehaviourB : public BehaviourInterface
{
public:
    BehaviourB(double b) : BehaviourInterface(), b_(b) {}
    double func() { return b_; }
private:
    double b_;
};

И затем я могу создать модель с двумя поведениями. В этом примере модель просто суммирует значения из каждого поведения.

[snippet2]

class SomeModel : public Model
{
public:
    SomeModel()
    {
        // Construct model with a behaviourA and behaviourB.
        behaviours_.push_back(std::shared_ptr<BehaviourInterface>(new BehaviourA(1))); 
        behaviours_.push_back(std::shared_ptr<BehaviourInterface>(new BehaviourB(2)));
    }

    double GetResult()
    {   
        // Sum the values from each behaviour.
        double result = 0;
        for (auto bItr = behaviours_.begin(); bItr != behaviours_.end(); ++bItr)
            result += (*bItr)->func();
        return result;
    }
}

int main(int argc, char** argv)
{
    SomeModel sm;
    double result = sm.GetResult();     // result = behaviourA + behaviourB = 1 + 2 = 3;
}

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

Я немного расширил это, чтобы позволить поведениям иметь доступ к модели, которой они принадлежат (введя другой интерфейс поведения, который содержит модель), который позволяет поведению быть возможность подключиться к другому поведению через модель, к которой относятся оба поведения. В этом отношении это не просто шаблон стратегии (поскольку поведения могут знать о других поведениях), однако он все же может быть достаточно обобщенным c, чтобы поведениям не нужно было знать детали реализации других поведений. Например,

Я представляю BehaviourWithModelKnowledgeInterface

[snippet3]

class BehaviourWithModelKnowledgeInterface : public BehaviourInterface
{
public:
    BehaviourWithModelKnowledgeInterface(Model& model) : model_(model) {}
protected:
    Model& model_;
}

И BehaviourA и BehaviourB получены из более нового интерфейса. ..

[snippet4]

class BehaviourA : public BehaviourWithModelKnowledgeInterface
{
public:
    BehaviourA(Model& model, double a) : BehaviourWithModelKnowledgeInterface(model), a_(a), c_(0) {}
    double func() { return a_; }
private:
    double a_;
};

class BehaviourB : public BehaviourWithModelKnowledgeInterface
{
public:
    BehaviourB(Model& model, double b) : BehaviourWithModelKnowledgeInterface(model), b_(b) {}
    double func() { return b_; }
private:
    double b_;
};

Это означает, что я могу изменить способ, которым я получаю результат от модели, если одно из поведений выполняет логику c, который Model::GetResult() использовал раньше.

например, я изменяю BehaviourA::func(), чтобы теперь добавить его значение со значением BehaviourB.

[snippet5]

class BehaviourA : public BehaviourWithModelKnowledgeInterface
{
public:
    BehaviourA(Model& model, double a) : BehaviourWithModelKnowledgeInterface(model), a_(a), c_(0) {}
    double func() 
    {
        // Get the value of behaviourB, and add to this behaviours value..
        return a_ + model_.behaviours_[1].func();
    }
private:
    double a_;
};

Тогда SomeModel::GetResult() становится ...

[snippet6]

class SomeModel : public Model
{
public:
    SomeModel()
    {
        // Construct model with a behaviourA and behaviourB.
        behaviours_.push_back(std::shared_ptr<BehaviourInterface>(new BehaviourA(1))); 
        behaviours_.push_back(std::shared_ptr<BehaviourInterface>(new BehaviourB(2)));
    }

    double GetResult()
    {   
        // Just get the result from behaviourA, as this will add BehaviourB as part of BehaviourA's implementation.
        double result = behaviours_[0].func();
    }
}

int main(int argc, char** argv)
{
    SomeModel sm;
    double result = sm.GetResult();     // result = behaviourA = 1 + behaviourB = 1 + 2 = 3
}

Так что BehaviourA теперь может быть только частью Model, который имеет BehaviourB. Не чистый шаблон стратегии (поскольку существует зависимость одного поведения от другого поведения), но это ограничение все еще в порядке, поскольку эти варианты поведения могут быть снова расширены, давая элементы гибкости шаблона стратегии, хотя и в ограниченном объеме по сравнению с исходным примером (TIL это называется связанной стратегией;)).

[snippet7]

class BehaviourAInterface : public BehaviourWithModelKnowledgeInterface
{
public:
    BehaviourAInterface(Model& model) : BehaviourWithModelKnowledgeInterface(model) {}
    virtual double funcA() {}
    double func() { return funcA(); }
}

class BehaviourBInterface : public BehaviourWithModelKnowledgeInterface
{
public:
    BehaviourBInterface(Model& model) : BehaviourWithModelKnowledgeInterface(model) {}
    virtual double funcA() {}
    double func() { return funcB(); }
}

И тогда реализации поведения становятся ...

[snippet8]

class BehaviourA : public BehaviourAInterface
{
public:
    BehaviourA(Model& model, double a) : BehaviourWithModelKnowledgeInterface(model), a_(a), c_(0) {}
    double funcA() { return a_; }
private:
    double a_;
};

class BehaviourB : public BehaviourBInterface
{
public:
    BehaviourB(Model& model, double b) : BehaviourWithModelKnowledgeInterface(model), b_(b) {}
    double funcB() { return b_; }
private:
    double b_;
};

И это означает, что я все еще могу использовать ситуацию, о которой BehaviourA знает о BehaviourB ( snippet5 & snippet6 ), но все же подробности реализации B неизвестны A.

, т.е.

class BehaviourA : public BehaviourAInterface
{
public:
    BehaviourA(Model& model, double a) : BehaviourAInterface(model), a_(a), c_(0) {}
    double funcA() { return a_ + model_.behaviours_[1].func(); }
private:
    double a_;
};

Все еще сохраняется, как и ранее для snippet5 и snippet6 .

Проблема

Проблема, с которой я столкнулся, заключается в том, что при определенных значениях BehaviourA и BehaviourB они используют общее вычисленное значение, и это значение тяжело для рассчитать, поэтому я хочу сделать это только один раз, но хочу, чтобы он использовался обоими поведения (потенциально). Я не хочу, чтобы это вычисленное значение было частью интерфейса поведения A или B, поскольку могут существовать другие поведения A или B, которые его не используют, и это также подразумевает, что оба поведения, возможно, должны реализовать его, поскольку они не могут полагаться на другое Имея его.

Для решения этой проблемы можно использовать несколько различных решений, но я не слишком уверен, какое из них правильное / лучшее для использования.

Решение 1

Модель имеет эту реализацию для расчета и содержит необязательный параметр, поэтому он рассчитывается только один раз.

class Model
{
public:
    double CalculateC() 
    { 
        if (c_)
            return *c_;
        c_ = SomeHeavyCalculation();        // c_ not set yet, so calculate it (heavy heavy calc).
        return c_;
    }
private:
    std::optional<double> c_;
}

Плюсы: BehaviourA или BehaviourB не должны его удерживать.

Ни BehaviourA, ни BehaviourB не нужно знать друг о друге (кивая в сторону развязанного паттерна стратегии) ​​

Любое поведение может его использовать.

Минусы: Не каждое поведение (или любое поведение) в некоторых реализациях) может даже потребовать этого.

Модель теперь несколько специализирована и теряет некоторое обобщение.

Модель может превратиться в некоторый объект состояния uber, содержащий все возможные значения. сифилисы, которые могут или не могут использоваться различными видами поведения. Грязный большой интерфейс потенциально.

Решение 2

Объект 'Model global state', который может содержать произвольные значения, которые могут заполнять некоторые поведения, а другие использовать.

class ModelState
{
public:
    double GetC() 
    { 
        if (c_)
            return *c_;
        c_ = SomeHeavyCalculation();        // c_ not set yet, so calculate it (heavy heavy calc).
        return c_;
    }
private:
    std::optional<double> c_;
}

То, что содержится в Model, и поведение может использовать его (или заполнить его, если его там нет)

class Model
{
public:
    ModelState& GetModelState() { return modelState_; }
private:
    ModelState modelState_;
}

Плюсы: Отделение Model от значения состояния Model остается обобщенным, и его поведение зависит от объекта ModelState, который он использовал. (когда создается экземпляр Model, он может определить, какой объект состояния он получает, основываясь на том, какое поведение используется).

Любое поведение может вызвать тяжелый cal c, поэтому порядок вызова поведения является agnosti c.

Минусы: Требуется некоторая логика c, чтобы определить, какие объекты состояния использовать. При создании экземпляров требуется больше сложности.

Некоторые объекты состояния могут перестать быть убер-объектами, содержащими множество вещей, которые могут или не могут использоваться различными видами поведения. Введем еще несколько поведений, которые используют значение модели «глобальное», и, возможно, мне придется представить другие объекты состояния для хранения значения этой модели «глобально».

Решение 3

Введите другое поведение, чтобы сделать this.

class BehaviourC : public BehaviourInterface
{
public:
    BehaviourC() : BehaviourInterface() {}
    double func() 
    { 
        if (c_)
            return *c_;
        c_ = SomeHeavyCalculation();        // c_ not set yet, so calculate it (heavy heavy calc).
        return c_;
    }
private:
    std::optional<double> c_;
};

Плюсы: Обобщает Model, не уменьшает гибкость шаблона стратегии больше, чем это уже есть при использовании дизайна, который поведения может «кое-что» знать о других видах поведения (опять же, не полностью чистый стратегический шаблон, но все же гибкий).

Минусы: насколько гранулярно мы хотим go с поведением, выполняющим операции, которые требуются для других поведений (хотя, если подумать, это будет похоже на правило трех рефакторинга). ... если два или более поведения требуют чего-то тяжелого для калибровки c, то «тяжелое для калибровки» 1187 * становится другим поведением).

Зависимость поведения может превратиться в минное поле ... внезапно чтобы использовать BehaviourA, нам нужно BehaviourC, BehaviourB BehaviourD et c ... в то время как я уже представил этот банк Существенная зависимость между поведением (связь), в настоящее время она довольно минимальна и хотела бы сохранить ее как можно меньше.

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

Решение 4

Каждое поведение вычисляет свою собственную ценность.

class BehaviourA : public BehaviourAInterface
{
public:
    BehaviourA(Model& model, double a) : BehaviourWithModelKnowledgeInterface(model), a_(a) {}
    double funcA() { return SomeHeavyCalculation() + a_; }
private:
    double a_;
};

class BehaviourB : public BehaviourBInterface
{
public:
    BehaviourB(Model& model, double b) : BehaviourWithModelKnowledgeInterface(model), b_(b) {}
    double funcB() { return SomeHeavyCalculation() + b_; }
private:
    double b_;
};

Плюсы: каждое поведение может быть несколько изолированным и не требует другого поведения, даже если другое поведение использует то же вычисленное значение. Увеличенная развязка.

Минусы: SomeHeavyCalculation() вычисление выполняется дважды каждым поведением. Это именно то, что я пытаюсь смягчить!

Этот cal c сам может хотеть быть реализованным по-другому (что фактически указывает на решение 3, являющееся лучшим решением).

Я не знаю что делать ????

Решение 1 Мне не нравится, так как я предпочитаю более обобщенный интерфейс модели и не хочу, чтобы он стал каким-то супер конкретным классом. Решение 2 Я думаю, что лучше, чем 1, однако страдает те же проблемы, которые могут возникнуть в решении 1 с состоянием становится Uber интерфейс. Это также означает большую головную боль с точки зрения обслуживания, поскольку должна существовать некоторая логика c или дизайн, который связан с поведением, чтобы использовать правильные объекты состояния с учетом поведения в модели. Связь теперь происходит не только между поведением, но и с соответствующими объектами состояния.

Решение 3 - это мое внутреннее представление о том, что должно быть сделано, но меня беспокоит то, что какой-то момент в будущем ... все Внезапно, чтобы использовать BehaviourA, мне нужен BehaviourC, чтобы использовать C Мне нужен D et c ... может возникнуть сильная связь, что затруднит построение определенных моделей без знания других стратегий.

Я действительно не знаю, какой из них использовать, или правильно ли я использую этот шаблон, чтобы использовать его полный потенциал ... или мне следует использовать другой шаблон (о котором я не знаю). Извиняюсь за длину вопроса, и я действительно надеюсь, что здесь не пропущено что-то очевидное.

Ответы [ 2 ]

3 голосов
/ 03 марта 2020

Наилучшим подходом является передача любых предварительно вычисленных значений поведениям в их конструкторе , чтобы у вас был следующий код вызова:

const double x = SomeHeavyCalculation();
const double y = SomethingElseWhichIsHeavy();

Behavior1 b1(..., x);
Behavior2 b2(..., y);
Behavior3 b3(..., x, y);

Таким образом, поведения все еще имеют один и тот же интерфейс и не зависят друг от друга:

b1.func();
b2.func();
b3.func();

Теперь вы можете обобщить это, разложив свое поведение на подэтапы и разделить эти подэтапы между всеми поведениями. Вы также можете смоделировать шаги как объекты / поведения, а не как необработанные значения, и кешировать результаты и т. Д. c.

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

2 голосов
/ 03 марта 2020

Я постараюсь ответить на основании комментариев, хотя вопрос немного сложнее. Вашему поведению BehaviorA и BehaviorB требуется доступ к какому-либо общему ресурсу, и этот ресурс также может слегка зависеть от модели, верно? Давайте сделаем это:

class BehaviourInterface
{
public:
  virtual ~BehaviourInterface() = default;
  virtual double func() = 0;
};

class ICommonResourcesGenerator
{
public:
  virtual ~ICommonResourcesGenerator() = default;
  virtual double gimmeMyresource() = 0;
};

class Gen1: public ICommonResourcesGenerator
{
public:
  ~Gen1() override = default;
  double gimmeMyresource() override {
    if (!needToCalculate()) return res_;
    calculate();
    return res_;
  }

private:
  bool needToCalculate() { return res_ < 0; }  // or whatever check you need to do
  void calculate() { /* calc */ }
  static double res_;
}

double Gen1::res_ = -1;

class BehaviourA : public BehaviourInterface
{
public:
  BehaviourA(double a, std::shared_ptr<ICommonResourcesGenerator> gen):
  a_(a), gen_(gen) {}
  double func() { return a_ + gen_->gimmeMyresource(); }
private:
  double a_;
  std::shared_ptr<ICommonResourcesGenerator> gen_;
};

class BehaviourB : public BehaviourInterface
{
public:
    BehaviourB(double b, std::shared_ptr<ICommonResourcesGenerator> gen):
    b_(b), gen_(gen) {}
    double func() { return b_ + gen_->gimmeMyresource(); }
private:
    double b_;
    std::shared_ptr<ICommonResourcesGenerator> gen_;
};

Затем ваша модель может инициализировать желаемое поведение с помощью соответствующих генераторов общих ресурсов:

class SomeModel
{
public:
  SomeModel() {
    behaviours_.push_back(std::shared_ptr<BehaviourInterface>(
      new BehaviourA(1, std::make_shared<Gen1>()))); 
    behaviours_.push_back(std::shared_ptr<BehaviourInterface>(
      new BehaviourB(2, std::make_shared<Gen1>())));
  }

private:
  std::vector<std::shared_ptr<BehaviourInterface> > behaviours_;
};

В качестве альтернативы, вы можете иметь double res_ в качестве нестатического c поле конкретного генератора, и оставьте std::shared_ptr<Gen> в вашем Model как личное поле, созданное один раз при создании модели. «Все зависит ...».

В любом случае, этот подход гарантирует, что оба поведения A и B смогут получить доступ к одному и тому же ресурсу, не вычисляя его более одного раза, и что тип ресурса неизвестен A и B, но определяется моделью.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...