Skip to content

Latest commit

 

History

History

lesson-notes

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

OOP Part 2

Lesson Objectives

  • Understand the utility of inheritance
  • Make a class inherit from a parent class
  • Understand the utility of creating a factory
  • Create a factory

Inheritance

Note: we are building on top of the code we wrote yesterday.

Let's say there is a new kind in our world of animals: Mythical creature. The mythical creature will have all the properties of methods as an animal and some additional ones. We could copy our Animal class, paste it, and add more, but what if we change the greet function? Then we would have to find every place we copy-pasted and carefully update. We will keep our animal as our one source of truth as a parent class.

Original Animal:

class Animal {
  constructor(name, type, color, walkStyle, isFriendly = true) {
    this.name = name;
    this.type = type;
    this.age = 4;
    this.color = color;
    this.isFriendly = isFriendly;
    this.walkStyle = walkStyle || "Walka, walka";
  }
  walk() {
    console.log(this.walkStyle);
  }
  greet(otherBeing) {
    console.log(`Sniff sniff, ${otherBeing}`);
  }
  classyGreeting(otherClassyBeing) {
    console.log(`Howdy, there, ${otherClassyBeing.name}`);
  }
  ageUp() {
    this.age++;
  }
}
const buttons = new Animal("Buttons", "turtle", "green");
const fluffy = new Animal("Fluffy", "cat", "calico", "Strut, strut", false);
const marshmallow = new Animal(
  "Marshmallow",
  "miniature horse",
  "white",
  "Clip clop, clip clop"
);

Mythical Creature:

class MythicalCreature extends Animal {
  grantWish(wish) {
    console.log(`I have granted you your wish to ${wish}`);
  }
}

const chips = new MythicalCreature(
  "Chips",
  "unicorn",
  "iridescent white",
  "clip clop, vanish"
);

console.log(chips);
chips.walk();
chips.grantWish("always write bug free code");

We can override previous functionality.

class MythicalCreature extends Animal {
  grantWish(wish) {
    console.log(`I have granted you your wish to ${wish}`);
  }
  walk() {
    console.log(`💫✨🌟 ${this.walkStyle} 💫✨🌟 `);
  }
}

const chips = new MythicalCreature("Chips", "unicorn", "iridescent white");

console.log(chips);
chips.grantWish("always write bug free code");
chips.walk();

We can reference the parent's class method and extend its original functionality.

class MythicalCreature extends Animal {
  grantWish(wish) {
    console.log(`I have granted you your wish to ${wish}`);
  }
  walk() {
    console.log(`💫✨🌟 ${this.walkStyle} 💫✨🌟 `);
  }

  greet(otherBeing) {
    super.greet(otherBeing);
    console.log("I am very pleased to see you today!");
  }
}

const chips = new MythicalCreature("Chips", "unicorn", "iridescent white");

console.log(chips);
chips.walk();
chips.grantWish("always write bug free code");
chips.greet("Marshmallow");

We can also add properties in the constructor.

class MythicalCreature extends Animal {
  constructor(name, type, color, isFriendly) {
    super(name, type, color, isFriendly);
    this.powers = ["invisibility", "laser eyes", "super strength"];
  }
  grantWish(wish) {
    console.log(`I have granted you your wish to ${wish}`);
  }
  walk() {
    console.log(`💫✨🌟 ${this.walkStyle} 💫✨🌟 `);
  }

  greet(otherBeing) {
    super.greet(otherBeing);
    console.log("I am very pleased to see you today!");
  }
}

const chips = new MythicalCreature("Chips", "unicorn", "iridescent white");

// console.log(chips);
chips.walk();
chips.grantWish("always write bug free code");
chips.greet("Marshmallow");
console.log(chips.powers);

super is another special keyword/function. Try misspelling it - and you'll see it won't work.

Factory

Sometimes we need a controlled way to generate other objects.

Let's think back to our example of creating a deck of cards for the game BlackJack.

class DeckOfCards {
  constructor() {
    this.cards = [];
    this.createDeck();
  }
  createDeck() {
    for (let i = 1; i <= 13; i++) {
      this.deck.push(i);
    }
  }
}

const deck = new DeckOfCards();
console.log(deck);

The above is a good start, but we must ensure we understand the specs.

The goal will be to play a simple game of Blackjack (two players, each gets two cards, determine who wins)

  • Cards with a face of 2 - 10, jack, queen, king ace

  • Values of cards 2-10 are the same as the face. Jack, queen, and king are worth 10. Ace starts as a value of 11 but can be changed to a value of 1

  • There are 4 sets of 13 cards (hearts, diamonds, spades, clubs)

  • The card objects should go in an array

  • There should be a method that 'shuffles' the deck. The card objects can be reordered

  • As each play happens, two cards are given to the player, and two are given to the computer player - these cards are removed from the array of card objects

Write down any questions about this model. What information is needed? What other considerations are there?

Cards are objects. They are not simple numbers. There are also four sets.

Let's create a Card class.

class Card {
  constructor(face, value, suit) {
    this.face = face;
    this.value = value;
    this.suit = suit;
  }
}

And update our Deck class.

class DeckOfCards {
  constructor() {
    this.cards = [];
    this.createDeck();
  }
  createDeck() {
    for (let i = 1; i <= 13; i++) {
      this.cards.push(new Card(i, i, "hearts"));
    }
  }
}

const deck = new DeckOfCards();
console.log(deck);

Let's add some more logic to ensure our card values match what they should be for a game of Blackjack.

