Skip to content

Latest commit

Β 

History

History
601 lines (400 loc) Β· 13.3 KB

File metadata and controls

601 lines (400 loc) Β· 13.3 KB

ACS 1320 - Lesson 3 - OOP

Overview 🌏

Class Objects and OOP. Use Object Oriented programming techniques to make your code modular and organized.

You've written lots of code so far you've probably incurred some technical debt. It's time to pay this off by refactoring.

Learning Objectives πŸ₯Έ

  • Use Refactoring to improve code quality
  • Build systems with Objects
  • Define classes
  • Use dependency injection

Refactoring Code 🍜

The goal of refactoring code in short is to improve your existing code base and put it into a shape that will accept future updates.

Refactoring is not about adding new features! Instead, you want to have the same functionality with an improved codebase underneath it.

πŸ†

Creating classes βš’οΈ

In JavaScript a Class starts as a function and a function is an object. Seriously, try this:

function FunkyTown() {
  console.log("Won't you take me to")
}

FunkyTown.band = 'Lipps Inc'

console.log(FunkyTown.band) // Lipps Inc
console.log(FunkyTown())    // "Won't you take me to"

A function is also a class! Yep, it sure is. Try this:

const myTown = new FunkyTown()
console.log(myTown) // {} <-- logs an empty object

Using the key word new in front of the function name returns a new empty object!

Wait, don't instances need some properties? Let's make a dog! 🐢

function Dog(name) {
  this.name = name
}

const myDog = new Dog('Spot')
console.log(myDog.name) // Spot

This creates a new object and assigns it to this.

// It's like this is happening in the background
new Dog() // this = {}

See how name got assigned to this in the Dog function?

The Dog function is the constructor for your class.

How do we add a method?

function Dog(name) {
  this.name = name
}

Dog.prototype.bark = function() {
  console.log(`${this.name} says: gimme a biscuit!`)
}

const myDog = new Dog('Spot')
console.log(myDog.name) // Spot
myDog.bark() // Spot says: gimme a biscuit!

Methods are added to the prototype property! You must use a function type function. Arrow functions don't work here!

Q: I heard you could use the class keyword? πŸ€”

A: Yes you can it's a new syntax. It's better, you should use it. πŸ’ͺ

Q: Is it different? πŸ€”

A: Nope, it's the same thing. We call it "syntactical sugar." 🍰 It's a nicer flavor of the same old thing.

How to make a Dog class πŸ• with the class keyword:

class Dog {
  constructor(name) {
    this.name = name
  }

  bark() {
    console.log(`${this.name} says: gimme a biscuit!`)
  }
}

const myDog = new Dog('Spot') // creates a new dog instance and calls the constructor. 
console.log(myDog.name) // Spot
myDog.bark() // Spot says: gimme a biscuit!

NOTE! We used the constructor() function to initialize the class.

Notice that all of the initial property values (name) are passed into the constructor function and assigned to this. You must assign values to this to store a value on the newly created class instance.

What about inheritence? πŸ‘©β€πŸ‘§β€πŸ‘¦

You can do that with JS. SpaceDogs can shoot lasers from their eyes.

class SpaceDog extends Dog {
  constructor(name, planet) {
    super(name)
    this.planet = planet
  }

  shootLaser() {
    console.log(`${this.name} shoots lasers!`)
  }
}

const spaceDog = new SpaceDog('Rocko', 'Mars')
console.log(spaceDog.name) // Rocko
spaceDog.bark() // Rocko says: gimme a biscuit!
spaceDog.shootLaser() // Rocko shoots lasers!
spaceDog.planet // Mars

We use the extends keyword to create a subclass of a prarent or super class.

NOTE! You must call super() in the constructor. πŸ¦Ήβ€β™€οΈ

Note! Calling super() you must pass any properties required by consructor of the super class. This is how name is set in our super class Dog. πŸ¦Έβ€β™‚οΈ

Modules πŸ“¦

Modules allow you to organize code. Modules allow us to import and export elements. Without modules all of your code is global!

Be sure to declare your script as a module:

Edit index.html:

<script src="main.js" type="module"></script>

Add type="module"

Use import and export to share things between modules.

Export your Dog from Dog.js:

//Dog.js
class Dog() { ... }

export default Dog // Exports Dog from this module

Import your Dog into main.js:

//main.js
import Dog from './Dog.js' // Imports Dog from Dog.js

Creating Classes for Breakout

The engineering team has decided to OOPify the whole game.

You are in charge of the refactor.

You need to refactor this game as Object Oriented. This will allow us to add new features in the future.

You need to make a class for each of the game objects.

  • Brick
  • Ball
  • Paddle
  • Score
  • Lives

Objects give you an abstract way to think about and visualize your code. Rather than having global variables that represent the ball and the paddle you will have objects that represent these and the variables that control their behavior now exist inside instances of these classes.

You'll be making a Class for each. Define properties in each class with the values that the object needs to do it's job.

Sprite class

A Sprite is a game object. Think about it like a rectangle on the screen. The game is built with Sprites. A rectangle has a location, size and color.

Everything on the screen has an x and y position and most things have a width and height and a color. Sprite class should define the properties:

  • x
  • y
  • width
  • height
  • color
