If you’ve spent much time in online tech forums or following technical folks on Twitter, you’ve probably heard an ongoing debate about the relative merits of Object-Oriented Programming (OOP) and Functional Programming (FP). What are they and how do they differ? Is one a better choice than the other?
I’ll answer the last question first. Is one a better choice than the other? It depends on what your program is trying to do. To make that decision we’ll have to dig deeper. Let’s start with some Wikipedia definitions:
Object-oriented programming (OOP) is a programming paradigm based on the concept of "objects", which are data structures that contain data, in the form of fields, often known as attributes; and code, in the form of procedures, often known as methods -- Wikipedia https://en.wikipedia.org/wiki/Object-oriented_programming
Functional programming is a programming paradigm, a style of building the structure and elements of computer programs, that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data -- Wikipedia https://en.wikipedia.org/wiki/Functional_programming
These are acceptable descriptions if you already understand the concepts they’re describing, but not helpful if you don’t. Let’s see if we can do better.
Both OOP and FP have the shared goal of creating understandable, flexible programs that are free of bugs. But they have two different approaches for how to best create those programs.
In all programs, there are two primary components: the data (the stuff a program knows) and the behaviors (the stuff a program can do to/with that data). OOP says that bringing together data and its associated behavior in a single location (called an “object”) makes it easier to understand how a program works. FP says that data and behavior are distinctively different things and should be kept separate for clarity.
Let’s look at some examples of the two approaches. I’m using Ruby because it natively supports both styles and is easy to read for non-rubyists, but other languages also support both, such as JavaScript and Scala.
Let’s say you run a company and you’ve just decided to give all your employees a $10,000.00 raise. (If this sort of thing happens often at your company, let me know when you’re hiring) How could we write a command-line script to make this change?
You most likely have all your employee records in a database with two attributes: the employee’s name and a current salary. In our examples, we’ll ignore the part where you transfer the data to and from the database. Let’s just focus on the “giving a raise” feature.
Here’s one approach using OOP:
class Employee
def initialize(name, salary)
@name = name
@salary = salary
end
def change_salary(amt)
@salary = @salary + amt
end
def description
"#{@name} makes #{@salary}"
end
end
This is our class, which we’ll use to generate new Employee objects. Most OOP languages use classes to generate objects in the same way a cook uses recipes to create meals -- the classes tell us how to construct the object, how it should behave, what it looks like, etc.
The @-sign before a variable makes it an “instance” variable: a variable that lives inside the object (aka instance) and can be used in its methods without having been passed in as an argument, the way change_salary uses @salary. The initialize method takes the name and salary we passed to new and stores them in the @name and @salary instance variables.
All the methods except for initialize are “instance methods” which also live inside the object (aka instance) and can be called on the object itself.
Now we’ll generate some objects to work with.
employees = [
Employee.new("Bob", 100000.0),
Employee.new("Jane", 125000.0)
]
Each Employee.new(...) call creates an object with data (@name and @salary) and behavior (change_salary and description). We call new twice, which gives us a pair of objects representing Bob and Jane stored in an array we are calling “employees”. In a normal app, this array of objects will typically be returned from the database directly by a tool such as ActiveRecord.
Now let’s give out those raises.
employees.each do |emp|
emp.change_salary(10000.0)
end
We call the each method on our array of employees, which hands us each employee in turn and stores it in a local variable called emp. We call the “change_salary” method we defined in our class, passing in a value of 10000.0. This adds our $10K to the employee’s salary and stores the sum in @salary, overwriting the existing value.
employees.each do |emp|
puts emp.description
end
Finally we can generate some output to make sure the results are what we intended, using the description method to build our output string instead of trying to access the data fields (@salary and @name) directly. This is called “data hiding” and it allows us to change the names of our instance variables without forcing the users of our object to change how they use it.
We use each again to output the results. This time, we use puts (put string) and call the description method for each of our objects to print their description, as we defined in our class.
There are several important things to notice about the OOP implementation. Data is supplied to an object at the time the object is created (when we called the ‘new’ method). Then we use methods on that object (change_salary and description) to interact with data that we’ve already stored. This gives us a good place to put all the behaviors that are associated with that object type, making them easy to find if we’re unfamiliar with the codebase or refreshing our memory after some time away. The names of these methods document object’s behaviors , which also helps us familiarize ourselves with the code. The object is an obvious location to add behaviors as our project becomes more complex.
Let’s implement this task using an FP approach.
employees = [
[ "Bob", 100000.0 ],
[ "Jane", 125000.0 ]
]
This time our data structure is an array of arrays instead of an array of objects containing the values. FP prefers to keep data in plain arrays and/or hashes and not “complicate” data by mixing it with behavior.
Instead of converting the data to an object and then calling methods on it, we write a pair of standalone methods called change_salaries (plural) and change_salary (singular). We pass change_salaries two arguments: the array of arrays representing our data and the change amount. change_salaries uses map instead of the each method we used in the OOP version. We’ll discuss map in a moment.
FP leans very heavily on tiny methods that do one small part of a larger job, delegating the details to other tiny methods. This combining of small methods into a larger task is call “composition”. In our example, change_salaries has a single job: call change_salary for each employee in the employees array and return those values as a new array. change_salary also has one job: return a copy of a single employee with the salary field updated. change_salaries delegates the calculation of the new salary to change_salary, allowing change_salaries to focus entirely on handling the set of employees and change_salary to focus on updating a single employee. A nice side-effect of this approach is if we had a single employee we wanted to change the salary for, we could call change_salary directly! Composition isn’t unique to FP -- OOP uses it as well -- but it is a cornerstone of the FP mindset.
happier_employees = change_salaries(employees, 10000.0)
Because we don’t have objects, change_salaries requires that we pass in not just the amount, but also the data we want to modify. This method returns the updated dataset, which we store in the happier_employees variable.
happier_employees.each do |emp|
puts "#{emp[0]} makes #{emp[1]}"
end
Finally, we use ‘each’ to walk through every record in happier_employees and generate the output message ourselves. This fits the FP model because FP likes to view everything as a data transformation: you start with this dataset, apply these transformations to it (in this case, adding $10K) and generate a new dataset.
Something you’ll notice right off is there are fewer lines of code, mainly because we don’t build the class for creating our objects.
Another subtle -- but important -- difference is that in the OOP version change_salary used each to process each employee, but the FP version uses map. Instead of changing the original value, ’map’ creates a copy of the array containing the return value of each pass. When all elements have been processed, map hands us the copy with all the new values, which we store in happier_employees. The original array is untouched!
This idea of not changing the contents (or “state”) of a variable once it’s been created is called immutability and is another key aspect of FP. In the OOP version, the contents of the employees array changes over time: before change_salary is applied the salaries are 100K and 125K, but afterwards they are 110K and 135K! This means we don’t always know which value we’ll have at any given point in the program: we have to walk through the program flow and find out if change_salary has been called or not. This can be difficult as the program increases in complexity. The FP version avoids this because employees represents the “before” state and happier_employees represents the “after” state. Their values are always the same over their entire existence.
So which approach should you choose for your next project?
Michael Fogus, author of “Functional JavaScript”, suggests in his blog post “FP vs OO, from the trenches” (http://blog.fogus.me/2013/07/22/fp-vs-oo-from-the-trenches/) that when he deals with data about people, FP works well, but when he tries to simulate people, OOP works well. Our example is “data about people” -- since we’re changing the salary directly -- so the FP version is shorter and simpler. If we described a feature like “employee requests time off” that requires a more-complicated interaction with the data -- likely creating a “time-off request” object attached to the employee -- then OOP might be a better fit. Hardcore OOP or FP developers could explain why their approach is better in either situation, which makes this debate so interesting. Read the comments on the post for an example of the dialogue that surrounds OOP and FP.
If you’re on a tight schedule and are already familiar with one approach or the other, stick with what you know. If it’s a personal project or you have some flexibility with deadlines, it might be worth trying each approach for a feature and seeing which results in a more pleasing, clear solution. We’ve only scratched the surface, and the problems you’re solving may look nothing like the trivial one I used in my example. Explore both and use your best judgement.