Chris
Chris

Reputation: 1353

Runner method as alternative to method chaining?

I have a class that I use for data analysis. Generally I need to create a couple instances of the class and then run a series of methods on each. The methods I need to run will vary from instance to instance.

Today my code (using the class) usually looks something like this:

object_alpha = MyClass()
object_alpha.method1()
object_alpha.method2()
object_alpha.method3(arg1)
object_alpha.method4()

object_bravo = MyClass()
object_bravo.method1()
object_bravo.method3(arg1)
object_bravo.method4()
object_bravo.method5()

I know the above example is not an example of method chaining. Method chaining is not currently possible because the methods do not return an object of the class.

This format gets a bit repetitive a tedious, especially with long descriptive variable names. My primary complaint is that I do not find it very readable.

I thought about changing my class to return a new object from each method call, so that I could do method chaining. But the side-effect of the methods do not change the class--they are making changes to a database via an API, so it feels strange to return a new object.

My thought is to create a runner class that would take a list of method names as strings. So that I could do something like this.

object_alpha = MyClass().runner([
    'method1',
    'method2',
    'method3(arg1)',
    'method4'
])

object_bravo = MyClass().runner([
    'method1',
    'method3(arg1)',
    'method4',
    'method5'
])

Is this a bad idea; is there a better approach?

Upvotes: 2

Views: 250

Answers (2)

Mad Physicist
Mad Physicist

Reputation: 114468

Your idea is good, but it can use one major improvement: instead of using a list of strings that you have to eval, use a list of tuples or something like that to contain the methods themselves and their arguments:

object_bravo = MyClass()
bravo_runner = [
    (object_bravo.method1, (arg1, arg2), {k1: v1, k2, v2}),
    (object_bravo.method2, (arg3), {}),
    (object_bravo.method3, (), {k3: v3}),
    (MyClass.method4, (object_bravo, arg4), {k4: v4})
]

The way to run that would be much easier than parsing strings:

for spec in bravo_runner:
    spec[0](*spec[1], **spec[2])

If you used namedtuple for the runner elements, it would look even better:

from collections import namedtuple
RunnerSpec = namedtuple('RunnerSpec', ['method', 'args', 'kwargs'])
object_bravo = MyClass()
bravo_runner = [
    RunnerSpec(object_bravo.method1, (arg1, arg2), {k1: v1, k2, v2}),
    RunnerSpec(object_bravo.method2, (arg3), {}),
    RunnerSpec(object_bravo.method3, (), {k3: v3}),
    RunnerSpec(MyClass.method4, (object_bravo, arg4), {k4: v4})
]

and the run method:

for spec in bravo_runner:
    spec.method(*spec.args, **spec.kwargs)

Conclusion

At this point, you may actually save some typing by just writing a method within the class for each sequence/scenario that uses self instead of the descriptive object name. That would save you the most in the end because you would have pre-constructed, named sequences of calls. No need to store them as lists. bravo_object.run_scenario3() is better than bravo_object.runner([big_ass_hard_to_read_list_that_is_basically_a_method_anyway]).run().

Upvotes: 1

Thomas Lotze
Thomas Lotze

Reputation: 5323

Basically, you're trying to invent a domain-specific language here. Since its sole purpose would be to execute something much like Python, only without having to name the context object, I don't think it's worth the hassle. The pattern of returning an object from a method to allow method chaining, even though the object is not the logical result of the operation, is actually not unheard of, so I'd go for that.

Upvotes: 1

Related Questions