Beliebte Suchanfragen
//

The best of both worlds: Harnessing the benefits of object-oriented and functional programming with FauxO

1.2.2023 | 7 minutes of reading time

Functional programming and OOP are often viewed as two separate paradigms in programming. And it is true that programming languages lean more towards one or the other, which influences how we are "supposed to" solve a problem in this language.

In this article, I want to share a pattern called "FauxO" that takes inspiration from both the functional and object-oriented paradigms to get the best of both worlds.

Note: I did not come up with this term. What's more, there are two notions of "FauxO" that I have encountered on the internet: One is from Gary Bernhardt's talk Boundaries, which is the one I am referring to in this post. The other is from Sandi Metz, who uses the term to describe OOP design gone wrong.

OOP

OOP models the world with objects that encapsulate state and behavior in one "thing". These objects communicate with each other through messages (= a method call with its parameters). Only the object itself may change its state and its public methods follow the "Tell, don't ask" principle: you tell what you want the object to do and it does it for you, often times returning a response. This is a very natural way to think about the world: When I hit an enemy with a sword, then that enemy's health is reduced. In other words, there is an object and that object is now changed/mutated in some way. To make sure that the mutations are allowed, an object's methods encapsulate logic to enforce invariants about its state. The purpose of encapsulation is to enforce invariants about the data's structure. Moreover, this encapsulation lets you change the implementation without changing the interface.

It's worth noting that Alan Kay, who coined the term "object-oriented", famously stated that he "didn't have C++ in mind" when he conceived the paradigm. In his view, "object-oriented" is more about message passing, like we would do with the Actor Model. Today, object-oriented languages give you more choices on how to do things, which is overwhelming for inexperienced developers, e.g. inheritance and abstract classes. Without proper experience, the number of design patterns and possibilities make it difficult to choose the right path.

This is not to say that all functional languages, which we'll look at shortly, are inherently better. It just means that some well-thought-out restrictions can guide an inexperienced developer to a better design.

Functional programming

Functional programming is (greatly over-simplified) about modeling the world with side-effect-free functions that operate on types rather than objects. These types can be thought of as data containers without any logic. Side-effect-free (or pure) functions, of course, cannot do anything useful by themselves, so many functional programming languages have some sort of special mechanism to perform side effects, like IO-Monads. Functional programming is not about getting rid of side effects. It is all about restricting (or at least making it obvious) where side effects can happen.

Next to pure functions, another important concept of functional programming is immutability. In functional programming, no value changes when a function is applied. Instead, the function returns a new value while the previous one is unchanged. When an enemy is hit with a sword, a new enemy is created that is almost the same as the previous enemy, but with less health. This is a very unnatural way to think about the world. It still yields some benefits:

  1. Predictability: Input parameters cannot be changed, so if you call a function enemy = strike(enemy, sword), you know without looking at the code of strike that the value for sword cannot have changed. Also, enemy is only changed because we replaced the value behind the variable. If we had a reference to the first value, e.g. if we had a statement enemy2 = enemy earlier, then the value behind the name enemy2 would still be the original value before the strike function was applied.
  2. Concurrency: Since data is immutable (i.e. read-only), concurrent access to a value is never a problem. Processes that do not share mutable state thus need to use another mechanism such as message-driven approaches (but remember that the benefits of concurrency are limited by how much code can be parallelized).

What about those invariants that objects in OOP had to make sure were enforced before and after every method invocation? Immutability can make sure a data structure stays in a valid state even after construction. This is done by enforcing the invariants at value-creation time; in the constructor, if you will. Since the data is immutable, the invariants always hold after this point. When an enemy is hit with a sword, a new enemy must be created and the constructor function can enforce all invariants again. This is described by a quote from Yaron Minsky:

Make illegal state unrepresentable

All values are always valid.1 If you have an enemy, you know that this enemy is in a valid state.

FauxO

FauxO transports the idea of pure functions and immutability into the OOP world. The goal is to have all invariants to be checked at object creation time and all objects to be immutable. Also, we want to hide internal implementation details and provide a stable interface for clients of our objects. This is best explained with an example, so here is some Java code:

1class Enemy {
2
3  ...
4  private final Health health;
5
6  public Enemy hit(Weapon weapon) {
7    var damage = calculateDamage(weapon);
8    // Handle other cases, e.g. health drops below zero
9    return new Enemy(this.health.subtract(damage), ...);
10  }
11}

Notice that the field of this object, health, is final and that the method returns a new object instead of mutating the existing one. In this way, we get the benefits of immutability from functional programming (namely predictability and concurrency) combined with OOP's idea of an object that encapsulates state and behavior.

The difference can be seen in the following table, which I shamelessly copied from Gary Bernhardt's talk Boundaries:

MutationData & Code
ProceduralYesSeparate
OOYesCombined
FunctionalNoSeparate
FauxONoCombined

One drawback of this approach is that it reduces composability compared to purely functional code. Functional programming is about composition and a pure, generic function can operate on many data structures. OOP and FauxO make composition more difficult because the methods of a class are now tied to one data structure, reducing reusability.

It is also not as "natural" as the OOP approach because we now get a modified version of the original "thing" back.

Why not just use a @With annotation?

Project Lombok has the @With annotation that generates methods exactly for this use case: creating a copy of a class with just one field changed. While this annotation does help to make immutable classes, it has a drawback. There is no behavioral logic in the generated methods. The class becomes an anemic domain model/anemic data container, which goes against the OOP idea to enforce invariants behind an encapsulation barrier.

But what about performance?

Performance has historically split programmers into two camps: Some programmers want to squeeze every ounce of performance out of their programs (C and C++) while others want to have a clear, mathematically provable and easy-to-understand model (written in LISP, Smalltalk, Haskell). The former group won the debate on performance in the past as functional programming used to be too impractical for software engineering, but it is not anymore. Garbage collection algorithms in languages like Java or the .NET platform clean up short-lived objects quite well. And new languages like Rust borrow ideas from functional programming while still producing extremely fast executables. Many concepts of FP have been widely adopted in non-functional languages.

We are in a situation where the problem is no longer how to squeeze the last ounce of performance out of computers, but how to manage the amount of complexity in software projects. FauxO and functional programming are no silver bullets, but they are tools that can reduce complexity by giving us guarantees and thus making reasoning about code easier.


[1]: How does this work when our software receives input from outside? We cannot assume the input is valid or that the user is even authorized. This is where a strong type system comes in. Switching examples, when we have to perform a validation check on a form, our validate function takes data of type UnvalidatedForm and returns data of type ValidatedForm | ValidationError[]. The clients of this function can immediately see that the result is either a validated form or a list of errors. All other functions that come afterwards in the process can declare ValidatedForm as the input type and do not have to do a pessimistic validation-check at the beginning. Even better, we get a compile-time error when we try to perform a process that requires the form to be validated. This can be extended even further, e.g. a function can declare to take an ApprovedForm, reducing the need to check whether a form is already approved at the beginning of a function. You can read more about this pattern in Scott Wlaschin's book Domain Modeling Made Functional.

share post

//

More articles in this subject area

Discover exciting further topics and let the codecentric world inspire you.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.