Functional Programming is preferable to Object-Oriented Programming

Joel Stevick
4 min readSep 27, 2021

--

Try to avoid object-oriented programming for other than property-binding and scoping data service APIs (which are intrinsic to the Angular paradigm).

  • Using component state yields code that is hard to reason about.
  • Moving from object-oriented programming (OOP) to functional programming (FP) will greatly improve the maintainability of our code base.

Assertion: FP will always give a better outcome than OOP when applied to the same problem.

TLDR;

“Object State + Behavior” = 👍

“Object State + Multi-step algorithms” = 👎

Let’s talk about OOP

One of the key principals of OOP is that data and behavior are bound together. Part of the motivation is to model real-world data and relationships. Presumably, having code that mirrors the subjects of your application is a good thing; which it is, if you are building smoke detectors, computerized automotive parts or other realtime controls.

OOP is a good fit for many use-cases

Suppose we are building embedded software for an a/c unit. It would make sense to create classes for the Thermostat, Fan, and Coolant system. Your software would implement an object for each component in the system; each with its own state and connections (event handlers) to other components. The logic contained in the various event handlers would operate on the state (e.g. temperature) maintained for that system component. You can imagine how this could work well and be very easy to maintain — it makes sense that “business logic” is coupled with state — after all, the modeled components do things, and they do things based on what they “know.”

OOP can be an application killer

Consider the case of a business application where the subjects are records in a database. Records don’t “do things”. Before and/or after a CRUD operation is performed for a record, business rules may need to be applied and actions taken (e.g. emails sent) that are scoped to many other records and types of records. Also, there are going to be use-cases that require your application to aggregate data across complex data relationship graphs.

OOP software design can facilitate (and even suggest) breaking application logic into multiple steps that operate on state that is acquired as each successive step is performed. If you do this, then the business rules that have been implemented will be difficult/impossible to reason about.

Use Functional Programming, it is safer

Rather, you should first acquire the data that is needed, and then pass that data as input to a function that will perform the required business rule on that data and return the results of that rule to the caller; which can then take whatever action fits the application requirements.

A function should never change data passed in an argument, it is a really bad thing to do 😡

Example application which illustrates the difference between OOP and FP

A code snippet from this example angular project on stackblitz.com is included below:

Your brain has to work harder🤔 to predict the initial output versus the output that is generated when the user clicks the “reload” button.

This project includes two components that do the exact same thing. One is implemented using OOP and the other using FP.

OOP Component

import { Component } from '@angular/core';
import { DataService } from '../data.service';
@Component({
selector: 'oop',
template: `
<h1>{{ label }}</h1>
`,
styles: [
`
h1 {
font-family: Lato;
}
`,
],
})
export class OopComponent implements OnInit {
label: string;
firstName: string;
lastName: string;
constructor(private dataService: DataService) {} ngOnInit(): void {
this.dataService.getFirstName().subscribe((firstName) => {
this.firstName = firstName;
});
this.dataService.getLastName().subscribe((lastName) => {
this.lastName = lastName;
});
// using this approach is hard to reason about
this.formatFirstName();
this.formatLastName(); this.formatLabel();
}
formatFirstName() {
this.firstName = this.firstName.toLowerCase();
}
formatLastName() {
this.lastName = this.lastName.toUpperCase();
}
formatLabel() {
this.label = `Mr. ${this.firstName} ${this.lastName}`;
}
}

FP Component

import { Component, Input, OnInit } from '@angular/core';
import { forkJoin } from 'rxjs';
import { DataService } from '../data.service';
@Component({
selector: 'fp',
template: `
<h1>{{ label }}</h1>
`,
styles: [
`
h1 {
font-family: Lato;
}
`,
],
})
export class FpComponent implements OnInit {
label: string;
constructor(private dataService: DataService) {} ngOnInit() {
forkJoin({
firstName: this.dataService.getFirstName(),
lastName: this.dataService.getLastName(),
}).subscribe(({ firstName, lastName }) => {
this.label = `Mr.
${(firstName as string).toLowerCase()}
${(lastName as string).toUpperCase()}`;
});
}
}

Clearly the FP component is cleaner code than the OOP component. Your brain has to work harder 🤔 to predict the component state generated by the OOP component than for the FP component.

For the OOP component, you may say to yourself “how ridiculous is that initialization logic?” — ask yourself this question: “do I do that all over the place, but with more complex data and business logic” 😳

Yes, this is a trivial case, but when business logic and data become complex, and features that are implemented use object-oriented techniques (especially those that use component state initialized asynchronously from different data sources) — the result is a body of code that is impossible to maintain.

forkJoin() is your friend.

--

--