-1

I previously asked about combining multiple similar functions (~10 variants) with lots of repeated or similar code into something more concise and OO. Following up on that, I've created a central class whose methods have enough flexibility to permute into most of the functions I need given the right arguments:

class ThingDoer:
    def __init__(self, settings):
        # sets up some attrs
        ...
    def do_first_thing(self, args):
        identical_code_1
        similar_code_1(args)
    def do_other_thing(self, otherargs):
        similar_code_2(args)
        identical_code_2
    def generate_output(self):
        identical_code
        return some_output

The actual thing is rather longer of course, with more different sets of args and otherargs etc.

I then use this class in a relatively clean function to get my output:

def do_things(name, settings, args, otherargs):
    name = ThingDoer(settings)
    name.do_first_thing(args)
    name.do_second_thing(otherargs)
    return name.generate_output()

My question is how to handle the many variants. Two obvious options I see are 1) have a dictionary of options specified differently for each variant, which gets passed to the single do_things function, or 2) have different subclasses of ThingDoer for each variant which process some of the different args and otherargs ahead of time and have do_things use the desired subclass specified as an argument.

Option 1:

# Set up dictionaries of parameter settings
option_1 = {args: args1, otherargs: otherargs1 ...}
option_2 = {args: args2, otherargs: otherargs2 ...}
...

# And then call the single function passing appropriate dictionary
do_things(name, settings, **option)

Option 2:

# Set up subclasses for each variant
class ThingDoer1(ThingDoer):
    def __init__(self):
        super().__init__()
        self.do_first_thing(args1)
        self.do_other_thing(otherargs1)

class ThingDoer2(ThingDoer):
    def __init__(self):
        super().__init__()
        self.do_first_thing(args2)
        self.do_other_thing(otherargs2)
...

# And then call the single function passing specific subclass to use (function will be modified slightly from above)
do_things(name, subclass, settings)

There are other options too of course.

Which of these (or something else entirely) would be the best way to handle the situation? and why?

3
  • 2
    Can you give a more specific example of what you're trying to accomplish? As the question is now, it's hard for me to know what pitfalls you're trying to avoid and what things you want to make clearer via OOP. Commented Jun 27, 2020 at 22:51
  • The variant functions that I originally had basically modify / compile a set of instructions, which is then fed into a specific external modelling software. The external software gets called many times with many different configurations of instructions in the course of my overall workflow. So the variants exist in part to configure the instructions differently and in part for workflow control at different stages of the overall process. Commented Jun 27, 2020 at 23:00
  • As to what I am hoping for re: coding choices, I am overall trying to minimise repetition (previous code had a lot of that) while maintaining reasonable readability & ease of understanding. Flexibility to add further variants later with minimal code duplication is good too. Speed is less important as the external software is far and away the bottleneck. Commented Jun 27, 2020 at 23:05

2 Answers 2

2

The questions you have to ask yourself are:

  1. Who will be using this code?
  2. Who will be maintaining the divergent code for different variants?

We went through a similar process for dealing with multiple different devices. The programmer maintaining the variant-specific code was also the primary user of this library.

Because of work priorities we did not flesh out device-specific code until it was needed.

We settled on using a class hierarchy.

We built a superclass modeled on the first device variant we built code to address a particular piece of functionality and wrapped this in automated testing.

When we extended functionality to a new device that did not pass testing with existing code, we created overriding, modified methods in the new device's subclass to address failures.

If the functionality was new, then we added it to the base class for whatever device model we were working on at the time and retrofitted changes to subclasses for old devices if testing failed and they needed the new functionality.

Sign up to request clarification or add additional context in comments.

Comments

1

Generally speaking, it depends on what level of customization you want to expose to those that will consume your API. Suppose we're writing some code to send an HTTP request (just an example obviously there are plenty of libraries for this).

If callers only care about easily configurable values, using keyword arguments with sane defaults is probably the way to go. I.e. your code might end up looking like:

from typing import Dict

def do_request(url: str,
               headers: Dict[str, str],
               timeout: float = 10.0,
               verify_ssl: bool = False,
               raise_on_status_error: bool = False):
    # ...

do_request('https://www.google.com')

If you want to expose more customized behavior you might benefit from defining a base class with several methods that can be overridden (or not) to provide more specific behavior. So something like this:

class HTTPClient(object):

    def do_request(self, url: str, *args, **kwargs):
        self.before_request(url, *args, **kwargs)
        result = self.send(url, *args, **kwargs)
        self.after_request(result)
        return result

    def send(self, url: str, *args, **kwargs):
        # Same stuff as the above do_request function.

    def before_request(self, *args, **kwargs):
        # Subclasses can override this to do things before making a request.
        pass

    def after_request(self, response):
        # Subclasses can override this to do things to the result of a request.
        pass


client = HTTPClient()
response = client.do_request('https://www.google.com')

Subclasses can implement before_request and after_request if they want, but by default, the behavior would be the same as the above functional equivalent.

Hopefully this helps! Sorry in advance if this isn't really relevant for your use case.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.