published 2024-11-03

How I wasted a decade of my career with OOP

Software architecture is hard

Do you think that good software architecture is difficult? Do you spend a significant amount of time refactoring your code just to keep it from becoming spaghetti code? Did you consider skill issues might be the reason you're struggling to make your architecture robust?

I faced similar challenges in the past. Recently, however, I had an epiphany that changed my approach, and I'd like to share the journey with you.

The dark ages of Java

When I started studying computer science in 2006, Java had just received support for generics. The language was at its peak of popularity (it is really hard to get reliable numbers from a time before Github and Stackoverflow, but it felt like this to me). I was happy to have the opportunity to learn it.

When Google launched the first commercial Android device in 2008, I was even happier to have made the right choice. Now I could use all of my newly aquired knowledge to write apps for smartphones, not just enterprise business applications, in my favorite language.

Object Oriented Programming (OOP) was everywhere around me, and Java made it unavoidable. You can not even start a writing program without defining a class first. So I went all in on it. I tried to approach every problem with object oriented patterns, and I tried to get really good at it.

Something was off

Fast forward over a decade. It is 2020, and I just became the team lead for backend and app development in my company. I made architectural decisions on a daily basis. I also had to explain them to my team and stick with them.

All our codebase was built in an Object Oriented (OO) style. I had progressed from Java to Kotlin already quite some time ago, but the underlying principles were still the same. Just with additional null-safety and nicer syntax. There were quite SOLID (haha) rules how to create new code and the team was well aligned. Often, we had to do refactorings to accommodate new features, but it all seemed normal to me. I considered myself as being quite good in producing maintainable code.

But from time to time I had to make exceptions from OO design. In some places, OO patterns turned out to not work well. After fixing design issues in my code, I was often left with data classes and static functions. I solved the problems, but the code was not object oriented anymore.

I could not explain why, but looking at certain structures, it just worked better to not bundle data and code. It created less code, was easier to understand, and just as well to maintain. But it felt wrong. I felt like I failed as an architect, because I couldn't solve all problems in a "clean" OO way.

What was even worse: I could not explain to myself or anyone on my team why and when it was ok not to use OOP. Seeing a codebase evolve over more than 5 years gave me the intuition in which places it was better to disregard OO design. But I couldn't nail it down to a set of rules. There was no advice, no literature, that I had read so far, when it was ok to make those exceptions.

I fell more and more into a cognitive dissonance. I preached OOP. But I reached for plain functions and simple structs more and more. Was I an OOP hypocrite? Or was I just not a good enough architect?

Functional Enlightenment

In 2022, there was a Humble Tech Book Bundle about Elixir. I dabble around in new languages all the time, and I had heard good things about Elixir before. So I took the opportunity and started learning Elixir.

Elixir was not the first functional language that I learned. I had done a course on Scala and played around with Haskell before, but never had used them in a real project. Funnily enough, the first programming language I learned in university was not an object oriented, but a functional one. But it never really clicked with them.

It was ingrained in me that functional programming (FP) is inferior to OOP. I learned that in university. This belief was never challenged during my career. Even the people who recommended me to try FP, did not convince me that it was an alternative for serious projects. Too high was the risk to get stuck with FP, and to miss out on good architecture without OOP.

But my experience with Elixir changed those beliefs. My programs did not turn out to be a mess, just because they didn't follow the "Clean Code" bible. On the contrary, everything felt more straight forward, and easier to structure. I did not have to think about how to encapsulate data in classes anymore. Deciding how to group data and how to group functions on their own is one order of magnitude easier than having to lump both together. Data can be just data, and logic doesn't have to wrap it.

Not having mutability took some time getting used to, but it was never the blocker I always thought it would be. On the contrary, it makes a lot of problems easy that I thought were inherently complicated. Getting concurrency right required rigorous mental effort before, but became effortless with message passing instead of sharing memory.

Overcoming OOP

Not having learned anything about functional architecture in university, I was trying to gain an understanding how to structure software without OOP. In doing so, I discovered more and more people talking about the exact same problems I had in OOP.

In his talk about Functional architecture, Mark Seemann points out, how hard it is to create a stable OO architecture, in contrast to when using a functional approach:

