OOP vs. Functional Programming — Which Is Better?

OOP vs. Functional Programming — Which Is Better?

The age-old question is finally answered. Kind of

Fernando Doglio's photo
Fernando Doglio
·May 3, 2022·

10 min read

Subscribe to my newsletter and never miss my upcoming articles

Which is better, PHP or Node.js? Or perhaps this one: Which is easier, frontend development or backend development?

As developers, we tend to ask these types of questions, trying to find an answer that doesn’t really exist. Which one is better, object-oriented programming or functional programming? That’s like asking which one is better, being able to see or being able to hear.

Let me solve the mystery quickly and simply: They’re both equally important, and it’s only you who can decide, for your particular context and set of needs, which one is actually better. Will your answer apply to me or anyone else in our industry? I hardly doubt it, so stick to “they’re both very useful,” and that’s it.

But the questions then remain: Which one do you choose, and how do you know which one is better for your project?

Let’s learn a bit about each paradigm and then understand how to make that choice.

What are OOP and FP?

Both object-oriented programming (OOP) and functional programming (FP) are programming paradigms. Essentially they’re ways of coding that determine the type of tools we’ll have at our disposal and how we’re going to structure the internal architecture of our code.

Each paradigm has its own set of constraints. Given the characteristics of each one and depending on what you’re trying to achieve, you’ll end up deciding on one or the other simply because they lend themselves better for different activities.

But whatever you decide to do, and if you’re going to get any type of learning out of this article, understand the following: These are standards that determine the way you write your code. They don’t limit you from doing anything. You can code any type of project with either of them.

So what’s the deal with OOP?

Object-oriented programming is one of the most common ways of writing code and, for some reason, one of the best-known paradigms out there. The moment you start learning to code anywhere, this one is usually covered.

The gist of it is that it tries to represent the real world through abstract constructions in your code. Essentially anything “real” about the problem you’re trying to solve can be represented as an object in OOP as well as anything abstract about it (making it easy to handle non-material concepts).

Let’s say you’re working on a fighting video game. Let’s imagine what kind of objects we’d have:

“Real” objects:

  • Every player will have their own object
  • The arena where they fight
  • Each fighter’s health
  • The timer measuring the fight’s duration

But there are also “abstract” objects that can be part of this OOP universe:

  • Probably each move will be encapsulated inside a different object
  • The level itself, if you’re playing solo
  • The AI that fights you and decides how to attack you

I can probably keep listing things here, but the point remains, you’re able to conceptualize simple and complex concepts as “objects.” What’s the benefit of it? That every object will be comprised by a state and by some behavior and it’ll all be represented as code within the same logic entity: a class.

Let’s break down that statement:

  • By state, I mean the set of variables that will represent how that object is doing at any given point in time. If we’re talking about the player, that might be its coordinates, the amount of health left, and the like.
  • And by behavior, I mean the actual code that “does things” related to it, for instance, moving the player around by modifying its coordinates or animating the player by showing different frames at any given time. These are all actions or behaviors grouped together because they have logical sense.

As part of the paradigm, these “classes” act as a blueprint for different copies of the same type of object. Essentially they let you have multiple versions of the same object by instantiating them one at a time. Imagine that our fighting game now is also multiplayer. Would you rather have the code we talked about duplicated several times for each player? Or would you rather have it written once and then behave individually for each player?

The second option would be the right answer, and OOP allows you to do that very easily.

Now if we wanted to go even deeper, within OOP, multiple tools and characteristics about it allow for even more dynamic behavior, such as:

  • Polymorphism, which allows for objects to take on different “shapes” (i.e., different sets of methods).
  • Encapsulation, which refers to how each object has its own state and behavior within itself.
  • Inheritance, which allows you to share state and behavior from one type of object to multiple others. It helps you represent a parent-child type of relationship with your objects.

Another interesting benefit of having our entities represented as objects in our code is that we can easily plot how they relate to each other on a type of diagram called a class diagram, which through UML (or Unified Modeling Language) allows us to capture how each entity relates to and uses others within our code.

There is a lot more about OOP that I just can’t cover in an article like this, but if you’re still learning about it, I’d recommend following the links I’ve left throughout the explanation because they’ll provide the extra details.

If you’re interested in picking up a language that strongly follows OOP principles, I would recommend going with either JAVA, C#, or even C++ if you’re into lower-level stuff.

What about functional programming?

Functional programming (FP), on the other side, focuses on behavior alone, in other words: functions.

While OOP has the concept of classes and methods, and complex ways of dealing with these structures, functional programming tries to simplify all of this into one basic concept: functions.

Functions in FP are, as you’ve probably guessed by the name, very important. In fact, if you’re coming from a purely OOP language, you’d be surprised to know that functions in FP are treated as first-class citizens, which means they can be used as values as well.

While in OOP and other paradigms you’d use methods (i.e., functions) to abstract a piece of logic behavior that can then be called upon, in FP you can also take that behavior and pass it around as a value (i.e., as if we were talking about a string or a number) or return it as a result of another function’s execution. That is wild but also very helpful if you think about it. It allows you to compose functions together, which is sort of the main way of working with FP.

