loading...

How I (re)learned OOP is not a silver bullet

alexm77 profile image Alex Mateescu ・7 min read
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 ;)

The lesson

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 ;)

Discussion

pic
Editor guide
Collapse
polentino911 profile image
Diego Casella

I had more or less the same path as yours (Turbo Pascal & Z80 assembler in high school, Java (and some matlab) in University, C++ on my own spare time, now Scala) and I can understand your epiphany.
Design patterns are useful, but in some scenario they're just too overkill :)
Also, if you're going fully functional, I recommend this video
youtube.com/watch?v=lZG74WbnhoE

Collapse
alexm77 profile image
Alex Mateescu Author

I'm not advocating going fully functional (or fully anything really). All I'm saying is we should look at the requirements first and then try to find a way of getting them implemented with as few abstractions and translation layers as possible.
And thanks for the link, I wasn't familiar with that (the presentation, not GoF, obviously ;) ).

Collapse
nssimeonov profile image
Templar++

I totally agree with you.

My philosophy is, that code should be simple, so anyone can understand it easily and do modifications over the years. We should design our software so that it can keep running and evolve over decades. I have a system still in use almost 22 years now and it's really obvious where the design was good and it was easy to adapt to business changes and what takes too much effort even for very small modifications.

OOP definitely helps, but if you do too much classes and inheritance, you can easily get lost and confused. Some design patterns may help in some ways, but if I have to name one, that I hate - it's dependency injection.

Collapse
alexm77 profile image
Alex Mateescu Author

Consider yourself lucky then. In 15 years I haven't seen one application that has held up well for 5 years, let alone 22.

Collapse
nssimeonov profile image
Templar++

I remember reading an article over 10 years ago on TheDailyWTF, where Alex Papadimoulis was sharing something, that made quite and impression to me - our industry is failing to recognise the failure. We usually declare a project successful when we deliver it and get paid for it. However what we do is similar to building machines. Take cars for example - they are built to last and to be safe. Two aspects we sort of neglect when building software. I know we try, but compared to the car industry - our attempts are such a joke.

What Alex said in the article was, that a project can be considered if people still use it decades later. And if not - we should be rather consider this project a failure, not anything else. Take for example Windows XP - people loved it and used as long as they could. Some systems, written in COBOL, that sadly are still in use are older than me and I don't think anyone can say, that I'm young with all my gray hair. These systems are a massive success.

Note, that I'm not saying, that these systems never got patched or modified in any way. A successful project evolves over the years and people keep maintaining and enhancing it, but in it's roots it's the same project, based on the original design and architecture.

Thread Thread
alexm77 profile image
Alex Mateescu Author

I'm not sure all projects are meant to live a decade or more. I think a better metric is whether you can patch/update a project over its lifetime (whatever that is) without costs skyrocketing or without having that "oh crap, all the original developers have left the team so now no one knows the code anymore!" moments.

Sadly, what I have noticed is that everyone knows how to write good code, everyone can criticize for hours past projects they have worked on on how poorly written the code was, yet when push comes to shove the first thing that get thrown under the bus is quality.

Collapse
nssimeonov profile image
Collapse
martinhaeusler profile image
Martin Häusler

You do have some points. Writing a webapp server in Spring Boot doesn't really feel like using OOP. However, OOP was essential to build the framework to begin with. I like Kotlin; it offers a bunch of functional-style APIs out of the box, but also has the sane OOP principles we know from Java. I also agree that this is not neccessarily a language issue. It is totally possible to write code in Java which isn't object-oriented at all. What makes me feel a little uncomfortable with languages like Go is that they don't support things like inheritance in case that I need them. While I'm still very enthusiastic about OOP, I find myself using a lot of functional APIs too.

Collapse
alexm77 profile image
Alex Mateescu Author

Remember, inheritance in Java (and probably other languages) is achieved by composition - hence my perpetual confusion when I see the "composition over inheritance" argument. Just add the "super" structure as a field in your "derived" structure in Go and voila! instant inheritance. Well, not really, but close enough for a lot of cases I imagine.

Collapse
martinhaeusler profile image
Martin Häusler

Well, that's the JavaScript way of understanding OOP. Except that JavaScript at least traverses the super-chain for you in search for a property or method. Here you would have to do it manually. Doing polymorphic dispatches is also not really possible without language support. I don't want to imply that Go is a bad language, it just removes one of my most familiar tools from the toolbox, which makes me feel uneasy.

Thread Thread
alexm77 profile image
Alex Mateescu Author

What can I say, life begins where your comfort zone ends ;)