Obeject-Oriented vs. Functional Programming
This is a stripped down version of Bill Gathen’s article, which is meant to highlight points that I’m most interested in. The original is here.
Programs have two primary components:
- Data - stuff a program knows
- Behaviors - stuff a program can do to/with the data
Object-Oriented Programming and Functional Programming use different approaches for how to best create understandable and flexible programs.
-
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.
Our examples will use Ruby to look at the two approaches – other languages also support both ,e.g., 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. 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. We’ll ignore the part where you transfer the data to and from the database, focusing on the “giving a raise” feature.
An 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
endThis 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 is
typically returned by the database.
Now let’s give out those raises.
employees.each do |emp|
emp.change_salary(10000.0)
endWe 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
endFinally 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.
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_salaryanddescription) to interact with our stored data -
We have a good place for the behaviors associated with our objects, making them easy to find if we’re unfamiliar with the code or refreshing our memory
-
Method names document object behavior, familiarizing us with the code
-
The object is an obvious location to add behaviors as complexity increases
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 for OOP.
FP leans very heavily on tiny methods that do one small part of a larger job, delegating the details to other tiny methods. Combining small methods into a larger task is called “composition”.
In our example…
-
change_salarieshas one job: callchange_salaryfor each employee in the employees array and return the values as a new array.change_salariesdelegates the calculation of the new salary tochange_salary, allowingchange_salariesto focus entirely on handling the set of employees. -
change_salaryalso has one job: return a copy of a single employee with the salary field updated.
A benefit of this approach is if we had a single employee we wanted to change the
salary for, we could call change_salary directly!
While OOP also uses it, composition 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 data, which we’ll store in happier_employees.
happier_employees.each do |emp|
puts "#{emp[0]} makes #{emp[1]}"
endFinally, we use ‘each’ to go through every record in happier_employees and
generate the output message. This fits the FP model because FP likes to view
everything as a data transformation: you start some data, apply transformations
(in this case, adding $10K) and generate a new data.
Notice there are fewer lines of code, mainly because we don’t build the class for creating our objects.
Another important thing to note 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!
The 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 our OOP example, the contents of the employees array changes over time:
- Before
change_salaryis applied the salaries are 100K and 125K - Afterwards salaries are 110K and 135K!
This means we won’t know which value we’ll have at any given point. The program
must walk through the flow to see if change_salary has been called, which can
be difficult as complexity increases.
- The FP version avoids this as
employeesrepresent the “before” state andhappier_employeesrepresents the “after” state. Their values never change.
Which one should you use?
Michael Fogus, author of “Functional JavaScript”, suggests a simple two-point guideline:
- When dealing with data about people, FP works well
- When trying to simulate people, OOP works well
Our example is “data about people” – since we’re changing the salary directly – so FP is shorter and simpler.
If we had a feature like “employee requests time off”, requiring a more complex interaction with the data – likely creating a “time-off request” object attached to the employee – then OOP might be a better fit.
Post written on 2015-06-29 00:00:00 -0700