Since you don’t really have to worry about state or entities with FP, you worry about behavior and distilling it into basic actions that can be composed into more complex ones.

The bad rep that FP has compared to OOP is because FP is taught in academia, mostly under very pure and restrictive conditions. What do I mean by this? FP was born and evolved from Lambda calculus, a formal system used in mathematical logic for expressing computations based on logic expressions. That sounds way too theoretical and like it has very little application to our day-to-day tasks. However, that’s not what FP is, and to be honest, if it was taught from a more practical point of view, a lot more developers would know about it and they’d be using it more often.

Just like OOP is a fantastic approach for representing scenarios where there are multiple entities (whether they’re real or abstract) involved, functional programming is an incredible paradigm to use when behavior is involved.

Imagine having to work on a data-flow solution that needs to capture data, clean it up, translate it (i.e., change its format), and then save it. These are all transformations that you can implement and represent as functions without having to worry about entities, their relationships, states, or behaviors.

function captureData(source) {  
  //...capture data  
  return data;  
}

function cleanUpData(rawData) {  
  //...clean it up  
  return cleanData;  
}

function translate(cleanData) {   
   //...apply transformations  
  return newDataFormat;  
}

function saveData(data) {  
   //...save the data  
   return data;  
}

These are all simple functions that take an input, perform some actions around it, and then return something else. There are no classes, objects, inheritance, or any other extra abstraction needed here. Just functions. Now notice how simple their signatures are and that, given we’re talking FP here, we can do things like composing them:

saveData(translate(cleanUpData(captureData(source))))

This looks a lot like composing mathematical functions, doesn’t it?

F(G(Z(x)))

First call Z with x, and then the resulting value will be used as input for G, which in turn will yield a result that will be used as input for F. The same goes for our functions. Start with captureData, which returns the raw data that later is used by cleanUpData. The returned clean data is then used as an input by translate, to finally be returned and used by saveData.

The benefit of this approach is that it’s very declarative given we haven’t really used any low-level abstraction (such as a FOR loop or variables or anything really) to represent the data flow. We’re simply stating what has to happen with our data, and we’re letting the environment (running our code) handle the rest.

The main concept to understand when trying to understand FP is that of pure functions or rather, functions that have no side effects (both in memory and I/O). This means that every time you call a pure function with the same input, you should expect the same result. If the function is somehow updating global variables or saving different data into a file, then that function is no longer pure and you should consider trying to break it down and distill its behavior.

Other interesting FP techniques include:

  • Currying functions, which allow you to create subfunctions that have predefined parameters.
  • Closures, which can act as a snapshot of a function’s scope at any given time. These are very useful when returning functions from other functions’ execution.

You can work on any type of problem using FP. There are FP frameworks for everything, from web development to data processing. It’s like I said at the beginning: The paradigm is not going to limit your possibilities.

That being said, folks coming from OOP have a really hard time realizing that at first, and thus they label FP as a learning toy and move on to “bigger and better” options. That is a huge mistake, and hopefully this article helps shed some light on that error.

If you’re interested in learning a bit more about FP, try going for languages such as Scala, Lisp, or Erlang.


If you liked what you’ve read so far, consider subscribing to my FREE newsletter “The rambling of an old developer” and get regular advice about the IT industry directly in your inbox


Wait, so which one should I be using then?

Short answer: both, either, I don’t care.

Really, for a richer experience, I would suggest both, given they’re such different approaches to solving the same problems (potentially). This is fantastic from a learning perspective because:

  • You’ll learn about tools and methodologies that the other wouldn’t teach you.
  • In practice, you’ll find a lot of languages accept a mixture of both paradigms (such as JavaScript or Python, to some extent). Knowing how to use both will help you make the most of those languages.

So instead of focusing on one paradigm over the other, my suggestion would be:

  • Focus on abstraction, functionality, and your goals. The problem that you’re trying to solve should determine the paradigm you’ll use, not the other way around.
  • Remember that the paradigm, much like the language, is just a tool. You can do the same with OOP and FP if you’re skilled enough in either of them, so why focus on the paradigm? Instead try to use the best tool for each situation.

Finally, consider the opposite: What if you decide you only want to learn and work with OOP? That’s great; if you explore it enough, you’ll become a great OOP master. But for every problem you find along the way, you’ll try to force a set of objects into it. Will it work? Yes, absolutely. Could there be an easier alternative in some of those situations? Yes, absolutely, but you’ll miss it because you’re only looking at it through your OOP glasses.

In programming there is no one solution or one approach to tackle every problem — we all know that. Then why spend time and energy on being a purist? This might sound crazy, but given enough time, you’ll find problems that will require a lot more working time to solve by applying the wrong paradigm than they would if you adapted, learned a new way of working, and used it to solve the problem.

You’ll come out of that experience a better programmer. Trust me, even if it’s just for fun, pick up a new paradigm, learn about a new language — it’ll help you improve the way you code with your main technology.

Let me ask you then: Are you a purist? Or are you willing to apply different paradigms for different problems? Share your experience in the comments!

Did you find this article valuable?

Support Fernando Doglio by becoming a sponsor. Any amount is appreciated!

See recent sponsors Learn more about Hashnode Sponsors
 
Share this