(Fowler) Refactoring introduces one of my favorites patterns, called the Command (formerly “method object”). This is an object built around a single method. He gives two refactoring patterns to use it:

Replace function with command (method object) Replace command (method object) with function

Python makes this particularly attractive because it gives you the __call__ method to make it seamless:

class ThingDoer:
	def __init__(self, ...):
		...

	def __call__(self, ...):
		...

do_thing = ThingDoer(...)
do_thing(...)

This construct makes a lot of sense when ThingDoer needs to manage persistent state across multiple calls to do_thing.

I often use it in a more underhanded way, however. Often, I need to go through a fairly lengthy process of setting things up for a single call. The traditional approach would be to use a Factory to construct some fairly lightweight function or command. But you’re only using it in one place.

In this case, I like to put the factory as a class method of my Command object, keeping the constructor free of side-effects for simpler unit testing. This keeps everything together, while still maintaining some separation:

class ThingDoer:
	def __init__(self, x, y):
		self.x = x
		self.y = y

	@classmethod
	def build(cls, ...) -> "ThingDoer":
		x = ...
		y = ...
		return cls(x, y)

	def __call__(self)   # Notice no arguments!
		...

What makes this underhanded is that we don’t end up passing in the setup arguments in the __call__ method but rather in the build method. This is not the end of the world, but it does come with some significant drawbacks:

  • It makes the call’s behavior more opaque. What exactly is this call doing? Viewed in isolation, it seems to work purely by side effects.
  • If you aren’t deeply familiar with Python, you may well have no idea what’s happening here. If the code reaches a larger team of engineers, the result may be accelerated rot.

This is all the more true if you also use @dataclass to make it even more succinct:

@dataclass
class ThingDoer:
	x: int
	y: str

	@classmethod
	def build(cls, ...) -> "ThingDoer":
		x = ...
		y = ...
		return cls(x, y)

	def __call__(self)   # Notice no arguments!
		...

I think this is beautiful, but people have certainly viewed it with shock.