2022-04-16
I haven't seen this pattern named yet, but I call it "ThingDoer".
It goes like this: take a function which does something.
def do_thing(x: Foo, y: Bar) -> Baz:
...
Disguise the function as a class:
class ThingDoer:
def __init__(self, x: Foo, y: Bar):
self.x = x
self.y = y
def do(self) -> Baz:
...
Et voilĂ !
Even in a language like Java, there's a better way to do it. (Use the class as a namespace and stick a function on it as a static method.) But doing it in a language with first-class functions is not only ugly, but needless.
This can be taken further.
Sometimes you find multiple methods, which work (and communicate) by mutating the internal state, and need to be invoked in the correct order:
thing_doer = ThingDoer(x, y)
thing_doer.do_part1()
thing_doer.do_part2()
z = thing_doer.result()
The functional equivalent would be one or more functions.
Doing it with one function is obviously both prettier and safer. (z = do_thing(x, y)
).
Doing it with multiple functions may be prettier, and should be less error-prone.
If the thing can go wrong, then instead of signaling it in the return
value (e.g. Optional[Baz]
), the programmer raises a custom exception:
class ThingDoerException(Exception):
pass
There's nothing wrong with using exceptions to signal errors, especially those
which are meant to bubble many levels up before they're caught. In Python, with
its EAFP approach, exceptions are common. (Heck, raising StopIteration
is part
of the built-in iterator protocol.)
But custom exception classes are useful only when the caller wants to distinguish between different classes of exceptions. Are you writing a library which exposes several specific kinds of failure in its public interface? Sure, go ahead! Write custom exception classes. (And make sure to subclass the proper built-in ones when appropriate.) But if the caller cannot sensibly adjust his behaviour depending on the error, there's no sense.
Another variation: putting the ThingDoer
in a separate module. Java allows
only one public class per file, and Java programmers often see it fit to create
a new package and directory for it, too. In Python, this can look like:
$ ls -lR thingdoer/
thingdoer/:
total 4
-rw-r--r-- 1 user user 0 Feb 24 01:23 __init__.py
-rw-r--r-- 1 user user 1234 Feb 24 01:23 thingdoer.py
Much boilerplate for no utility.