8 min read

Moving from functional to object-oriented programming

Switching a programming language within the object-oriented world or the functional world is merely a change of using new syntax and new libraries that do the same thing.


Although these two styles (object-oriented and functional programming) both accomplish the same thing by computation, the way they approach the problem requires a very different mindset. In order to be consistent and make our code manageable, we follow the mindset of whichever style in which we are coding.

Writing code that is easy to maintain is an art.

The problem

The problem arises when there is either a professional demand or personal choice to move from one mindset of programming to another. The codebase ends up with a mixture of functional and object-oriented code, which is complicated and costly to maintain. It is also difficult to develop a new feature in this case. At OnceHub we experienced this problem when we had some procedural/functional code in our supposedly object-oriented microservice.

That object-oriented microservice is in Ruby (Rails API), so the examples I will be using here will also be in Ruby, but the concepts are easily applicable to any object-oriented language (for example, TypeScript/Python).

We will not get into the concepts of object-oriented programming or how to write practical object-oriented code (for that we will have another blog post); rather, we will review the equivalent concepts and contrasts of functional programming in the object-oriented world.

Potential solution

The hardest part: Data and behavior together

Object-oriented world: 

  • Here we think about everything as if it is a real-world object and has some behavior. Now in technical terms, an object is nothing but a structure and its behavior is just the methods it has. But in functional programming, we have data and functions as different entities. So this is a major difference. 

  • In a good object-oriented world, every object does one thing and knows as little about other objects as possible, so there are few interdependencies among them. This is similar to having functions with no side effects in functional programming and depending only on their inputs, like a pure function. 

  • Objects' main focus is on their behavior and not on data, so data is mutable, unlike immutable data in functional programming.

To highlight how different the approaches are in OOP and FP, I’ve borrowed this example below:

You run a company and you just decided to give all your employees a $10,000 raise. How would you tackle this situation programmatically?

In OOP:

  1. Create Employee class that initializes with name and salary, and has a change salary instance method
  2. Create instances of employees
  3. Use each method to change the salary attribute of employees by +10,000

In FP:

  1. Create employees array, which is an array of arrays with name and corresponding salary
  2. Create a change_salary function that returns a copy of a single employee with the salary field updated
  3. Create a change_salaries function that maps through the employee array and delegates the calculation of the new salary to change_salary
  4. Use both methods to create a new dataset, named ‘happier employees’

Now we have an idea about how these two are different, let’s see how to make our codebase more object-oriented from a mixed-style (OOP + FP) codebase. The following are some of the problems we faced and how we solved them.

Single Responsibility Principle

This sounds very easy but takes effort to implement. If you’re coming from a functional programming background, I am assuming that you know about it.

We had a class whose primary function was to send some data, which gets used for initializing components in a front-end service. Now as the functionality increased, we began to have more to initialize; one of which was to send a welcome message to that front-end service. The logic of calculating that required at least three good functions, which could also be used in other classes, like when making the JSON response. 

The test cases of this class were not fully testing the logic of making the initialized data. They just checked that the method was getting called and then partially compared the response, in which we didn’t check the welcome messages. It was done for one simple reason: the member function was doing a lot of things and no one wanted to touch it. Problems it created:

  • No test for actual logic of creating the initialize data because no one wanted to touch that big piece of code.
  • Very hard to change or add a new feature in that code.
  • Overall -> Very costly in development

How did we handle it?

We extracted out the code for making welcome messages in a helper class, and now we just include it wherever we want. This helper class only takes the current session object and builds welcome messages based on its properties. 

# Initial Code

class InitializeFrontEnd                 # Demo name
def initialize(config)
  @config = config
end

def setup_data
  collection = Message.find_by(website_id: @config.website_id, is_welcome: true)
  # more filtering and condition on result based on config data
  messages = collection.collect {|question| <some conditions> }
  .
  . # (some other things on which I am not going into details)
 
  # Making JSON
  response = {
    welcome_messages: messages
    .
    .
  }
  render response
end

