5

Here is the situation:

I have a database and a class that is basically a representation of a set of fields of different tables in it. Let's call it Message. "Message" does not know of the database and whenever I need to synchronize it to DB I use separate functions accepting instances of Message to do it.

Whenever I need to do some operation that involves synchronizing data from "Message" to database, I do not just call the Modifier1/Modifier2 functions because more often than not, I need to either pass or roll back the whole sequence. (usually when ORA- DB error occurs)

So, I have a queue of actions set up:

std::list<std::function<DBType>>

In reality there is a wrapper class around this queue that receives actions via signals/slots mechanism and executes the whole accumulated pack of actions on:

Execute();

So, it's like this : if I need to update the database in some piece of code:

   // sending update #1
   emit dbAction(std::bind<CREATE SOME FUNCTOR HERE>);
   // sending update #2
   emit dbAction(std::bind<CREATE SOME OTHER FUNCTOR HERE>;
   // flushing the queue
   emit flushDb();

the actions execute, everyone is happy. End of the story. Until now that is. Now, I realized I have to have a set of preconditions on "Message" that will prevent these actions from passing through.

Options available:

1) To inject verification code into called functions. But the conditions have nothing to do with them and entirely depend on external context.

2) To let user of the queue to check conditions in outside code. But let's be realistic - even if I am the said user, what's not enforced, is easily forgotten.

3) To use some class to emit these actions after checking preconditions on the arguments. But this will require to update its interface for any new action and will not allow to just send a lambda through.

4) My current insane solution: instead of just sending std::function with already bound arguments, create a templated functor that keeps parameters accessible until they are actually needed by the queue.

//abstract class so that I can store unrelated template instantiations in a single container in my queue
class  IMessageActionDB
{
  public:
    virtual ~IMessageActionDB();
    enum EActionTypes
    {
          noaction = 0,
      action_type_1 = 1,
      //...
      action_type_N = N
    };

    void operator()(DBType db)
    {
        if(!Verify())
            throw exception;
        BindToExecutable();
        calledFunc(db);
    };
     // will bind whatever is stored in MessageActionDB to the calledFunc
    virtual void BindToExecutable() = 0;
    // will check the action with supplied checker functor
    bool Verify() = 0;

    protected:
    std::function<void(DBType)> calledFunc;
};

// templated action that will accept any function/number of parameters for later use
template<class FunctionType, class ...Ts>
 class  MessageActionDB : public IMessageActionDB
{
  public:
    MessageActionDB(FunctionType f, std::tuple<Ts...> t);

    void BindToExecutable();
    {
        //perform template mumbo jumbo to bind func and parameterPack to calledFunc
        calledFunc = bind_tuple(func, parameterPack)
    }
    bool Verify() 
    {
        //call verifiers in order returning if action satisfies or not
        return result;
    };

  private:
     std::tuple<Ts...> parameterPack;
     FunctionType func;
     std::list<std::function<bool(Ts...)>> actionVerifier;
}; 

(Note taht I an still unsure if I want to put verifiers into constructor parameters or let them be generated by a factory singleton in a constructor body)

This way, I can assign verifier for action based on its EActionTypes type, and queue wrapper can use Verify function at invokation time to check if the action is allowed.

Is this sane? Is there a better way/place to inject checking of preconditions?

Quite frankly, my solution feels awkward, I can't shake the feeling that I've done something completely wrong.

1

Your description sounded a lot like functional approach. But let me try to inject some object orient-ness ;)

From what I've understood so far, you have a simple pipeline which is passed a Message object. Then, depending on some conditions (or unconditionally), some database actions are executed and then the whole operation is committed (or rolled back).

So, I see it like this:

Each action, performed on the database is a Command pattern implementation (DBCommand).

When you need some conditions to be applied to the command execution, you can create a new command, that implements Composite pattern (ConditionalDbCommand). When ConditionalDbCommand's execute() is called, it first verifies the conditions and if they are ok, execute() of inner command is called.

The pipeline can also look like a command. It holds a list of commands to execute and commits/rolls back wrapping transaction.

  • It was my first idea, but I decided against it as, when your code has explicitly stated(in class body) parameters to check, then you lose the flexibility of just passing the closure to the queue and have to write a subclass for each action. Note that my solution at the end of the post is already Command pattern, just a generalized one and instead of being a ConditionalDbCommand it simply carries actionVerifier with it – Zeks Nov 18 '15 at 12:22
  • You can just as well rename my MessageActionDB to DBCommand btw. The approach is 'functional' only in a sense that it has a generalized Command class accepting closures rather than using subclasses everywhere – Zeks Nov 18 '15 at 12:24
  • @Zeks Well, you decide, of course. But I tend to prefer proper algorithm incapsulation into classes (Strategies for example) vs closures. It improves the testability of the code (I can test strategies separately) and enforces architecture onto project. – Vladislav Rastrusny Nov 18 '15 at 12:43
  • 1
    If anything I will have to think about that. I tend to shy away from class hierarchies where I can use generalized approach as it seems more flexible but maybe some kind of order might be, well, in order – Zeks Nov 18 '15 at 12:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.