Assembly in Progress...

Programming Methodologies in JavaScript

Overview: Differences Between OOP and Functional Programming

OOP uses inheritance, via super-classes and sub-classes, and structures code around "what something is". The approach is similar to packing everything each item will need in a box, then only pulling out what is needed when it is needed.

Basic Qualities
  • Only a few functions work on common data.
  • Uses an inheritance model.
  • Object state can be modified.
  • Functions use side effects.
Three potential problems with an inheritance model:
    Sub-classes might only need (or perhaps should only use) one method, yet they still absorb all methods from the parent classes.
  • There is potential fragility if changes to a super-class has unforeseen effects on a sub-class.
  • The state of an object and function side-effects can become difficult to manage as projects get larger.

Functional Programming uses composition, via an assembly-line style combining of many simple functions to act on fixed data. This structures code around "what something has". Unlike OOP's "box" approach, this is more like putting data in one end of a pipe and then the processed data comes out on the other end.

Basic Qualities
  • Many functions work on fixed data.
  • No modification of state.
  • Pure functions - no side effects.
  • More declarative - about what needs to be done.

Object Oriented Programming

    Benefits of OOP

    • Clear and understandable.
    • Easy to extend.
    • Easy to maintain.
    • Memory efficient.
    • Adheres to "D.R.Y." philosophy (Don't Repeat Yourself).

      Four Pillars of OOP

      • Encapsulation - neat & clean self-contained packages that can interact with each other
      • Abstraction - Hiding the complexity to make interaction with the code basic, intuitive and complete.
      • Inheritance - Avoiding duplicated code by sharing methods and properties.
      • Polymorphism - Ability to call a single method on different objects and have the method function different based on the object or situation.

      Using encapsulation these objects offer complete units ready to be used as needed. The properties like name and weapon makeup the state of the object while the methods like attack are there to affect the state of the object.

      const elf = {
          name: 'Orwell',
          weapon: 'bow',
          attack() {
              return 'attack with ' + elf.weapon
          }
      }
      
      const elf2 = {
          name: 'Sam',
          weapon: 'Sword',
          attack() {
              return 'attack with ' + elf.weapon
          }
      }
      

      The problem with the objects above is they repeat code and, presumably, there will be more of these. Duplicating this many times is time-consuming and inefficient at runtime.

      The concept of "Factory Functions", or functions that build objects, can make the objects above cleaner and more efficient to build.

      // Factory function - build elf objects
      function createElf(name, weapon) {
          return {
              name: name,
              weapon: weapon,
              attack() {
                  return 'attack with ' + weapon
              }
          }
      }
      
      const peter = createElf('Peter', 'Stones');
      console.log(peter.attack()); // RESULT: "attack with Stones"
      
      const sam = createElf('Sam', 'Fire');
      console.log(sam.attack()); // RESULT: "attack with Fire"
      

      This is better, but still will house common code, like attack() in different places in memory for each object. To prevent this duplication, object Inheritance can allow each object to reference, through prototype chaining, a core Elf object. Constructor Functions were originally used for this purpose. Constructor Functions use the new keyword to automatically return a new object.

      // Constructor functions
      // A capitol first letter is common to indicate a Constructor function.
      function Elf(name, weapon) {
          this.name = name;
          this.weapon = weapon;
      }
      // Setup the prototype chain
      Elf.prototype.attack = function() {
          return 'attack with ' + this.weapon
      }
      
      // the new keyword is required for using Constructor functions to point "this" to the object being created.
      const peter = new Elf('Peter', 'Stones');
      console.log(peter.attack()); // RESULT: "attack with Stones"
      
      const sam = new Elf('Sam', 'Fire');
      console.log(sam.attack()); // RESULT: "attack with Fire"
      

      A step better than Constructor Functions, Object.create() was eventually built into JavaScript to create the prototype chain.

      // Factory function - build elf objects
      const elfFunctionsStore = {
          attack() {
              return 'attack with ' + this.weapon
          }
      }
      
      function createElf(name, weapon) {
          // Set up the prototype chain
          let newElf = Object.create(elfFunctionsStore);
          newElf.name = name;
          newElf.weapon = weapon;
          return newElf;
      }
      
      const peter = createElf('Peter', 'Stones');
      console.log(peter.attack()); // RESULT: "attack with Stones"
      
      const sam = createElf('Sam', 'Fire');
      console.log(sam.attack()); // RESULT: "attack with Fire"
      

      Object.create() was generally preferred over constructor functions as it is cleaner and more readable, but it is a step away from true Object Oriented Programming(OOP). The addition of classes in ES6 allows for OOP style of blueprinting an object in a class. With this, all properties and methods for the object can be contained within the object class and be available for each object it creates. Plus, classes make understandable use of the this keyword.

      Everything in the constructor section of a class gets created as a new property on the new object. Everything outside of the constructor section, like functions, will be referenced by the new object and not duplicated. This makes non-unique bits of code, like functions, take up less memory.

      // ES6 Class
      class Elf {
          constructor(name, weapon) {
              this.name = name;
              this.weapon = weapon;
          }
          attack() {
              return 'attack with ' + this.weapon
          }
      }
      
      const peter = new Elf('Peter', 'Stones');
      console.log(peter.attack()); // RESULT: "attack with Stones"
      
      const sam = new Elf('Sam', 'Fire');
      console.log(sam.attack()); // RESULT: "attack with Fire"
      

      Creating a new object that is a subset of a main object is thought of as creating an instance of a class. This process is called instantiation. For this, using instanceOf can, as the name implies, show if something is an instance of something else

      // ES6 Class
      class Elf {
          constructor(name, weapon) {
              this.name = name;
              this.weapon = weapon;
          }
          attack() {
              return 'attack with ' + this.weapon
          }
      }
      
      const peter = new Elf('Peter', 'Stones');
      console.log(peter.attack()); // RESULT: "attack with Stones"
      
      const sam = new Elf('Sam', 'Fire');
      console.log(sam.attack()); // RESULT: "attack with Fire"
      
      // Check for instanceOf
      console.log('Is Peter and instance of Elf?', peter instanceof Elf);
      console.log('Is Sam and instance of Elf?', sam instanceof Elf);
      

      The above accomplished an OOP-style of programming (even though JavaScript technically is still just using prototypal inheritance under the hood), but this can be improved to a more complete OOP approach. Using a super-class and then extending this to create sub-classes allows for sharing of common functions between all objects created, while each sub-class object can share functions and properties unique to their group

      For this, you need to call the super-class' constructor and pass the needed variables. This is done through the super(props) function.

      // Set the super-class
      class Character {
          constructor(name, weapon) {
              this.name = name;
              this.weapon = weapon;
          }
          attack() {
              return 'attack with ' + this.weapon
          }
      }
      
      // Extend the super-class
      class Elf extends Character {
          constructor(name, weapon, type) {
              super(name, weapon, type);
      
              // Add properties unique to this sub-class, but also unique variations for each instance of this sub-class
              this.type = type
          }
      }
      
      class Ogre extends Character {
          constructor(name, weapon, color) {
              super(name, weapon);
      
              // Add properties unique to this sub-class, but also unique variations for each instance of this sub-class
              this.color = color;
          }
      
          // Add functions unique to this sub-class and shared by all in the sub-class
          makeFort() {
              return 'The strongest fort in the world has been made'
          }
      }
      
      const peter = new Elf('Peter', 'Stones', 'Wood Elf');
      console.log(peter.name); // RESULT: "Peter"
      console.log(peter.type); // RESULT: "Wood Elf"
      console.log(peter.attack()); // RESULT: "attack with Stones"
      
      const sam = new Elf('Sam', 'Fire', 'House Elf');
      console.log(sam.name); // RESULT: "Sam"
      console.log(sam.type); // RESULT: "House Elf"
      console.log(sam.attack()); // RESULT: "attack with Fire"
      
      const shrek = new Ogre('Shrek', 'club', 'green');
      console.log(shrek.name); // RESULT: "Shrek"
      console.log(shrek.attack()); // RESULT: "attack with club"
      console.log(shrek.makeFort()); // RESULT: "The strongest fort in the world has been made"
      
      // Check for instanceOf
      console.log('Is Peter an instance of Elf?', peter instanceof Elf); // RESULT: true
      console.log('Is Sam and instance of Elf?', sam instanceof Elf); // RESULT: true
      
      console.log('Is Shrek an instance of Elf?', shrek instanceof Elf); // RESULT: false
      console.log('Is Shrek an instance of Character?', shrek instanceof Character); // RESULT: true
      console.log('Is Shrek an instance of Ogre?', shrek instanceof Ogre); // RESULT: true
      
      // Under the hood, JS is just creating prototype chains
      console.log('Is Ogre a prototype of Shrek? ', Ogre.prototype.isPrototypeOf(shrek));
      console.log('Is Character a prototype of Ogre? ', Character.prototype.isPrototypeOf(Ogre.prototype));
      
       

      Four ways to bind this

      Creating with the new Keyword

      function Person(name, age) {
          this.name = name;
          this.age = age;
      }
      
      // this becomes bound to the object created with the new keyword
      const person1 = new Person(Xavier ', 55);
      

      Implicit Binding

      // this will reference an object, even without directly declaring it.
      person2 {
          name: 'Karen',
          age: 40,
          hi() {
              console.log('hi' + this.name)
          }
      }
      
      const person1 = new Person(Xavier ', 55);
      

      Explicit Binding

      const person3 = {
          name: 'Karen',
          age: 40,
          hi: function() {
      
              // this is explicitly bound to the window object here
              console.log('hi' + this.setTimeout)
          }.bind(window)
      }
      
      person3.hi();
      

      Arrow Function

      const person4 = {
          name: 'Karen',
          age: 40,
          hi: function() {
              // this is bound to the lexical environment with an arrow function, not to where it is invoked like a regular function
              var innerFunction = () = & gt; {
                  console.log('hi' + this.name)
              }
              return innerFunction
          }
      }
      
      person4.hi();
      

      Functional Programming

      • Functional programming was introduced in LISP over 60 years ago (1958).
      • Current popular functional programming languages include Pascal, Scala, and Clojure.
      • Functional programming works really well for distributed computing (multiple machines interacting with data) and parallelism (machines working on the same data at the same time).
      • A strong focus on simplicity concerning data and functions is a characteristic of Functional Programming languages.
      • The separation of concerns - data and functions - creates clarity.
      • Pure functions are a staple of functional programming.
      • All objects created in functional programming are immutable and sharing state is not allowed. 

      Main goals are to achieve code that is:

      • Clear and understandable.
      • Easy to extend.
      • Easy to maintain.
      • Memory efficient.
      • D.R.Y.

      Pure Functions

      Rules for Pure Functions
      • Referential Transparency:Functions that always return the same output given the same input.
      • Side Effects:Cannot modify anything outside of the functions.
      Qualities of Pure Functions
      • Only one task.
      • Return statement.
      • No shared state.
      • Immutable state.
      • Composable.
      • Predictable.

      Can the average program only have pure functions? No. There will always be a certain amount of interactions/side effects in any program. The key, as far as Functional Programming is concerned, is to organize the code in a way that isolates these side effects.

      // These are NOT Pure function as each will alter the array every time it is called, and the order of calling these makes a difference.
      const array = [1, 2, 3];
      
      function mutateArray(arr) {
          arr.pop();
      }
      
      function mutateArray2(arr) {
          arr.push()
      }
      
      // Rewritten as Pure functions
      const array = [1, 2, 3];
      
      function removeLastItem(arr) {
          const newArray = [].concat(arr);
          newArray.pop();
          return newArray
      }
      
      function addToArray(arr) {
          const newArray = [].concat(arr);
          newArray.push(1)
          return newArray
      }
      
      console.log(removeLastItem(array)); // RESULT: [ 1, 2 ]
      console.log(addToArray(array)); // RESULT: [ 1, 2, 3, 1 ]
      console.log(array); // RESULT: [ 1, 2, 3] (has not been altered)
      
      // Another example of a Pure function
      function multiplyBy2(arr) {
          return arr.map(item = & gt; item * 2)
      }
      
      console.log(multiplyBy2(array)); // RESULT: [ 2, 4, 6 ]
      console.log(array); // RESULT: [ 1, 2, 3] (has not been altered)
      

      Idempotence

      This is the concept of a function doing exactly what is expected every time it is called. The function may or may not be Pure (it might have side effects) but it still performs the same action and returns the same result every time it is called with the same variables. In addition, an idempotent function can call itself repeatedly and still return the same output as if it did not call itself

      This concept is especially valuable for parallel and distributed computing in that it makes the code predictable and consistent

      // Not Idempotent
      function notIdempotent(num) {
          Math.random(num)
      }
      
      notIdempotent(5) // RESULT: (This returns a different number every time, because the function is not idempotent)
      
      // An idempotent function that is not Pure becasue it interacts outside its environment
      function antIdempotentOne(num) {
          console.log(num);
      }
      
      antIdempotentOne(5) // RESULT: 5
      
      // An example of being able to call itself repeatedly and still output the the same as if not calling itself
      Math.abs(Math.abs(Math.abs(-50)));
      

      Imperative vs Declarative

      Imperative: Telling the machine what to do and how to do it. Declarative: Telling the machine what to do and what you want to have happen, but not how to do it. This will generally still be compiled by something more imperative, but the engineers interaction with the code can be less complex with declarative functions Functional Programming helps us be more declarative.

      // Imperative command - Details the actions to be taken to produce a result
      for (let i = 0; i & lt; = 3; i++) {
          console.log(i);
      }
      
      // Decalrative - No instruction on how to perform this, only what should happen
      [1, 2, 3].forEach(item = & gt; console.log(item))
      

      Currying

      Currying is the technique of reducing functions to a very basic utility with no more than one argument that can then be linked together to perform larger operations. This can reduce system demands and control complexity.

      // Without currying
      const multiply = (a, b) => a * b;
      multiply(3, 4); // RESULT: 12
      
      // With currying
      const curriedMultiply = (a) => (b) => a * b;
      curriedMultiply(3)(4); // RESULT: 12
      
      // Other possibilities for currying
      const multBy5 = curriedMultiply(5);
      const multBy7 = curriedMultiply(7);
      const multBy250 = curriedMultiply(255);
      
      multBy5(3); // RESULT: 15
      multBy7(3); // RESULT: 21
      multBy250(3); // RESULT: 750
      multBy5(multBy7(3)); // RESULT: 75
      multBy5(multBy5(3)); // RESULT: 105
      

      Partial Application

      Partial Application is the technique of partially filling a function's variables/ actions so that the function can be reused with less system demand because it is partially saved in memory. The function would expect the rest of the variables on the second call. This can also create cleaner, more obvious code while eliminating repeated identical variable input..

       // Without Partial Application
      const multiply = (a, b, c) => a * b * c;
      multiply(3, 4, 10); // RESULT: 120
      
      const partMultBy5 = multiply.bind(null, 5);
      
      partMultBy5(4, 10) // RESULT: 200
      

      Memoization (Cache)

      Memoization is the act of storing the output of a function in memory to be read multiple times. This keeps the data available while avoiding running the same function and parameters multiple times.

      // Using closure wraps the variable and keeps it away from the Global scope.
      function memoizeAddTo80(n) {
          let cache = {};
          return function(n) {
              if (n in cache) {
                  return cache[n];
              } else {
                  // THhs console.log simulates the longer calculation that might be taking place
                  console.log('long time');
                  const answer = n + 80;
                  cache[n] = answer;
                  return answer;
              }
          }
      }
      
      const memoized = memoizeAddTo80();
      console.log(1, memoized(7)) // RESULT: long time 87
      console.log(2, memoized(7)) // RESULT: 83
      

      Compose & Pipe

      Composability is a system design principle that handles different components directly interacting. Essentially, it's a process for combining different functions and running them in various combinations. This is somewhat like a function assembly line, where the output of each function feeds into the next one, and so on. Pipe is the same as compose, but it runs the functions in reverse order. The first function fires first with the output feeding the next function, whose output feeds the next function, etc.

      // Create a compose function
      // This runs the last parameter first (g) and
      // the return from that is fed into the first parameter (f)
      const compose = (f, g) => (data) => f(g(data));
      
      // Create a pipe function
      // This runs the first function (g) and
      // the output from that is fed into the next parameter (f)
      const pip = (f, g) => (data) => f(g(data));
      
      const multiplyBy3 = (num) => num * 3;
      const makePositive = (num) => Math.abs(num);
      
      // Compose it all in a littel assembly line
      const multiplyBy3andAbsolut = compose(multiplyBy3, makePositive);
      
      // Run it
      multiplyBy3andAbsolut(-50); // RESULT: 150
      
      // Pipe it all in a littel assembly line
      const multiplyBy3andAbsolut = pipe(multiplyBy3, makePositive);
      
      // Run it
      multiplyBy3andAbsolut(-50); // RESULT: 150
      

      Arity

      Arity is the number of parameters a function accepts. In Functional Programming, an arity of one or two is desired.

      // This has an arity of 2
      const compose = (f, g) => (data) => f(g(data));
      
      // This has an arity of 4
      const funct2 = (f, g, h, i) => console.log(f, g, h, i);
      

      Bringing All of This Together

      Below uses the principles of Functional Programming to create a working shopping cart.

      // Setup the user
      const user = {
          name: 'Kim',
          active: true,
          cart: [],
          purchases: []
      }
      
      // The history1 variable will store user each step to keep a record
      const history1 = [];
      
      // Setup compose functionality
      const compose = (f, g) = & gt;
      (...args) = & gt;
      f(g(...args))
      
      // Use compose to set up the assembly line
      const purchaseItem = (...fns) = & gt;
      fns.reduce(compose);
      
      // Fill the assembly line
      purchaseItem(
          emptyUserCart,
          buyItem,
          applyTaxToItems,
          addItemToCart
      )(user, {
          name: 'laptop',
          price: 50
      });
      
      // Record the initial user data,
      // update the cart and then return a copy
      // of the user object with the updated info
      function addItemToCart(user, item) {
          history1.push(user)
          const updatedCart = user.cart.concat(item)
          return Object.assign({}, user, {
              cart: updatedCart
          });
      }
      
      // Record the initial user data and
      // then return the cart with the updated amount
      function applyTaxToItems(user) {
          history1.push(user)
          const {
              cart
          } = user;
          const taxRate = 1.3;
          const updatedCart = cart.map(item = & gt; {
              return {
                  name: item.name,
                  price: item.price * taxRate
              }
          })
          return Object.assign({}, user, {
              cart: updatedCart
          });
      }
      
      // Record the initial user data and
      // then return a copy of the object with the item added to the cart
      function buyItem(user) {
          history1.push(user)
          const itemsInCart = user.cart;
          return Object.assign({}, user, {
              purchases: itemsInCart
          });
      }
      
      // Record the initial user data and
      // then return an empty copy of the object.
      function emptyUserCart(user) {
          history1.push(user)
          return Object.assign({}, user, {
              cart: []
          });
      }
      

      Leave a Reply

      This site uses Akismet to reduce spam. Learn how your comment data is processed.

      q
      ↑ Back to Top