Better Ways to Implement Single-Method Classes

Tom Oram
Cloudnative.ly
Published in
4 min readJul 3, 2023

--

/imagine a class with a single method rewritten as a higher-order function

Suppose you’re coming to a programming language which allows you to program in multiple paradigms, like Python or TypeScript, but you have come from an object-oriented programming (OOP) background. In that case, you might choose to continue building your software in the OOP way you love. Writing code this way is perfectly reasonable, but you might create unnecessary boilerplate code if you insist on using the class keyword for everything. I want to show how you can rewrite a common type of class differently, reducing the amount of typing you have to do and making the code more straightforward to understand.

Single Method Classes

The type of class I’m talking about is a single-method class. These classes represent a single action, but they might wrap up some state or dependencies which the method will use when invoked.

Let’s look at some examples:

Simple Action

The simplest form is a function with no dependencies wrapped in a class. The only reason these classes exist is that the language does not support functions outside of classes. Java introduced the Runnable interface for this exact reason.

class Adder:
def calculate(self, a: int, b: int) -> int:
return a + b

Here we can move the class out of the function altogether.

def add(a: int, b: int) -> int:
return a + b

You might think you have lost the ability to define an interface that your Adder class implemented so that you supply alternative implementations to consumers — for example, a Calculator interface. You may not be able to create an interface (or a Protocol in the Python world) when you only have a function; however, you can still define an abstraction by creating an alias to a callable type.

Calculator = Callable[[int, int], int]

Dependency Injection

Another example of a class with a single method is one where we use constructor injection to provide dependencies for the method. This enables us to pass only the instance to where we need it without having to pass the dependencies too. Here’s an example of such a class.

class OrderCreator:
def __init__(self, order_repository: OrderRepository, email_sender: EmailSender):
self._order_repository = order_repository,
self._email_sender = email_sender

def create_order(self, customer: Customer, items: list[Items]) -> None:
order_id = OrderID.generate()
order = Order(order_id, items)
self._order_repository.save(order)
confirmation_email = Email(to=customer, subject=f"Thank you for your order ${order_id}")
self._email_sender.send(confirmation_email)

We would then use it like this:

order_creator = OrderCreator(some_order_repository, some_email_sender)

# at another code location
order_creator.create_order(logged_in_customer, cart.get_items())

We can create this pattern by creating a constructor function which returns a new closure function with the dependencies baked in. We can again use a type alias to define a meaningful named type.

CreateOrder = Callable[[Customer, list[Items]], None]

def new_order_creator(order_repository: OrderRepository, email_sender: EmailSender) -> CreateOrder:
def create_order(customer: Customer, items: list[Items]) -> None:
order_id = OrderID.generate()
order = Order(order_id, items)
order_repository.save(order)
confirmation_email = Email(to=customer, subject=f"We have recieved your order {order_id}")
email_sender.send(confirmation_email)
return create_order

We would then use it like this:

create_order = new_order_creator(some_order_repository, some_email_sender)

# at another code location
create_order(logged_in_customer, cart.get_items())

Deferred Dependencies

This pattern is the same as the previous example but used the other way around. In this case, we inject the values in the constructor but provide some dependencies when we invoke the method. Let’s see an example:

class Email:
def __init__(self, to: str, subject: str, body: str):
self._to = to
self._subject = subject
self._body = body

def send(self, sender: EmailSender) -> None:
sender.send(self._to, self._subject, self._body)

We would then use it like this:

email = Email(
to=customer.email_address,
subject=f"We have received your order {order_id}",
body=f"""
Dear {customer.name}
Thank you for your order!
"""
)

# at another code location
email.send(some_email_sender)

Because the pattern is the same as the previous one, the classless version follows the same pattern:

SendEmail = Callable[[EmailSender], None]

def create_email(to: str, subject: str, body: str) -> SendEmail:
def send(sender: EmailSender) -> None:
sender.send(to, subject, body)
return send

We would then use it like this:

send_email = create_email(
to=customer.email_address,
subject=f"We have received your order {order_id}",
body=f"""
Dear {customer.name},
Thank you for your order!
"""
)

# at another code location
send_email(some_email_sender)

Summary

If you create classes with a single method, you might be better off just creating a function. Using functions might seem like you are not doing OOP, but the model remains the same; an instance wraps a behaviour with a single way to invoke it. Also, you can use type aliases to give these functions named abstractions, which you can use for type signature, descriptiveness and communication. The command classes can be necessary for object-oriented languages because there is no better way to implement them.

--

--

Passionate about all aspects of software. Engineer at Armakuni.