Modular, extensible, maintainable, and testable functional strategy pattern - part 1
Tags: python, architecture, functional, codequality
Published: 2021-09-10 15:09:54

The **Strategy Pattern** is likely the most used design pattern. *Who said it? Well, I said it.* The strategy pattern concept is simple. At a given moment, the execution workflow must take one from different paths depending on the evaluation of the current state, being *state* a variable or a group of variables.

There are countless situations to apply the strategy pattern; however, we will focus on analysing a **single variable** to control the execution flow.

We have variable `var1` and, depending on the boolean result of different evaluations, `var1` is processed differently.

```
var1 is int -> process it as an int
var1 is long string -> process the long string
var1 is short string -> process the short string
```

What could be the most **extensible**, **maintainable**, **modular**, and **testable** way to implement a strategy pattern here? Let's see different options:

1) `if-statements` are the most easily grasping strategy, likely used by all novice programmers but also advanced ones (when coding for very straightforward cases :wink:). 

```python
if isinstance(var1, int):
     # do some logic here
elif isinstance(var1, str) and len(var1) > 100:
     # do other logic here
elif isinstance(var1, str) and len(var1) <= 100:
     # do yet another logic here
else:
     # everything before was false
     # do this other thing
```
The case presented above might be a feasible quick implementation for some simple cases. But here, we imagine all operations are long and complex because we are developing an extensive program :relaxed:. So, what are the problems of a direct `if/elif/else` implementation as the one presented above? 

* `if-statements` will become too lengthy and maybe nested as the number of options grows.
* Also, having the business logic directly under the `if clauses` render that code challenging to test. Surely, impossible to test on its own.
* Adding more options will just make the code less readable, especially if implementations are lengthy.
* And, the code is not modular because you cannot use those logic blocks anywhere else as they are hardcoded directly under the `if-clauses`.

**How can we improve from this situation?**

The first step you can do to improve the code's modularity and testability is to encapsulate each logical process in its separate function:

```python
if isinstance(var1, int):
     process_is_int(var1)
elif isinstance(var1, str) and len(var1) > 100:
     process_is_long_string(var1)
elif isinstance(var1, str) and len(var1) <= 100:
     process_is_short_string(var1)
else:
     do_else_logic(var1)
```

By doing this, you will be able to **test** the different functions in their dedicated unit-tests. Also, your code becomes more modular because you can re-use those functions anywhere else; even have them as a public, well-documented library API.

The second step towards improving the quality of your code is to transform your asserting statements into, also, individual functions.

```python
if is_int(var1):
     process_is_int(var1)
elif is_long_string(var1):
     process_is_long_string(var1)
elif is_short_string(var1):
     process_is_short_string(var1)
else:
     do_else_logic(var1)
```

Notice how you can now use the evaluation functions (`is_int`, `is_long_string`, and `is_short_string`) anywhere else in the code and test them properly. Also, suppose you want to add additional features to those functions in a later stage, for example, new assertions or logging. In that case, you can do it easily without the need to modify the `if/elif` block.

*(side comment)* Once you reach this point of understanding, your brain will likely spark the light of using a dictionary to control the flow. Probably you have read about that strategy somewhere. I have used it also. Using dictionaries to control flow is productive in some cases, but its use won't excel in this particular case. So I will discuss using dictionaries to control flow in other posts of my series (keep an eye on them) and won't talk about them here. *(end of side comment)*

Finally, **there is a way that out-stands the previous two**, both `if-else` blocks and that crazy dictionary strategy I told you about but didn't explain :wink:.

The implementation design that best suits our example (in my experience and opinion)  is using a `for` loop. Yes, a for-loop. **How?**

First, we create a **list of tuples** where each tuple has two values: 1) the evaluation function (boolean logic) and 2) the business logic. The `for` loop will loop through the list and operate on the first function that evaluates to `True`.

```python
# note how our logic is getting defined in separate functions
# as we evolve from if-statements to this pipeline-like method
options = [
    # (if_this_is_true, do_this).
    (is_int, process_is_int),
    (is_long_string, process_is_long_string),
    (is_short_string, process_is_short_string),
    (lambda x: True, process_the_else_logic),
    ]

for evaluation, function in options:
    if evaluation(var1):
        function(var1)
        # the loop does not need to continue
        # after a function is executed
        break  
```

In case the `else` logic does not receive `var1` as an argument, you can move it to the `else` method of the for-loop:

```python
options = [
    (is_int, process_is_int),
    (is_long_string, process_is_long_string),
    (is_short_string, process_is_short_string),
    ]

for evaluation, function in options:
    if evaluation(var1):
        function(var1)
        break
else:
    process_the_else_logic()
```

**Benefits of this last approach?** Several:

1. The for-loop executes the boolean logic only once and only if the previous evaluated to `False`, so there are no losses against the `if-else` block.
2. You can easily change the order of the process by changing the order of the tuples in the list without the need to be moving large pieces of code around.
3. You can add/remove new functionalities by adding/removing tuples in the list without changing the engine.
4. It is effortless to read and understand **what** the code will do. Understanding **how** the code will do it is another story. For that you would need to read into each function. Congratulations, you have separated the *what* from the *how*. 

Honestly, I see no flaws in this method. I think it really is **extensible**, **maintainable**, **modular**, and **testable**. If you think differently or know other possible examples, let me know in the comments. One can apply the *strategy pattern* to countless situations. This is one of them. Likely, you have heard of the strategy pattern when implementing OOP, but life is not always about OOP despite what *they* told you :stuck_out_tongue:

But this is not the whole story. For example, many times, we use `if-statements` to evaluate the state of multiple variables. And, of course, that is also possible using the for loop strategy presented here. But asserting the state of multiple variables is a more complex case that would require the use of `functools.partial`, so we leave that discussion for another post.

I hope you enjoyed this quick yet profound discussion, which I am sure will help you design larger and more maintainable software packages. Despite not being skilled in other languages besides Python, I trust you can apply this same (or similar) idea in other realms of programming.

Cheers,