Learning from other languages: Functions are simpler than classes
An idea that circulated a lot around the time when I first learned Ruby about two decades ago was that of "learn a new language every year". It was originally proposed by the Pragmatic Programmers (at least according to Martin Fowler).
The idea is that by learning a new programming language every year, we can broaden our perspective and become better programmers overall.
While I don't believe the "every year" part is strictly necessary, it is my experience that there is always something to be learnt from other programming languages and paradigms.
Today I'd like to share one of the lessons I learned outside Ruby: Functions are simpler than classes.
You might assume that I got this from a functional programming language, but in fact this was something I internalized while having the displeasure of working extensively with React for a couple of years.
In the early versions of React, components were defined as classes (not native JS classes, mind you, but rather using React.createClass), but with time a new way of defining components was introduced: functional components, i.e. plain JavaScript functions that return JSX.
To begin with, the distinction between class components and functional components was typically that the former had state and lifecycle methods, while the latter were stateless and purely presentational. I believe this is where I started to clearly see the appeal of functions over classes: they were simpler, easier to reason about and trivial to debug.
As React evolved, functional components gained the ability to have state and lifecycle methods through the introduction of hooks, and class-based components were mostly abandoned altogether. Whether that ended up being a net positive or not, I can't say, as I moved away from frontend development around that time. But the idea of functions being simpler than classes, and the distinguishing point being whether a piece of functionality needed state or not, stuck with me.
With regard to my Ruby programming, this made me more mindful of when to reach for a class and when to just go for module methods. Example:
class PostCreator
attr_reader :headline, :body
def initialize(headline, body)
@headline = headline
@body = body
end
def call
Net::HTTP.post(
URI("https://api.example.com/posts"),
{ headline: headline, body: body }.to_json,
"Content-Type" => "application/json",
)
end
end
module PostCreator
def self.call(headline, body)
Net::HTTP.post(
URI("https://api.example.com/posts"),
{ headline: headline, body: body }.to_json,
"Content-Type" => "application/json",
)
end
end
Yes, this is a trivial example, but I've lost count of how many times I've suffered through reading classes with exactly this shape. Classes without any meaningful use of state, forcing instantiation ceremony without any benefit. If we subscribe to the idea that simplicity is a virtue in software design, one of these is obviously simpler than the other.
Some concrete benefits of modules with functions over classes include:
- Easier to grep for usage: this is more pronounced for objects with more than a single method, but with the class style, you can only meaningfully grep for mentions of the PostCreator class, not for the generic call method on it.
- Less indirection: you don't have to jump between initializer and instance methods to follow the flow of data.
- Less accidental confusion: PostCreator defines reader methods for headline and body, so now you have to ask yourself whether those might be called from somewhere else, and even if they were defined as private, you'd still have the indirection of finding their definition to be sure of that.
- Easier to stub in tests: you can just stub the module method directly, without having to go through the rigmarole of mocking an instance of a class.
- More performant: no need to instantiate an object just to call a method.
Final disclaimer: I personally would not use call as a method name, whether I wrote a class or a module, but since that's what many people do with "service objects" it seemed like an apt example.
Further reading watching
I recommend watching Dave Thomas's recent talk Start writing Ruby (stop using classes) from The San Francisco Ruby Conference 2025. The talk strongly advocates for this idea and similar adjecent ones.
While I don't fully agree with all the points made in the talk, directionally, I think it has the right spirit. It was also pretty great to see Dave back on stage talking about Ruby after such a long time!
History
React added functional components in React 0.14, released in 2015.
React 16.8, released in 2019, introduced hooks, which allowed functional components to have state and lifecycle methods.
Ruby has had modules and static method support since its inception.