I'm not a philosopher, but I sometimes enjoy understanding the whys more than the hows, especially when it comes to theoretical programming.
OOP: The "What Entity" paradigm
Looking at the world through the lens of entities or, to be more precise, objects.
What is OOP?
Object-Oriented Programming (OOP) is structured around objects—data structures that encapsulate properties and behavior.
Properties are self-explanatory (you'll understand better with the example), while the behavior of the program is described by methods.
OOP communicates in a clear, imperative manner - each method is a directive, an action to perform something.
The first task in OOP is identifying the objects.
These objects have properties that act like drawers; they store sockets which, in this context, refer to data.
In JavaScript, this data can be anything: socks, t-shirts, hats, gloves, television cables, televisions, desks, chairs, houses, cities, planets, creatures, and any tangible or fictional objects.
All these items can be stored as variables, or in the terminology of OOP, classes.
For instance, consider this Drawer object in JavaScript:
class Drawer {
// Furniture!
// Properties of the Drawer are declared within the constructor.
constructor() {
this.socks = [];
// It is also possible to store multiple properties, or perhaps, a reference to another class.
}
addSock(sock) {
// Class properties are accessed using the keyword `this`
this.socks.push(sock);
}
countSocks() {
return this.socks.length;
}
}
The Drawer represents an entity, binding related properties (socks) and behaviors (methods addSock and countSocks) together.
How does it work?
In OOP, you create an instance of your class (instantiate an object) and interact with it using the defined methods.
Here's how you'd use the Drawer:
const myDrawer = new Drawer();
myDrawer.addSock('red');
myDrawer.addSock('blue');
console.log(myDrawer.countSocks()); // Output: 2
Why use it?
The structure OOP provides makes it really easy and beginner-friendly to represent objects inside the code.
You figure out real-world entities, define what they do (through methods), and then write it into your program.
With this real-world mirroring, OOP helps you apply your everyday knowledge to your programming. You deal with objects all the time in real life.
Using them in your code just feels natural!
Functional Programming: "The Workflow" Paradigm
The beauty of working with Functional Programming (FP) is that you're less focused on entities. Instead, you want to describe the workflow/process.
What is it?
FP is about breaking each process into small steps—the shorter, the better—that combined, solve your problem.
FP views a program as a series of transformations applied to data. It values pure functions with single responsibilities (no side effects), immutability, and first-class functions.
In FP, steps are seen as data transformations.
Let's see how we could handle our 'socks in drawer' problem with FP:
let drawer = [];
const addSock = (drawer, sock) => [...drawer, sock];
const countSocks = (drawer) => drawer.length;
drawer = addSock(drawer, 'red');
drawer = addSock(drawer, 'blue');
console.log(countSocks(drawer)); // Output: 2
These steps show how we can transform our data (drawer and socks) to solve the problem. We start with an empty drawer, add socks to it, and then count them. Each step is a small transformation that brings us closer to the solution.
How does it work?
In FP, data and actions are separate. It's like cooking:
- Your raw ingredients are your data 🥚🥛🌾
- Your cooking process represents the actions 🍳
- You feed your raw ingredients (data) into your cooking process (functions)
- These functions output a new dish (new data), but your raw ingredients (data) are still raw. They're not modified!
let rawIngredients = ['eggs', 'flour', 'milk'];
let cookPancakes = (ingredients) => { /*...process...*/ return 'pancakes'; }
let pancakes = cookPancakes(rawIngredients);
// You now have pancakes, but your rawIngredients are still ['eggs', 'flour', 'milk']
Why use it?
It makes your code predictable and easy to test, by working with deterministic functions, there's less room for unexpected behavior.
By keeping your functions small, you guarantee that each method solves one single problem and the solution is achieved by chaining multiple steps.
Other benefits of using are:
Predictability: FP is like a well-written recipe. You can predict the outcome, given the same set of ingredients (data) every time. It eliminates surprises in your code 📖
Ease of testing: In FP, you can test each function (or step) separately, just like testing how well your egg-beating technique works before you proceed to the next cooking step. This isolation makes your code easy to test and debug 🥚➡️🍳
Reliability: For projects where high reliability is required, FP shines. It reduces the chances of unexpected bugs, making it a dependable choice for crucial projects 👍
Suitability for modern problems: Like using the right utensil for the right dish, FP provides the right tools to handle data-centric issues in programming. By focusing on the workflow, data transformations (not the data itself), you ensure your code is more concise and less propense to bugs.
JS Flexibility
With JavaScript, both approaches are valid, although I would recommend FP over OOP, as I think it suits more with the language's characteristics.
Why I recommend FP for JavaScript
Don't get me wrong, I absolutely appreciate the OOP's mind model, SOLID principles, hierarchy and polymorphism.
In fact, throughout my career, I've primarily written OOP code in in C#, .NET, Flutter, Xamarin Native, C and Java.
However, as I dove deeper into the world of JavaScript, I found myself increasingly drawn towards Functional Programming.
This transition wasn't motivated by hype.
Instead, it originated from a genuine sense of joy while crafting Functional Programming code.
Yet, this wasn't solely driven by personal satisfaction. finding more joy in writing Functional code compared to OOP, there are three concrete technical points that solidified my preference.
State Management
Usually in a JavaScript web application you need to handle local or global state.
Relying solely on OOP principles to handle state may introduce some challenges, specially when it comes to tracking changes.
FP advocates for immutability, the concept of data not being changed, but always replaced, once created.
This eliminates bugs arising from unexpected data mutation, leading to a more robust and predictable application.
Furthermore, with FP's preference for pure functions, tracking changes becomes transparent and intuitive, as each function's output is solely dependent on its input.
// Instead of mutating state directly,
// FP creates a new copy with desired changes
const newState = Object.assign({}, oldState, {updatedProp: newValue});
// A pure function in FP - given the same input, the output is always the same
const square = num => num * num;
Data Transformations
JavaScript often manipulates DOM or AJAX responses. FP excels here, treating these operations as a series of data transformations, which can be very handy when you're working with some Reactive Framework.
// Mapping data to DOM elements
const elements = data.map(item => `<span key={item.id}>${item.name}</span>`);
The map function is a pure, deterministic function.
It receives a callback function (often an arrow function, as used here) as its parameter and consistently returns a new array, leaving the original array unchanged, applying the FP concept of Immutability.
Reactive Frameworks
Reactive frameworks like React, Vue, or Svelte, embrace the reactive programming philosophy.
At the heart of this philosophy is writing code that springs into action upon state changes - like having a bingo card and looking for all selected numbers.
Whenever the selected numbers list changes, you'll "execute" a "reactive" behavior, that is:
- Analyze your card
- If the new selected number is present on your card, circle it
- If not, do nothing
This behavior will react to the state update of the selected numbers of the bingo.
Summarizing
Why objects?
Because it provides a clear and intuitive way to represent real-world behaviors and relationships by encapsulating them within recognizable objects.
By creating these virtual representations of real-world entities, we can easily conceptualize, organize, and manipulate our code in a manner that reflects reality.
Why steps?
The core philosophy of Functional Programming is to simplify complex problems by breaking them down into manageable pieces.
If you're struggling to solve a problem, break it into smaller tasks.
If the problem persists, break it down further until you have a specific question that can be answered through online searches or tools like ChatGPT.
This approach empowers beginners and experienced programmers to tackle complex problems by progressively addressing smaller, more comprehensible components.
By breaking down challenges into manageable tasks, you gain a clearer understanding of the overall problem and find solutions more easily while still keeping code readability.
At the end of the day, you'll end up creating solution pipelines that guide the data hand-in-hand through the problems and requirements of your application.