Simplifying Messy Conditions: Adaptive Models

Hello!

Today we’re getting into the thorns of programming. Beware cactus. 🌵

Years ago I found Martin Fowler’s article on Adaptive Models. Adaptive models let you replace nests of conditions with a declaration of actions. That pattern has helped clean up my DevOps code a ton of times.

Fowler is a better programmer than me. His article is The Source of Truth for this pattern. However, it’s pretty in-depth and to me it’s complicated. I bet he’s mostly writing for people who write more code than I do. Developers, not DevOps. I’ve struggled to share the pattern with other folks like me, who are only partly developers, so I wrote simplified explanation.

Imagine we’re converting HTML files into markdown with this fake script:

file_names = [
    'do_not_convert.md',
    'also_do_not_convert.md',
    'convert.html',
    'also_convert.html'
]

def should_convert(file_name):
    extension = file_name.split('.')[1]
    if extension == 'md':
        return False
    elif extension == 'html':
        return True

def convert(file_name):
    new_file_name = f"{file_name.split('.')[0]}.html"
    print(f'{file_name} > {new_file_name}')

if __name__ == '__main__':
    for file_name in file_names:
        if should_convert(file_name):
            convert(file_name)

We’re printing instead of logging and doing lots of other little things wrong, but that’s just because this is demo code. The real problem is this:

def should_convert(file_name):
    extension = file_name.split('.')[1]
    if extension == 'md':
        return False
    elif extension == 'html':
        return True

We have to write logic that understands each case. We only have two file types so it’s not too bad here, but in real life it’ll be worse. We’ll need to skip files that start with “demo”, run different conversion functions for different file types, include any files that start with “demo” but aren’t HTML. It’ll get ugly. It always does. You’ll have a mess of conditions.

Adaptive modeling gives us an alternative: write a dictionary that models each case, then trigger the right action by looking up our case in the dictionary (adapting to the model).

convert_extensions = {
    'md': False,
    'html': True
}

file_names = [
    'do_not_convert.md',
    'also_do_not_convert.md',
    'convert.html',
    'also_convert.html'
]

def convert(file_name):
    new_file_name = f"{file_name.split('.')[0]}.html"
    print(f'{file_name} > {new_file_name}.html')

if __name__ == '__main__':
    for file_name in file_names:
        extension = file_name.split('.')[1]
        if convert_extensions[extension]:
            convert(file_name)

One lookup in the convert_extensions dictionary and we’re done. We don’t need the should_convert() function anymore.

Both scripts do the same thing, but I think the second one, the adaptive one, is better. I like reading the code more. It’s super nice to be able to see all our cases in a dictionary instead of reading if conditions. We can handle new file types just by adding them to convert_extensions.

Plus, if we add a file we don’t know how to convert (e.g. test.doc), we get a clear error even though we didn’t write any error handling code:

Traceback (most recent call last):
  File "adaptive.py", line 21, in <module>
    if convert_extensions[extension]:
KeyError: 'doc'

Error handling, for free. Boom. 💥

When I’m writing a block of conditions, I try to stop and think about whether I’m better off writing my cases into a model and only using code to interpret that model. Often, it’s a cleaner pattern.

With a more complex model you can do much more than what I’ve done here. I recommend reading Fowler’s article. His example and his coverage of the pattern are more in-depth than mine. He also goes deeper into what you gain and lose with the pattern in one of his sections (in his case the code actually gets longer). My goal here is just to introduce a power tool you might want to carry in your toolbox.

Keep your automation fancy!

Adam

If this was helpful and you want to save time by getting “copy and paste” patterns for Python DevOps in your inbox, subscribe here. If you don’t want to wait for the next one, check out these: