Stop Abusing Classes
I've come to realize that many of the problems with modern code stem from the overuse of classes and objects.
Are you programming using classes and objects? If so, I suspect you're making your life more complicated than it could be.
Many of us have accepted as gospel that object-oriented programming is the one true way of structuring our code. And, as the faithful have done throughout time, we then become zealous, evangelizing our beliefs and discounting or ignoring evidence that indicates that maybe our version of the truth is not as universal as we believe.
Let me start by nailing a code example to the church door.
The Rails Service Object (or What Not To Do…)
The Rails philosophy is fat model/skinny controller (which itself should be evidence of the misuse of OO). So, to keep the controllers small, the convention is to move logic out into so-called service object.
Maybe we have a controller with a create method:
Too much logic: create a service object (in a separate file)
The controller then becomes:
Ugh, people think. That looks messy. Let’s metaprogram away the object creation. So, yet another file (although this one is shared between all service objects):
Our services are then subclasses of ApplicationService
and our controller becomes
We sit back, glowing with satisfaction that we have come up with a class based solution to our problem. All Hail Eris!
Wrong On So Many Levels
Let me start by promising that I’m not picking on Rails here. This kind of thinking (and this kind of code) is easy to find just about everywhere that classes can be found. It’s just that the Rails Service object pattern illustrates the issue nicely.
We’ll start with our original service object:
What does this do? The intent is to wrap a chunk of functionality (the inside of the call
method) inside an object. The method takes a parameter (a message), but we pass it indirectly; it is actually passed to the constructor, which saves it in an instance variable, which is then accessed when call
is invoked.
Stop for a second and look at this. How can anyone, at any time, think that this is a reasonable thing to do?
This is just plain wrong on so many levels.
The only reason that the object is created is so that we store away the parameter, and later use it in
call
. Having constructed the object, all we can do with it is:invoke the
call
method, andaccess the value of
message
This is a one-shot class: the only use for it is to create an object, then immediately invoke
call
, and then wait for the object to get garbage collected.The method we invoke is named
call
simply because that’s a Ruby interface that makes our object callable: we can invoke it using a couple of shortcuts. But we never use that capability; we just invokecall
directly.We make
message
a readable attribute, but we never use it outside the class.
Then look inside the first variant of our controller:
How can this code not set alarm bells ringing?
we create an object and never pass it to anything: it exists solely to execute a side effect.
we pass a parameter to the constructor, and no parameter to
call
. Can this ever be reasonable?
Then we have our triumphant bit of metaprogramming:
This exists solely to hide our embarrassment at writing .new.call
in the controller. Again, this should be a major red flag.
OK, Smart Ass. What Should it Be?
No idea, because I don’t know your app. But a good starting place might be:
What we wanted was a function we could call from our controller, so why not just write a function? Put it in a module so that it’s namespaced, and we’re done.
Clean, understandable, and no pointless classes.
One More Issue
There’s another misuse of classes and objects here which is even more insidious:
A controller in Rails is basically a collection of handlers for incoming requests. Each handler is a method that is called with the incoming HTTP request and which should return the HTTP response. And, underneath it all, HTTP is a stateless protocol.
Logically, then, using a class to hold a bucket of request handlers is just silly: namespace them in a module, and you’re done.
“But Dave…”
…you say.
“Rails controllers also have lots of magic variables and methods. You can access the request parameters, and the session, and call rendering, and….”
Sure you can. And Rails is just doing what everyone else does, with massive parent classes injecting everything and the kitchen sink into every child.
Again, I’m not picking on Rails: this is a universal problem.
We may misuse classes, but we totally abuse inheritance. Why should we use inheritance to perform magic when we can make it explicit?
Mea Culpa
Just in case you think I’m adopting a holier-than-thou attitude to other people’s code, I've been writing coding with objects and classes for over 40 years. I drank the Kool-Aid; classes were clearly the best way to model the world and construct our systems. I never really thought about it; it was so obviously correct.
Except it wasn't.
I started noticing things. My classes were getting to be massive; dozens of methods and twenty or more instance variables. I was misusing inheritance to propagate functionality. I was writing way too many stateless classes and creating way too many singleton objects. And I found myself adding methods to classes just so I could write function chains: a().b().c()...
.
My code was complex because I insisted on making everything an object. My designs were complex because I needed to think about classes at compile time as well as objects at run time.
(Any of this feel familiar?)
So I decided to experiment. But to justify the approach I took, I need to set the stage.
Maintain State or Transformation Data
C++, Java, TypeScript, and the rest all emphasize that classes are a data type, encapsulating and protecting state. And, because this is a good thing, we often dial it up to 11 by insisting that everything is an object.
As a result, when we program, we're thinking about maintaining state. We put our code into classes and run it in objects, because that's where the state is. And that leads us to ridiculous situations such as "service objects" in Rails, where a stateless class wraps a single method named `call`.
This is all in contrast to the style adopted by those writing in functional programming languages. These folks view programming as a way of transforming state, not maintaining it. State is something passed between functions and not hoarded in one place.
In the OO world, you'd convert a string to uppercase using something like str.toUpperCase()
. The functional crew would do something like upperCase str
(or even str |> upperCase
, which makes the transformation even more explicit).
The first difference between the two is that the OO case doesn't tell us whether the original string is converted to uppercase, or whether a copy is returned. In the functional case, upperCase
is a function that has no ability to modify its parameter, so we know the result will be a new string.
The second difference is more profound. In the OO model, the functions that act on strings are part of the string class. In the functional world, these functions are free-standing. They would typically declare than they took a parameter which was a string, but that would not be the only option; these functions could be made more generally applicable with no additional coding, as long as you had the concept of interfaces (or types). In TypeScript, you could have any of these:
Separating state and transformations gives you more options
The thing to take away is that classes will typically have to implement every method that acts on their state, and that can be a lot of methods. Functional approaches are more extensible, as the functions are implemented outside the state holder.
Neither OO Nor Functional
So I wondered what would happen if I tried to marry the two styles: use objects as state holders, but then move the functionality that used that state out of classes.
The short summary is that it has worked really well for me.
Here's an extract from something I wrote for the 2024 Advent of Code. The problem involved finding grid points between pairs of points.
The idea here is that Geometry::Point
and Geometry::Shape
are opaque types. Internally, they're objects, but they have no methods apart from their (read-only) x
and y
accessor functions. Code that uses this module receives values of these types, but has no idea what they contain. The only way to use them is via the methods inside module Geometry
.
"But, Dave!" I hear you cry. “That's almost a class definition.”
Indeed. But it's not a class definition. The Geometry
module has no state, and its methods do not use instance variables. The module provides the types and themethods, but the code that uses the module holds all the required state.
And this leads to code that's so much easier to work with. Compare a functional approach
with traditional OO
In the OO code, we need to define two classes, Point
and Slope
. Then, when it comes time to create an add_slope
method, we have to chose where it lives: it's all about slopes, so maybe it belongs in class Slope
. But it returns a Point
, so maybe it's in Point
. There's also some ambiguity reading p3 = p1.add_slope(s1)
. Does this change p1
, so that p1
and p3
both now reference an updated object? Or does it return a new point, so p1
and p3 are different?
The more functional approach has none of these issues, and really has almost no drawbacks: most of the time you just write func(p1,p2)
instead of p1.func(p2)
.
This is My New Coding Style
Over the last year or so, this has been my go-to approach for all new code.
I've been surprised at how effective it has been. My code feels cleaner, and I seem to have fewer dependencies. I no longer have all my methods squeezed into a class; instead, my classes are just value types, and I create lots of different modules that work with them. Just about the only code that I add to classes is to do with validation or serialization.
Use classes for data, and regular functions as mutators
It also works well with type-safe languages. In fact, I've found that its easier to write TypeScript in this style than it is using classes.
Your turn?
Object orientation is not the be all and end all of programming. In fact, over-using OO leads to many of the problems we see in today’s code. Objects are there for when you need to decorate state with some functionality that is specific to that state, but recognize that every time you do this, you are adding coupling to your code.
So, if you’re the kind of OO programmer I used to be, here’s a challenge. Try cutting back on classes; instead write functions that pass around state. Keep an eye out for things that are more difficult, and for things that become easier. Give it a month, and see whether this style works for you, too.
As always, let me know below.
Have fun!
Dave
Interested in your thoughts on using `extend self` over `module_function`
If I may first ask about something off topic, I found a PDF of Yourdon's 1978 Structured Programming book and searched it for "unit test" and I found four instances of the phrase but no description of what he wanted it to mean to his readers. Do you have any idea of what a unit test was at that time? I think that in the 80s I had a college class that used that book and I have no idea.
Returning to the topic, I think there is an OO programming heuristic that simply says "add another class". I thought came from Reil's OO Design Heuristics book but I can't find it. Many design patterns rely on the addition of new classes so, it isn't wrong but, is it in the general case? When I long ago mentioned the heuristic to a popular Java architect, he objected, "You can't just say that". I asked that if after he assisted a programmer with his code were there more or fewer classes in the resulting code and to his dismay, his answer agreed with the heuristic. Do you see any value in the heuristic?
This article reminds me that Alan Kay wished he had called it Message Oriented Programming.