Motto: “If the implementation is hard to explain, it's a bad idea.” - The Zen of Python
Me and my little epiphany
There I was, your typical Java programmer, trying not to fall behind the new trends. I was reading about new and future additions to JDK, trying to keep tabs on NoSQL solutions, keeping up with the myriad of Spring Framework modules, testing frameworks, build tools, the works. When I got to the “learn one new language each year” part, I went for Go. And I was immediately hooked: a language that goes back to basics when other languages seem dead serious about adding everything and the kitchen sink. Go seemed like a nice language, with a very rich standard library that could get a lot done in a short amount of time (I believe one of its goals is to be “as productive as Python”). A few toy apps later, I’m pretty convinced that is, indeed, the case. But then I had an “aha!” moment when I realized: Hey, this thing doesn’t have objects! How on Earth did I code my toy projects so fast? For what it’s worth, with Go you do get structs and you get to tie methods to those structs, which sounds awfully like an object. But you don’t get inheritance, a “protected” keyword or any intricacies like that.
Now, for a little bit of background, this was not, by far, my first step outside the OOP world. In fact I started programming in BASIC, I moved to Pascal and later to C (with a tiny bit of C++). I dabbled a little bit with ASM even. And then in college there was this OOP course that used Java for support. And it clicked. Things started falling into place and I had found a language that worked for me better than anything before it. I finished college, I got my first job doing Java and tried to get better at it ever since. Many years later I had a little run-in with Erlang and then Scala (which are significant because they has started the train of thought I am about to share with you). And then, when I got to Go, I finally put two and two together and got the “aha!” moment I have told you about.
We all have our ‘aha!’ moments, so what’s so special about this one? The answer is simple: when I stepped outside my box and looked at things in perspective, I have realized there are times when OOP instead of helping, does the exact opposite. Many times, depending on what you’re working on.
I realized in my decade and a half of experience, not one client has come to us to ask for a hierarchy of objects. Not one. What clients ask instead is “when I do this/give you that, I need the program to do that”. That sounds terribly similar to how you describe a program in a functional language. You will say, clients aren’t always tech-savvy, it’s not their job to know how the requested solution should look like. And that is fair enough. Let us look at the real world and examine some typical requirements and how we go about fulfilling them.
Looking at the real world
For the sake of the next few examples, we’ll assume we’re using Java and we are putting GoF patterns and SOLID principles to good use.
One of the most common type of applications you’ll be requested to develop is a web application. We won’t analyze step by step how we get from specification to implementation, but we all know where this ends up. It ends up with a series of services and a series of stuff that goes in a database. The services will be stateless (because stateful is trickier to deploy in a cluster), thus they’ll each be reduced to set of functions in a class with virtually no member variables. The persistable stuff on the other hand is all member variables and no functionality outside of getters and setters. We’ve applied the best practices of OOP… and we got the typical solution in a functional language: functions that simply handle data structures. Because your typical entity is no different from a map/dictionary.
Of course, the whole application is more complex than that. Some of its functionality comes from the application container it runs in. And that is not trivial functionality, it’s important stuff like transactions. Let’s look at transactions and see how the container enables them basically for free. To enable transactions, you simply annotate some method (in case you’re not doing Java/Jakarta EE, in which case you get transactions by default for designated methods) and voila! Instant transactions. Of course, in order to do this, the container needs to inject some generated code into the JVM, which can throw a monkey wrench in the JIT compiler. And we cannot make our classes final, which prevents a series of other optimizations. Now contrast this with a more functional friendly language where it is idiomatic to pass a function as a parameter to another function and you can have a runInTx(...) method that starts your transaction, invokes the function passed to it and then closes the transaction or rolls it back as needed. Arcane meets dead-simple.
Moving on, what if we are required to model a system that’s a little more stateful than your typical web application? Now we’re in OOP paradise: we have a series of components (objects), each with their own state (member fields) and specific actions (methods). Our objects are now proper objects, doing objects’ stuff. We have a bit of a problem when an event is to be processed, because we need to inspect several objects to determine the state we’re in which will lead to a bit of if..then..else statements in our code. But other than that our solution is perfectly fine. However (I bet by know you already knew that however was coming), a solution like this won’t scale to a more complex system. You won’t need to scale it, because the starting system is highly unlikely to grow significantly more complex over time in a real world situation. Still, for the sake of this story, let’s pretend we’re now being asked to write a piece of software that controls the International Space Station. Modelling every system on the station as an object might be doable, but trying to determine the state of the system will produce an (un)healthy dose of spaghetti code. If our language offers capable pattern matching we can handle this better, but even that approach will only take us so far. How do we move forward? We apply one of my favourite techniques: we go back to basics. And the basics say that when a FSM changes state, it simply applies a function on the current state of the system and the input event and moves the whole system into a new state. So if we do the right thing and encode our states and the input events, we simply create a sparse matrix of states and events, having the action to be taken in each cell. The action itself is probably fairly complex and will be made up of several smaller actions. But once again, we have shed all need for objects and we got ourselves a data structure and series of functions. Almost like a functional approach is stalking us ;)
Now, to be perfectly clear, the above is not meant to imply OOP is bad or that you should avoid it. I have picked a few instances of common requirements where the simple solution looks more like functional programming and less like OOP. But there are (parts of) projects where the solution may be more befitting of OOP. I believe graphics toolkits are a good example of that.
Acknowledging this, more recent general purpose languages (I’m thinking here Go or Rust) have included both OOP-oriented and functional-oriented traits. Even Java, where everything is an object, had iterators since forever and has worked on adding functional traits with streams and method references. (I have purposely chosen not to mention these in the examples above because I had no intention of comparing languages, but showing the pitfalls of one sided thinking instead.)
Why do I think this all matters? After all, try as you might, any software accumulates debt over time (I have no illusions about that), so why go through trouble to get it a little more clean in the beginning? Well, in my mind, this can help you avoid some (many?) of those awkward moments when the customers asks for a little change and you have to explain to them that because your implementation is so complex, that little change will take weeks or even months to deliver. The closer you can fit your solution to your customers requests, the fewer of these moments you’ll have to go through. And that goes a long way both towards building customer satisfaction and keeping the morale within your team up.
In closing, all I want to say is: just like a handyman comes to your door with his toolbox, so are we programmers going to our clients’ doors with our toolbox (made up of languages, programming techniques, knowledge of build tools and whatnot). And if we’re not that confident that handyman at the door that showed up with only a screwdriver and maybe a hammer will do a good job, we can be absolutely sure we won’t service our customers very well if we show up at their door with only one language and/or framework in our minds. Next time you’re the handyman, make sure your toolbox is packed with as many tools as it can fit ;)