class Sprite {
  constructor(x = 0, y = 0, width = 100, height = 100, color = '#f00') {
    this.x = x
    this.y = y
    this.width = width
    this.height = height
    this.color = color
  }
}

export default Sprite

A Sprite class should have a render() method. This method needs to take in the canvas context as a parameter since this is required to draw the object.

class Sprite {
  ...

  render(ctx) {
    ctx.beginPath();
    ctx.rect(this.x, this.y, this.width, this.height);
    ctx.fillStyle = this.color;
    ctx.fill();
    ctx.closePath();
  }
}

Q: Why pass the ctx as a parameter it's a global variable?

A: We want to avoid global variables! These open our code up to problems.

It also means the code only works when this mysterious value ctx happens to be defined in the global scope. If you were to use this class in another project error messages would start asking you why the mysterious ctx is not defined.

Passing ctx as a parameter is safe and reliable, the variable is scoped to this method, any programmer can see where it is defined and decide how they will provide it.

Brick Class 🧱

The Brick class can extend the Sprite class. Brick adds a new property status to Sprite. A brick can also initialize itself to fixed expected values. See the width, height, and color.

class Brick extends Sprite {
  constructor(x, y, width = 75, height = 20, color = '#0095DD') {
    super(x, y, width, height, color) // pass arguments to Sprite!
    this.status = true // adds a new property
  }
}

A brick is just a Sprite with an extra property called status and some standard values for width, height, and color.

Use the brick class! The bricks are stored in an array and initialized like this in the original code:

// original code 
var bricks = [];
for (var c = 0; c < brickColumnCount; c++) {
  bricks[c] = [];
  for (var r = 0; r < brickRowCount; r++) {
    bricks[c][r] = { x: 0, y: 0, status: 1 };
  }
}

The brick object is here:

bricks[c][r] = { x: 0, y: 0, status: 1 };

The Object here has the properties: x, y, status.

{ x: 0, y: 0, status: 1 }

We can now replace this with:

// New code! 
bricks[c][r] = new Brick(0, 0) // Make an instance of Brick

Here the two parameters are the x and y.

Let's be clear!

{ x: 0, y: 0, status: 1 }

// and 

new Brick(0, 0)

Make an object with the same properties.

Ball class ⚽️

A Ball class might add properties of dx, dy, and radius to Sprite.

The Ball class might look like this:

class Ball extends Sprite {
  constructor(x = 0, y = 0, radius = 10, color = "#0095DD") {
    super(x, y, 0, 0, color)
    this.radius = radius;
    this.dx = 2
    this.dy = -2
  }

  move() {
    this.x += this.dx
    this.y += this.dy
  }

  render(ctx) { // Overrides the existing render method!
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.fillStyle = this.color;
    ctx.fill();
    ctx.closePath();
  }
}

Here Ball Class defines instances which will have four properties. Two of the properties, radius, and color are assigned when the Ball is initialized. color has a default value.

  • color: the color the ball will render as
  • radius: the size of the ball measured as it's radius
  • x: the position of the ball on the x-axis of a canvas
  • y: the position of the ball on the y-axis of a canvas

Notice the Ball Overrides the render method since it must draw itself as a circle! Remember Sprites only draw a rectangle.

Making an instance

Make instance of a class like this:

const ball = new Ball(200, 200, 10)

console.log( ball.x ) // 200
console.log( ball.y ) // 200
console.log( ball.radius ) // 10

What about drawing the ball?

Objects like: Ball, Brick, and Paddle own all of the properties they need to render themselves on canvas.

It makes sense they should own their render method.

These objects should have a render() method.

class Ball {
  ...
  render(ctx) {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.fillStyle = this.color;
    ctx.fill();
    ctx.closePath();
  }
}

Important: The render method should take the canvas context as a parameter.

What about global vars?

You should try to minimize the use of global vars. The original tutorial used many global variables.

  • Q: What global variables were used in the original tutorial?
  • Q: Where are these values now that you have refactored your code into classes?

The canvas context ctx is a global variable in the original code. To reference a global variable in a class is bad practice. Imagine you needed to change the name of that variable, you'd have to make lots changes. What if you wanted to use this class in another project, that project would have to define a variable with the same name.

A better way to handle this is to pass the dependancy into the class as a parameter.

...
const ctx = canvas.getContext('2d')
...
class Ball {
  ...
  render(ctx) { // pass ctx here! 
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.fillStyle = this.color;
    ctx.fill();
    ctx.closePath();
  }
}

const ball = new Ball()
ball.render(ctx) // pass the global ctx to ball.render()

Dependency Injection πŸ’‰

Call this: dependency injection

...
const ctx = canvas.getContext('2d')

class Ball {
  ...
  render(ctx) {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.fillStyle = this.color;
    ctx.fill();
    ctx.closePath();
  }
}

const b = new Ball(200, 200, 10)
b.render(ctx) // pass the cts as an argument! 

Calling methods and passing dependencies.

...
const ctx = canvas.getContext('2d')
const ball = new Ball(...)
...

function draw() {
  ball.move()
  ball.render(ctx) // pass the dependency!
  ...
}

Lab

Start working on Assignment 3

Classes

Read up on classes in the JS docs:

After Class

Additional Resources

  1. Video Playlist walking through the entire assignment: https://www.youtube.com/playlist?list=PLoN_ejT35AEiSYr-OhYV-C6uWZgPLBMZM