Object-oriented architects and developers have, over the years, learned many hard lessons about successfully designing systems with object-oriented programming. This has led to a plethora of ‘best practices’ that are painfully passed on from older to younger generations via books, lectures, consulting, blog posts, etc.

Many of these ‘best practices’ must be explicitly taught, because they don’t evolve naturally from object-oriented programming. Surprisingly, many of these hard-won ‘best practices’ fall naturally into place when applying functional programming.

Gaining more confidence in writing functional code, it slowly dawned on me. My problem was not that I am a bad programmer. My actual issue was that I defaulted to solving all problems in an Object Oriented manner. I was indoctrinated to do so, and I dug myself deeper and deeper into this trap. Never did I consider that always reaching for OO design might lead to a bad solution. There always had to be a good OO solution. At least that was my thinking. Why would there even exist languages where everything is an object otherwise?

When I heard Bryan Hunter telling his story on Beam Radio, how he got out of the OOP trap, I could not sympathize more. It was the exact same journey that I took:

I suffered in that land more than I realized I was suffering at the time. It seemed kind of fun to begin with. And then after a while I realized […] we were solving all these OO problems that everyone was solving and we had all of the defects that Uncle Bob would talk about in his books. […] And we were like: "If we could just be better craftsmen, the next project would be perfect."

[…] I didn't know there was an alternative to living in the minefield.

I can not recommend more to just listen to the first 10 minutes of this podcast. It took me over a decade to see the world beyond the minefield that OOP is! And finally I know am not alone. Listen to it now, you can read the rest of my story later.

Resisting OOP

After coming to the realization that reaching for OOP as a default was my ultimate problem, I am now on a mission. A mission to get functional programming into a place where you don't have to justify yourself for doing it. Where OOP is not the default solution. Where you don't reach for OOP without contemplating other options. And where developers understand the benefits of functional programming.

It is heart-wrenching to hear experienced, honorable programmers like Phillip Hracek on Inside the Project say a sentence like:

I'm just going to use, normal, you know, object oriented programing.

Osa Gaius explains why it is so hard to resist OOP in the current world of software engineering. There are a lot of social factors that contribute to OOP still being the norm. But I think it is critical that we resist. Not just because we need more diversity in programming. In the long run, it will lead to a better foundation in software engineering. We can write more maintainable, easier understandable and less bug-ridden code if we just drop the notion that OOP is the norm, the default, for everything.

To be clear, I am not saying that OOP is always wrong. That it inevitably leads to bad design. There are cases where OOP might work just fine. I am saying that we have to stop treating OOP as normal. There is nothing that makes OOP inherently special, superior or more "normal" than any other paradigm.

There are certainly problems out there that match an object oriented solution well. But most problems that we solve on a daily basis do not ask for a "Dog" and "Cat" inherit from "Animal"-style solution. It is nice to have a model of the real world in your code, but in the end, we have to organize data and logic. And most of the time we don't deal with cats and dogs, but with SimpleBeanFactoryAwareAspectInstanceFactories instead. Most of the time we don't implement simulations. (And even when we do, an ECS might be the better approach.)

How to get there

I am still unlearning the patterns that I intuitively reach for, in favor of simpler, better solutions. Having the freedom to not reach for OOP out of sheer compulsion makes a world of difference already.

There is already some momentum in the industry shifting away from OOP to other paradigms (mostly thanks to Rust), but it still feels like there are huge reservations against FP. How can we get all the lost souls out of the minefield?

Start by using everything your language gives you. You don't have to wrap everything in a class. Separate data and logic. Use closures and higher-order functions to pass around logic. And don't be afraid of the bOOgeyman. You will be surprised how far this will get you, and how easy it will become to maintain your code.

Learn a functional language. Then learn why The Best OO Language is a Functional One. Then stop using and teaching OOP as the default solution to architecture. Stop spreading the idea that OOP is "simpler to reason about" than FP. You will notice when you do it.

If you don't trust me on this, let me tell you one last thing:

Even Robert Martin, the author of the "Clean Code" book, that has cursed a whole generation of programmers (including me), has moved on to Clojure, a functional language. Even though, for all the wrong reasons like its syntax. He also doesn't even mention OOP a single time in this article, just that:

the future is looking very functional to me.

Probably because he still makes a fortune from people wandering around in the minefield.