.
.
.
other methods
.
.
end

# After refactoring

class InitializeFrontEnd
include WelcomeMessage

def initialize(config)
  @config = config
  @welcome = WelcomeMessage.new(@config.current_session)
end

def setup_data
  response = {
    welcome_messages: @welcome.messages
  }
  render response
end
end

After this refactoring, we did introduce a dependency in our InitializedFrontEnd class, but we are fine with this tradeoff, as now the tests are easy and the heavy logic is lifted into a helper class that can have separate tests. 

Remember that a class should do the smallest possible useful thing. That thing ought to be simple to describe. If the simplest description you can devise uses the word “and”, the class likely has more than one responsibility. If it uses the word “or”, then the class has more than one responsibility and they aren’t even very related. -- Sandi Metz

Complex Data Structure

Depending on a complex data structure is worse. It is very hard for anyone to understand it and how to make a change in it. 

What is a complex data structure, you ask? Well, imagine we are taking an array of arrays as a parameter in a constructor of a class in a data variable called `data` and then using that like this, “data[0][1]”, in different methods of a class.

Direct references into complicated structures are confusing, because they obscure what the data really is, and they are a maintenance nightmare because every reference will need to be changed when the structure of the array changes.

Example: Consider a class Departmental Store that takes data as an argument in its constructor. This data is an array of arrays having the cost price of an item at the 0th index and tax on that item at the 1st index. 

(Here we are not taking the actual example of our microservice, as that is very complex)

# Initial Code

class Store
attr_reader :data
  def initialize(data)
  @data = data
end

def selling_prices
  data.collect{ |item| item[0] + item[1] }
end
end

# After refactoring

class Store
attr_reader :items

def initialize(data)
  @items = make_items(data)
end

def prices
  items.collect { |item| item.cost_price + item.tax}
end

Item = Struct.new(:cost_price, :tax)
def make_items(data)
  data.collect {|cell| Item.new(cell[0], cell[1]) }
end
end

The takeaway is ->

If you are compelled to take complex structure as input, then hide the mess even for yourself. 

Writing loosely coupled code

Every new class reference inside a class makes it dependent on the referenced class. The more we have it, the less it is available for reuse. 

We used to have methods in our classes that directly referred to another class. The problem with this is that we are hard coding a behavior to a specific kind of object. Rather, it is possible that other objects may also possess that behavior, and in that case, our class should work.

For example: Consider the InitializeFrontEndclass that generates a bill. Now it should be able to generate a bill of an item purchased or for a service taken by the customer.

If we have our Bill class like this: 

class Bill
attr_reader :price, :tax

def initialize(price, tax)
  @price = price
  @tax = tax
end

def generate
  total = Item.new(price, tax).market_value   # Only works for Item class
  "$ #{total}"
end
end

Even if there will be other classes like a `Service` class that will respond to `market_value` behavior, we are not able to use Bill#generate behavior for that. Thus, our object is not reusable. 

Instead, look at the following code:

class Bill
attr_reader :price, :tax, :any_service

def initialize(price, tax, any_service)
  @price = price
  @tax = tax
  @any_service = any_service
end

def generate
  total = any_service.market_value
  "$ #{total}"
end
end

We inject our dependency and expect to respond to `market_value` behavior in the “generate” function. 

The takeaway is ->

One object should not know much about any other objects/classes. Give it something and expect some behavior out of it. 

In the second part, we will look at creating flexible interfaces, reducing cost with duck typing, and cost-effective testing. 

These are the principles we are including in our daily development at OnceHub. To find more content like this, you can also follow my personal blog.

Happy growing. :)


Raounak Sharma, Full Stack Developer

Raounak loves helping a business grow through his technical contributions, working with startups and multi-national corporations. He enjoys the challenge of maintaining quality in software, especially as the industry changes with every market trend. He's worked as a Full Stack Developer at OnceHub since July 2019. In his free time, he plays football for clubs in Delhi, India and loves trekking, listening to audiobooks while doing cardio, and watching stand-up comedy.