class DeckOfCards {
  constructor() {
    this.cards = [];
    this.createDeck();
  }
  createDeck() {
    for (let i = 1; i <= 13; i++) {
      for (let i = 1; i <= 13; i++) {
        let face = i;
        let suit = "hearts";
        let value = i;
        if (i === 1) {
          face = "Ace";
        } else if (i === 11) {
          face = "Jack";
          value = 10;
        } else if (i === 12) {
          face = "Queen";
          value = 10;
        } else if (i == 13) {
          face = "King";
          value = 10;
        }
        this.cards.push(new Card(face, value, "Hearts"));
      }
    }
  }
}

const deck = new DeckOfCards();
console.log(deck);

Now, let's add the different suits:

Hint: write your j for loop first. Then copy and paste the entirety of your i loop inside. Hint 2: Highlight the first hearts you want to replace, then press command d until you have highlighted the ones below. Now type suits[j] in their place

class DeckOfCards {
  constructor() {
    this.cards = [];
    this.createDeck();
  }
  createDeck() {
    const suits = ["Hearts", "Diamonds", "Clubs", "Spades"];
    for (let j = 0; j < suits.length; j++) {
      for (let i = 1; i <= 13; i++) {
        let face = i;
        let suit = "hearts";
        let value = i;
        if (i === 1) {
          face = "Ace";
        } else if (i === 11) {
          face = "Jack";
          value = 10;
        } else if (i === 12) {
          face = "Queen";
          value = 10;
        } else if (i == 13) {
          face = "King";
          value = 10;
        }
        this.cards.push(new Card(face, value, suits[j]));
      }
    }
  }
}

const deck = new DeckOfCards();
console.log(deck);

We now need a way to shuffle the deck. There is no built-in shuffle method in JavaScript. So let's look up how to do it.

There is a Fisher-Yates Shuffle - Mike Bostock wrote this one - he also wrote the D3 JavaScript library, which is used to create a lot of visuals used by the NYT.

shuffle (array) {};
class DeckOfCards {
  constructor() {
    this.cards = [];
    this.createDeck();
    this.shuffle(this.cards);
  }
  createDeck() {
   const suits = ["Hearts", "Diamonds", "Clubs", "Spades"];
    for (let j = 0; j < suits.length; j++) {
      for (let i = 1; i <= 13; i++) {
        let face = i;
        let suit = "hearts";
        let value = i;
        if (i === 1) {
          face = "Ace";
        } else if (i === 11) {
          face = "Jack";
          value = 10;
        } else if (i === 12) {
          face = "Queen";
          value = 10;
        } else if (i == 13) {
          face = "King";
          value = 10;
        }
        this.cards.push(new Card(face, value, suits[j]));
      }
    }
  }
  }
  shuffle(array) {
    let m = array.length;
    let i = 0;
    // While there remain elements to shuffle…
    while (m) {
      // Pick a remaining element…
      i = Math.floor(Math.random() * m--);

      // And swap it with the current element.
      [array[m], array[i]] = [array[i], array[m]];
    }
    return array;
  }
}

const deck = new DeckOfCards();
console.log(deck);

Bonus

Alternative Syntax: Switch Statement

We have a lot of if/else statements inside our Create Deck function. We can rewrite it to use a switch statement:

const createDeck = () => {
  const suits = ["Hearts", "Diamonds", "Clubs", "Spades"];
  for (let j = 0; j < suits.length; j++) {
    for (let i = 1; i <= 13; i++) {
      switch (i) {
        case 1:
          value = 11;
          face = "A";
          break;
        case 11:
          value = 10;
          face = "J";
          break;
        case 12:
          value = 10;
          face = "Q";
          break;
        case 13:
          value = 10;
          face = "K";
          break;
        default:
          value = i;
          face = i;
      }
      deck.push(new Card(value, face, suits[j]));
    }
  }
  return deck;
};

This works a lot like an if/else statement but can be easier to read if there are a lot of cases.

Game Play

Get Two Cards

const playerHand = [];
const theHouseHand = [];

playerHand.push(deck.cards.pop());
playerHand.push(deck.cards.pop());
theHouseHand.push(deck.cards.pop());
theHouseHand.push(deck.cards.pop());

console.log(playerHand);
console.log(theHouseHand);

Get Sums

let playerHandSum = playerHand[0].value + playerHand[1].value;
let theHouseHandSum = theHouseHand[0].value + theHouseHand[0].value;
console.log(playerHandSum, theHouseHandSum);

Compare Sums

if (playerHandSum > theHouseHandSum) {
  console.log("player wins");
} else if (playerHandSum < theHouseHandSum) {
  console.log("The House wins");
} else {
  console.log("it is a tie");
}

Can you think of the edge case where this logic may not work as expected?

Hint What happens if someone has two aces in their hand?

How would you solve it?

Continue Game Play

We would likely want each player to have a name, an amount of money to bet, a total amount of money, and more. We could keep making separate values for each one like playerName, playerHand, playerBet, playerBankroll, playerNumOfWins. But what becomes the problem with such a strategy? What happens if this game ends up going into production and is played by millions of people, and we are expected to implement all the rules of Black Jack? What could we do to organize our code?

Further Reading

Eloquent JavaScript

Chapter 6: The Secret Life of Objects

Extreme Super Bonus

Read this code, analyze what it does, then research what is prototype is in JavaScript, how it works and what does it do?

How does it relate to this lesson in classes?

String.prototype.reverse = function () {
  return this.split("").reverse().join("");
};

console.log("foMO".reverse());

Check out README3.md from the linked list lesson (note: go through the linked list lesson to understand the examples provided)

Check out this section of You Don't Know JS: this & Object Prototypes

All the You Don't Know JS Books