Removing consecutive if tests
Consider a set of exclusive tests followed by actions. That is:
if (test1())doSomething();
else if (test2())
doSomethingElse();
else if (test3())
...
If you get the data for the conditions and the actions into a single convenient parameter, if necessary by creating an arbitrary struct, then we have:
Param foo(...);
if (test1(foo))
doSomething(foo);
else if (test2(foo))
doSomethingElse(foo);
else if (test3(foo))
...
This is exactly equivalent, logically, to the sequence in
using func_pair=std:pair<std::function<bool(const foo& )>, std::function<void(foo&)>>;
std::vector<func_pair> vec;
auto iter = std::ranges::find_if(vec, [&foo](func_pair inVal) -> bool { return inVal.first(foo); });
if (iter != vec.end())
iter->second(foo);
if the vector has been populated with the appropriate tests.
(This is not exactly the Command pattern, which is more a generalization of the switch statement. I introduced this a few posts ago, in "Commands and the STL": this is an expansion on that post.)
The latter is certainly not more intuitive. (You could teach the if/else if sequence to a beginning class in computer programming; the idea of an unordered array of pairs of functions is nowhere near as obvious.) It's almost certainly not more efficient, (probably about equivalent in execution, but it requires time to set up). Why would one consider using it?
One obvious reason is that all the tests have to be set out as one coherent block of code in the if/else based version. In the vector-based version tests can be added one at a time (which can be a help to comprehension).
They can be added selectively, based on different contexts. You can get the same effect by putting disabling or enabling logic into the tests themselves, but that is somewhat ugly.
The set of tests can also evolve or change during the course of of the program (you might have one set of tests for after work hours, for example) and this allows you to make the change in one place at the time rather than testing for the context each time. Likewise, you can load which tests to apply from a configuration file.
They can be filtered via take or drop views, altering the selection of tests. Likewise, you can use a join view to extend a set of tests selectively.
You can use the same structure to support an iterative rather than exclusive set of tests (i.e. omit the else's) by using for_each with a functor which executes with the second function if the first matches. Note that the model allows for changing the state of the parameter object in the second function, so one rule can be set up for by another rule.
They can be added selectively, based on different contexts. You can get the same effect by putting disabling or enabling logic into the tests themselves, but that is somewhat ugly.
The set of tests can also evolve or change during the course of of the program (you might have one set of tests for after work hours, for example) and this allows you to make the change in one place at the time rather than testing for the context each time. Likewise, you can load which tests to apply from a configuration file.
They can be filtered via take or drop views, altering the selection of tests. Likewise, you can use a join view to extend a set of tests selectively.
You can use the same structure to support an iterative rather than exclusive set of tests (i.e. omit the else's) by using for_each with a functor which executes with the second function if the first matches. Note that the model allows for changing the state of the parameter object in the second function, so one rule can be set up for by another rule.
auto iter = std::ranges::for_each(vec, [&foo](func_pair inVal) -> bool { if (inVal.first(foo)) inVal.second(foo); });
You can modify the tests on the fly by adding additional logic to the first, testing, function in a specific context.
You can use std::any_of or std::none_of to determine if any conditions are met, without executing the bodies. You can use std::count_if to determine how many are met given the initial values. These sound unintuitive, but it's easy to see applications in unit testing and in data analysis (are the tests truly exclusive for this piece of data?).
The sequence logic can be extracted into a general call (most obviously, using templates) in a way which is not practical for if/else tests. If you do so, you can provide options for handling the case when no match is found by adding generic final actions with tests that always return true (e.g. a no-op to avoid having to test against vec.end(), or throwing if the tests are supposed to have complete coverage) via simple options on the generic form.
All that being said, it's undoubtedly overkill to use the construct for logic sequences which
1) Are short,
2) Have no shared logic with other locations, and
3) Can be shown to be probably thus localized throughout the life of the project.
You can modify the tests on the fly by adding additional logic to the first, testing, function in a specific context.
You can use std::any_of or std::none_of to determine if any conditions are met, without executing the bodies. You can use std::count_if to determine how many are met given the initial values. These sound unintuitive, but it's easy to see applications in unit testing and in data analysis (are the tests truly exclusive for this piece of data?).
The sequence logic can be extracted into a general call (most obviously, using templates) in a way which is not practical for if/else tests. If you do so, you can provide options for handling the case when no match is found by adding generic final actions with tests that always return true (e.g. a no-op to avoid having to test against vec.end(), or throwing if the tests are supposed to have complete coverage) via simple options on the generic form.
All that being said, it's undoubtedly overkill to use the construct for logic sequences which
1) Are short,
2) Have no shared logic with other locations, and
3) Can be shown to be probably thus localized throughout the life of the project.
This is one mechanism for replacing if/else logic along the lines recommended by Five Lines of Code. But in small contexts, I suspect that it interferes with rather than enhances maintainability, unless the mechanism were to be a genuine idiom shared generally by all the developers.
Comments
Post a Comment