mardi 3 mars 2020

Heavy calculation required and shared between two behaviours in coupled strategy pattern. Where should it be done and held

I am using strategy pattern to have a calculation model which can have different behaviours.

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

And I can then create a model with two behaviours. In this example, the model simply sums the values from each behaviour.

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

Works nicely, allows me to implement different behaviours, each behaviour is sandboxed from all other behaviours (decoupled strategy).

I extended this slightly to allow the behaviours have access to the model they belong (by introducing another behaviour interface which holds the model), which allows a behaviour to be able to hook into another behaviour via the model that both behaviours belong. In this regard it's not pure strategy patten (since behaviours can know about other behaviours), however it can still be generic enough so that behaviours do not need to know implementation details of other behaviours. e.g.

I introduce BehaviourWithModelKnowledgeInterface

[snippet3]

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

And BehaviourA and BehaviourB derive from the newer interface...

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

This means I can change the way I get the result from the model by having one of the behaviours perform the logic that Model::GetResult() used to do.

e.g. I change BehaviourA::func() to now add its value with BehaviourB's value.

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

Then SomeModel::GetResult() becomes...

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

So BehaviourA can now only be part of a Model which has a BehaviourB. Not pure strategy pattern (as there is dependency of one behaviour to another behaviour), but this restriction is still ok as these behaviours can be extended again giving elements of the flexability of strategy pattern, albeit in limited capcity compared to the original example (TIL this is called coupled strategy ;)).

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

And then the behaviour implementations become...

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

And this means I can still use the situation that BehaviourA knows about BehaviourB (snippet5 & snippet6), yet still the implementation details of B are unknown by A.

i.e.

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

Still holds, same as before for snippet5 and snippet6.

The problem

The problem I have is that, with certain BehaviourA's and BehaviourB's, they use a common calculated value and this value is heavy to calculate so I only want to do it once but want it used by both behaviours (potentially). I do not want this calculated value to part of the behaviourA or B interface as there can be other behaviourA's or B's that do not use it, and it also implies that both behaviours may have to implement it since they can't rely on the other having it.

To address this there a number of different solutions that can be used but I'm not too sure which one is correct/best to use.

Solution 1

The model has this implementation to calculate it, and holds an optional so it is only calculated once.

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

Pros: Neither BehaviourA or BehaviourB have to hold it.

Neither BehaviourA or BehaviourB needs to know about each other (nod towards decoupled strategy pattern)

Any behaviour can use it.

Cons: Not every behaviour (or any behaviour in some implementations) may even require it.

Model is now somewhat specialised and looses some generalisation.

Model may turn into some uber state object holding all possible values which may or may not be used by different behaviours. Messy large interface potentially.

Solution 2

A 'Model global state' object which can hold arbitary values which some behaviours may populate, and others use.

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

Which is held by the Model, and behaviours can use it (or populate it if not there)

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

Pros: Decouples Model from state meaning Model remains generalised, and its behaviours have dependencies on the ModelState object that it used. (when Model is instantiated, it can deduce which state object it reuires, based on which behaviours are used).

Any behaviour can trigger the heavy calc, so behaviour invocation ordering is agnostic.

Cons: Requires some logic to deduce which state objects to use. More complexity required with instantiating.

Some state objects may end being uber objects containing loads of things which may or may not be used by different behaviours. Introduce some more behaviour's which use a model 'global' value, and potentially I have to introduce other state objects to hold this model 'global' value.

Solution 3

Introduce another behaviour to do 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_;
};

Pros: Keeps Model generalised, doesn't reduce strategy pattern flexability more than it already is when using the design that behaviours can know 'something' about other behaviours (again not a totally pure strategy pattern, but still flexible).

Cons: How granular do we want to go with behaviours performing operations that other behaviours require (although thinking about it, this would be similar to rule of three refactor... if two or more behaviours require something that is heavy to calc, that 'heavy to calc' thing becomes another behaviour).

Dependancy of behaviours can turn into minefield... suddenly to use BehaviourA, we need BehaviourC, BehaviourB BehaviourD etc... while I have already introduced this potential dependency's between behaviours (the coupling), it is currently fairly minimal and would like to keep it as minimal as possible.

Having another Behaviour means in future the list could get large and thus loosing even more purity of the behaviour patter and requiring a big list of depdencies given some behaviour. Very coupled!

Solution 4

Each behaviour calculates it's own value.

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

Pros: Each behaviour can be somewhat sandboxed and does not have requirement on another behaviour, even if another behaviour uses the same calculated value. Increased decoupling.

Cons: The SomeHeavyCalculation() calculation is performed twice by each behaviour. This is exactly what I'm trying to mitigate!

This calc itself may want to be implemented differently (which actually points to Solution 3 being the best solution).

I dunno what to do ????

Solution 1 I don't like as I would prefer a more generalised model interface and don't want it to become some uber concrete class. Solution 2 I think is better than 1, however suffers the same problems that may occur in Solution 1 with the state becoming an uber interface. It also means more headache in terms of maintenece as there needs to be some logic or design which relates to the behaviours to use the correct state objects given the behaviours in a model. Coupling is now not just between behaviours, but also there corresponding state objects.

Solution 3 is my gut feeling about what should be done, but what worries me is that some point in the future... all of the sudden, to use BehaviourA I need a BehaviourC, to use C I need a D etc... heavy coupling may occur, making it hard to construct certain models without knowing to include other strategies.

I really don't know which one to use or if I am using this pattern correctly to it's full potential.... or if I should be using another pattern (which I am unaware of). Apologies for the length of the question and I really hope I am not missing something obvious here.

Any help would be appreciated... my head hurts thinking about this :(.

Aucun commentaire:

Enregistrer un commentaire