Object Oriented Vs. Functional Style in JavaScript
In this last blog post of this series about big words in JavaScript, I will explain about JavaScript as a multi-paradigm, prototype-based object-oriented, functional programming language.
This blog post will be a bit different from the previous ones, as you'll see.
However, don't expect it to be a deep dive into the prototype chain π.
Basics of OOP:
Object-oriented programming is a paradigm based on the concept of wrapping pieces of data, and behavior related to that data, into special bundles called objects, which are constructed from a set of βblueprintsβ, defined by a programmer, called classes.
Object-oriented programming is based on four pillars:
- Abstraction: hide details and get only an overview perspective of what we're implementing.
- Encapsulation: keep properties and methods private inside the classes, so they aren't accessible from outside the class. Some methods can be exposed as a public interface (API).
- Inheritance: make properties and methods of the class available to child classes, forming hierarchical relationship between classes. This allows us to reuse common logic and model real-world relationship.
- Polymorphism: A child class can overwrite a method it inherited from its parent class.
Functional Programming:
The main principle in Functional Programming is usage of pure functions.
For a function to be considered a pure function, it must follow these 4 rules:
- Have input parameters
- Not use any stateful values
- Always return the same output, giving the same input
- Not cause side-effects (as much as it possible)
The 'JavaScript way' of implementing both paradigms:
Actually, JavaScript is not a pure (classical) Object Oriented programming language, neither Functional programming language.
JavaScript has elements from both paradigms. It has classes (in ES6), closures and higher order functions. You can write code in JavaScript in both paradigms, but in the 'JavaScript way'.
Prototype-Based Object-Oriented
There are 2 types of object oriented programming:
Classical: class and instances of the class. The instance extending the super class must call super() in its constructor before setting its own properties and methods.
class Person { constructor(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; }}class Athlete extends Person { constructor(firstName, lastName, sportsCompetitions){ super.constructor(firstName,lastName); this.sportsCompetitions = sportsCompetitions; }}
Prototype-Based: the main principle is that objects are linked to a prototype object. The prototype contains methods (behavior) that are accessible to all objects linked to that prototype. In other words, behavior is delegated to the linked prototype object.
const Person = function(firstName, lastName) { this.firstName = firstName; this.lastName = lastName;}Person.prototype.sayHello = function() { console.log(`Hello ${this.firstName} ${this.lastName}`);}const jack = new Person('Jack', 'Nicholson');jack.sayHello();
JavaScript is prototype-based object-oriented programming language. Even though it has classes (syntactic sugar), behind the scenes it works exactly like constructor function (function that create objects), ie prototype inheritance.
Functional Style
Let's say it out loud: functional programming in JavaScript doesn't exist. We can implement functional style with closures, higher order function, currying, partial application etc.
- example of higher order function - function that can take another function as an argument, or returns another function
let list = [1, 2, 3, 4, 5];const multipleByTwo = item => item*2;// take function as an argumentlist.map(multipleByTwo);
- example of currying - the transformation from a function that takes multiple arguments, to a function that takes a single argument, and returns a function that takes the remaining arguments.
const multiply = (a, b) => a*b;const curriedMultiply = a => b => a*b;curriedMultiply(2)(5);const curriedMultiplyByTwo = curriedMultiply(2);// take the list from the prev examplelet list = [1, 2, 3, 4, 5];list.map(item => curriedMultiplyByTwo(item));
Same Game, Different Approach
As I already mentioned, I want this last part to be different from the previous ones. Thus, I have decided not to explain all the 'by the book' stuff, but implementing the Tic Tac Toe game in both paradigms
Functional Style Tic Tac Toe:
Starting with a confession, and careful not to annoy functional programmers π this won't be entirely functional, but there will be many functions that do one thing, and we'll then use these functions as tools.
When I started to think about how to implement the Tic Tac Toe game in a functional way, I decided to look at the board as a 3x3 matrix (I took a different approach in the OOP implementation).
<div id="container"> <div class="square" id="square0"></div> <div class="square" id="square1"></div> <div class="square" id="square2"></div> <div class="square" id="square3"></div> <div class="square" id="square4"></div> <div class="square" id="square5"></div> <div class="square" id="square6"></div> <div class="square" id="square7"></div> <div class="square" id="square8"></div></div>
NOTE: the HTML above, and the one in the OOP part is just to get a feeling. You will not be able to see the squares without CSS.
The top left corner is marked as index zero, and the bottom right is the last index (8). Therefore, the winning combinations can be represented through two-dimensional array. If all letters in one of the inner arrays are the same, then we have a winner.
const winningCombinations = [ [0,1,2], [3,4,5], [6,7,8], [0,4,8], [2,4,6], [0,3,6], [1,4,7], [2,5,8]];
Another thing I thought about is the two players: the human player, and the computer. What to do and what to check in every turn, and how to know if a square is empty or not.
Let's translate all of these thoughts to code:
The helper functions:
- grid - function that gives us access to any 'square'. I used 'Array.from()' to convert the 'array like' of nodes which 'document.getElementsByClassName()' returns to a 'real' array.
const grid = () => Array.from(document.getElementsByClassName('square'));
- squareId - helps us access an element in the array. Accessing an element in an array requires a number, therefore we have to get rid of the 'square' part in the id and convert the string that represented by the square's id to a number.
- emptySquare - function that helps us detect the squares that are empty.
- opponentChoice - the computer's choice. Returns a random empty square.
const squareId = (squareElm) => Number.parseInt(squareElm.id.replace('square', ''));const emptySquare = () => grid().filter(squareElm => squareElm.innerText === '');const opponentChoice = () => squareId(emptySquare()[Math.floor(Math.random() * emptySquare().length)]);
- takeTurn - take 2 arguments: index and letter and helps us to put the correct letter in the correct index.
const takeTurn = (index, letter) => grid()[index].innerText = letter;
- allSame - function that checks the winning condition.
const allSame = (arr) => arr.every(squareElm => squareElm.innerText === arr[0].innerText && squareElm.innerText !== '');
The listeners:
- clickFn - function that receives an event and calls 'takeTurn()' function. 'takeTurn()' takes the 'squareId()' function as the first argument, and 'X' (the letter) as the second argument. 'squareId()' takes the event target to detect in which square (or in which index) to put the 'X'*.
After the turn was taken, we check if that turn leads to a win, and if not - we switch to the second player's turn (the computer).
const clickFn = (event) => { takeTurn(squareId(event.target), 'x'); if(!checkForVictory()) opponentTurn();};
- Set and remove listeners on each square
const enableListeners = () => grid().forEach(squareElm => squareElm.addEventListener('click', clickFn));const disableListeners = () => grid().forEach(squareElm => squareElm.removeEventListener('click', clickFn));enableListeners();
The play game functions:
- endGame - function that disables the listeners.
const endGame = () => { // could also paint the winning letter in a different color. disableListeners();};
- checkForVictory - function that loops through the 'winningCombinations' array, creates a sequence and checks if that sequence leads to a win. If so, it ends the game.
const checkForVictory = () => { let victory = false; winningCombinations.forEach(comb => { const gridComb = grid(); const sequence = [gridComb[comb[0]], gridComb[comb[1]], gridComb[comb[2]]]; if (allSame(sequence)) { victory = true; endGame(sequence); } }); return victory;};
- opponentTurn - picks an index randomly. First we have to remove the listeners, to prevent the human player from clicking on the board. Then, it takes a turn with the computer's random choice ('opponentChoice'), and the 'O' letter, checks for victory, and if there is no victory, it enables the listeners so that the human player could play.
const opponentTurn = () => { disableListeners(); setTimeout(() => { takeTurn(opponentChoice(), 'o'); if(!checkForVictory()) enableListeners(); }, 1000);}
A comic relief:
Object Oriented Tic Tac Toe:
As this is an OOP implementation, I decided to build the HTML in a different way to introduce another approach.
<div class="container"> <div class="row"> <div class="col"></div> <div class="col"></div> <div class="col"></div> </div> <div class="row"> <div class="col"></div> <div class="col"></div> <div class="col"></div> </div> <div class="row"> <div class="col"></div> <div class="col"></div> <div class="col"></div> </div> </div>
Think about the Tic Tac Toe game. What do we have? Which functions (or classes) do we need?
We need a board, 2 players (the human player and the computer) and an indication of who is playing (turn).
function TicTacToeGame() { const board = new Board(); const humanPlayer = new HumanPlayer(board); const computerPlayer = new ComputerPlayer(board); let turn = 0;}
- The turn function - (nested inside the start function), decides which player's turn it is. If we have a winner, nothing happens. I decided that the even numbers indicate that this is the human's turn (and the odd numbers indicate the computer's turn).
function takeTurn() { if (board.checkForWinner()) { return; } if (turn % 2 === 0) { humanPlayer.takeTurn(); } else { computerPlayer.takeTurn(); } turn++; }
- The board function - that function is responsible to all the things that we have on the board. We have properties such as the positions and wining combination (which I implemented exactly like in the FP approach), and we have methods such as checking for a winner.
function Board() { this.positions = Array.from(document.querySelectorAll('.col')); this.checkForWinner = function() { let winner = false; const winningCombinations = [ [0,1,2], [3,4,5], [6,7,8], [0,4,8], [2,4,6], [0,3,6], [1,4,7], [2,5,8] ]; winningCombinations.forEach((winningComb) => { const pos0InnerText = this.positions[winningComb[0]].innerText; const pos1InnerText = this.positions[winningComb[1]].innerText; const pos2InnerText = this.positions[winningComb[2]].innerText; const isWinningComb = pos0InnerText !== '' && pos0InnerText === pos1InnerText && pos1InnerText === pos2InnerText; if (isWinningComb) { winner = true; winningComb.forEach((index) => { // add css class which color the winner positions (index) }); } }); return winner; }}
- The player functions - each one of them takes the board and put the correct letter ('X'/'O') in the correct (clicked) position. Like in the functional approach, the computer picks its position randomly (as long as it's available).
function ComputerPlayer(board) { this.takeTurn = function() { let availablePositions = board.positions.filter((p) => p.innerText === ''); const move = Math.floor(Math.random() * (availablePositions.length - 0)); availablePositions[move].innerText = 'O'; }}function HumanPlayer(board) { this.takeTurn = function() { board.positions.forEach(el => el.addEventListener('click', handleTurnTaken)); } function handleTurnTaken(event) { event.target.innerText = 'X'; // remove the event listener - to disable the click and let the computer play board.positions .forEach(el => el.removeEventListener('click', handleTurnTaken)); }}
- The start game function - in this function I used the 'MutationObserver()' function in order to observe whether things are changing in the DOM. Whenever the child of a 'div' changes, it will be passed as argument to the observer.
this.start = function() { const config = { childList: true }; const observer = new MutationObserver(() => takeTurn()); board.positions.forEach((el) => observer.observe(el, config)); takeTurn(); }
- Starting the game
const ticTacToeGame = new TicTacToeGame();ticTacToeGame.start();
Conclusions
Now it's the part in which I have to say which paradigm I prefer and why.
To be honest, while I understand the power of functional programming, and it also makes me feel smarter and enlightened π to write in functional style, I think I prefer OOP. It feels more natural to me, but maybe it's just because it's my 'first' paradigm.
Feelings aside, as always in programming, it's all about what is appropriate for our program. The beauty in JavaScript is that we don't have to choose only one, we can use both π.
Thanks for reading so far. I hope it was clear and helpful.
Two weeks from now I am going to start a new job (let's see how many of you really got so far and will congratulate me π), so I have not yet decided what the next post will be about, but I promise it will be π€―, stay tuned.
β Go to All Posts