Commands and the STL
Consider the following classic (so classic I'm presenting it in C++03 terms) execution model:
std::map<int, IFunctor*> CommandMap;Where the IFunctor interface has a virtual method
virtual void process(const Message& inMessage) = 0;
That's basically the substructure of a command pattern, if we assume that the Message class has
int getMessageType() const;
as a function.
There's a potential problem.
What if your data to be processed has to be analyzed not by some nice comparable key, but by a complex of conditions, where the characteristics looked at are variable, although either the conditions are exclusive - no two can be true if the same message - or they can be strictly ranked. Thus, say, the message is analyzed based on the client associated with it or the money associated with it, or the jurisdiction it is from, or a combination of the three. (E.g. the first priority condition might be "Client is a PEP", then, "Transaction is for over 100,000 and client has a certain credit rating or better", then "client is a branch of a foreign corporate parent" ...).
You can force this into a command model - evaluate a transaction to create an arbitrary token based on the criteria and their weighting, then go to a map. However, note that the process of generating the tokens - a series of if/else tests - looks a lot like the logic needed to execute the logic directly, except that you've stuck the token in the middle. (If you can get the originating source of a received message to generate the code on that end, great. Otherwise, not such a great idea.)
Consider inverting it. If you change the signature to
virtual bool process(const Message& inMessage) = 0;
where the function now returns true if the message matches that class's requirements, then you can iterate over a vector or array of the objects until it returns true. (Or instead of a return value, the object can have a link to the next one in order, in which case you have a singly linked list implementing a Chain of Responsibility pattern.) That's not as fast at dispatch as a command pattern, but it's reasonably clean.
An array version might be able to be initialized with a constexpr model, shifting the cost of setting up the tests to compile-time.
The vector version can be implemented with find_if(), but there is a catch: it is theoretically possible to have an actual iteration order different from the order of items in the vector (though find_if is guaranteed to return the first matching element). With tests which have no side-effects this is not of concern, whereas in this case it would be a concern: you could have messages handled by actions with the wrong priority).
However, you can sever the test and the execution so that they are different functions in different objects, making for cleaner responsibilities; it also allows for reuse if (as will frequently be the case) the predicate is of use in more contexts than the one (If you have special rules for handling PEPs then you probably have them poking up in different circumstances). Most importantly, it is not vulnerable to odd implementations of find_if(), as the test itself has no side effects. If you use a sentinel at the end which always returns true but is associated with a no-op you don't even have to check the return value, but just execute the second component of the object pointed to by the returned iterator.
However, you can sever the test and the execution so that they are different functions in different objects, making for cleaner responsibilities; it also allows for reuse if (as will frequently be the case) the predicate is of use in more contexts than the one (If you have special rules for handling PEPs then you probably have them poking up in different circumstances). Most importantly, it is not vulnerable to odd implementations of find_if(), as the test itself has no side effects. If you use a sentinel at the end which always returns true but is associated with a no-op you don't even have to check the return value, but just execute the second component of the object pointed to by the returned iterator.
Alternatively, if your tests are truly exclusive, and can be meaningfully ordered, you can use lower_bound to get a binary_search model for matching the condition. This is likely to be a significant benefit only if there are a fair number of tests (the sort where there's an additional benefit in using generate() or generate_n() to build the vector/array rather than having a massively long hard-coded set of if/else tests). In this case you do not want a sentinel.
In C++20 the associated structure now looks like
std::vector<std::pair<std::function<bool(const Message&)>, std::function<void(const Message&)>>>
If you want to, and it's a stateless function or a closure with reentrancy and thread-safety, you don't have to execute the function when you find it. You can pass it on as an object to be called later. You can execute it in a coroutine, or associate it with different data for it to be executed on.
If your predicates are truly exclusive you can order them in terms of most frequent and / or cheapest, to facilitate execution. If they are merely prioritized, then you may be stuck with expensive but rare tests up front. (Not many of your clients will be PEPs.)
In C++20 the associated structure now looks like
std::vector<std::pair<std::function<bool(const Message&)>, std::function<void(const Message&)>>>
If you want to, and it's a stateless function or a closure with reentrancy and thread-safety, you don't have to execute the function when you find it. You can pass it on as an object to be called later. You can execute it in a coroutine, or associate it with different data for it to be executed on.
If your predicates are truly exclusive you can order them in terms of most frequent and / or cheapest, to facilitate execution. If they are merely prioritized, then you may be stuck with expensive but rare tests up front. (Not many of your clients will be PEPs.)
Comments
Post a Comment