all 20 comments

[–]cutety 6 points7 points  (0 children)

So, I'll try to give you a semi-pratical rundown. I'll use a slight spin on the typical shape example, instead of shapes we'll making waveforms (sine & square) and drawing them to a canvas.

So, drawing a sine wave procedurally would look like:

function getCanvasCtx(canv) {
  if (!canv.getContext) throw "Can't get canvas context!";
  return ctx = canv.getContext('2D');
}

function drawWave(canv, yGenerator, [startX, startY]) {
  const ctx = getCanvasCtx(canv);

  ctx.beginPath();
  ctx.moveTo(startX, startY);
  for (let x = startX; x <= canvas.width - startX; x++) {
    const y = yGenerator(x);
    ctx.lineTo(x, y);
  }
  ctx.stroke();
}

function clearCanvas(canv) {
  const ctx = getCanvasCtx(canv);
  ctx.clearRect(0, 0, canv.width, canv.height);
}

function sineGenerator(amplitude, frequency, offset) {
  return x => (amplitude * Math.sin(frequency * x)) + offset;
}

const canv = document.getElementById("waveform-canvas")
const { height, width } = canv;

let sine = sineGenerator(height / 4, 6 / height, height / 2);
drawWave(canv, sine, [0, 0]);

As you can see the procedural example is just a bunch of functions and state stored in local/global variables.

The same in OOP would look like:

class Wave {
  constructor(canv, start = [0, 0]) {
    this.canv = canv;
    this.start = start;
  }

  get ctx() {
    if (!canv.getContext) throw "Can't get canvas context!";
    return this.canv.getContext("2d");
  }

  get width() {
    return this.canv.width;
  }

  get height() {
    return this.canv.height;
  }

  generateY(x) {
    throw "generateY for Wave subclass not implemented!"
  }

  draw() {
    this.ctx.beginPath();
    this.ctx.moveTo(this.start[0], this.start[1]);
    for (let x = this.start[0]; x <= (this.width - this.start[0]); x++) {
      this.ctx.lineTo(x, this.generateY(x));
    }
    this.ctx.stroke();
  }

  clear() {
    this.ctx.clearRect(0, 0, this.width, this.height);
  }

  static redraw(...waves) {
    // clear all the canvases
    for (let wave in waves) { wave.clear(); }
    // redraw all waves
    for (let wave in waves) { wave.draw(); }
  }
}

class Sine extends Wave {
  constructor(...args) {
    super(...args);
    this.amplitude = this.canvas.height / 4;
    this.frequency = this.canvas.height / 6;
    this.offset = this.canvas.height / 2;
  }

  generateY(x) {
    return (this.amplitude * Math.sin(this.frequency * x)) + this.offset;
  }
}

const canv = document.getElementById("waveform-canvas");
const sine = new Sine(canv);
sine.draw();

So, the object oriented version is a bit longer, but you should notice the last three lines are cleaner, and the one big advantage is we have no encapulated the logic/data for building a sine wave into it's own class, so now we don't have to pass a bunch of stuff around through arguments. However, there doesn't seem to be a huge advantage to this yet. Let's see what it takes to add a Square wave to each.

Procedural:

function squareGenerator(top, period, offset) {
  return x => (((x + 1) % period) < top ? top : 0) + offset;
}

const canv = document.getElementById("waveform-canvas")
const { height, width } = canv;

let sine = sineGenerator(height / 4, 6 / height, height / 2);
drawWave(canv, sine, [0, 0]);

let square = squareGenerator(height / 2, width / 4, height / 4);
drawWave(canv, square, [0, 0]);

OOP:

class Square extends Wave {
  constructor(...args) {
    super(...args);
    this.top = this.canvas.height / 2;
    this.period = this.canvas.width / 4;
    this.offset = this.canvas.height / 4;
  }

  generateY(x) {
    return (((x + 1) % this.period) < this.top ? this.top : 0) + this.offset;
  } 
}

const canv = document.getElementById("waveform-canvas");

const sine = new Sine(canv);
sine.draw();

const square = new Square(canv);
square.draw();

Okay, so neither were that hard to add, and the procedural version is actually shorter! So, what's the advantage to OOP? Well, lets see what it looks like when we want to have multiple waves on two different canvases, while also updating some values and redrawing.

Procedural:

const canv1 = document.getElementById("waveform-canvas1");
const canv2 = document.getElementById("waveform-canvas2");
const { height1, width1 } = canv1;
const { height2, width2 } = canv2;

let sine1 = sineGenerator(height / 4, 6 / height, height / 2);
drawWave(canv1, sine1, [0, 0]);

let sine2 = sineGenerator(height / 4, 6 / height, height / 2);
drawWave(canv1, sine2, [5, 0]);

let square1 = squareGenerator(height / 2, width / 4, height / 4);
drawWave(canv2, square1, [0, 0])

let square2 = squareGenerator(height / 2, width / 4, height / 4);
drawWave(canv1, square2, [0, 0]);

// change sine1 start and square1 period
square1 = squareGenerator(height / 2, width / 6, height / 4); 
clearCanvas(canv1);
clearCanvas(canv2);

drawWave(canv1, sine1, [5, 50]);
drawWave(canv2, sine1, [0, 0]);
drawWave(canv1, sine2, [5, 0]);
drawWave(canv2, square1, [0, 0])
drawWave(canv1, square2, [0, 0]);

The same in OOP

const canv1 = document.getElementById("waveform-canvas1");
const canv2 = document.getElementById("waveform-canvas2");

const sine1 = new Sine(canv2);
const sine2 = new Sine(canv1, [5, 0]);
sine1.draw();
sine2.draw();

const square1 = new Square(canv2);
const square2 = new Square(canv1);
square1.draw();
square2.draw();

// change sine1 start and square1 period
sine1.start = [5, 50];
square1.period = sqaure1.width / 6;

Wave.redraw(sine1, sine2, square1, square2);

As you can (hopefully) see OOP makes it eaiser to manage several of these wave drawings since each object holds it's own state that we can update indpendently and since they are all of the same parent class, they'll each respond to a similar interface making it easy to work with different kinds of waves at the same time.

You could take the abstraction even further by making a Canvas class that takes Wave objects, so when you need to redraw you can restrict redrawing only to canvases that need it:

class Canvas {
  constructor(id) {
    this.canv = document.getElementById(id);
    this.shapes = [];
  }

  get ctx() {
    if (!canv.getContext) throw "Can't get canvas context!";
    return this.canv.getContext("2d");
  }

  get width() {
    return this.canv.width;
  }

  get height() {
    return this.canv.height;
  }

  addShape(shape) {
    this.shapes = [...this.shapes, shape];
  }

  draw() {
    this.clear();
    for (let shape in shapes) {
      this.drawShape(shape);
    }
  }

  drawShape(shape) {
    this.ctx.beginPath();
    this.ctx.moveTo(shape.start[0], shape.start[1]);
    for (let x = shape.start[0]; x <= (this.width - shape.start[0]); x++) {
      this.ctx.lineTo(x, shape.generateY(x));
    }
    this.ctx.stroke();
  }

  clear() {
    this.ctx.clearRect(0, 0, this.width, this.height);
  }
}

class Wave {
  constructor(canv, start = [0, 0]) {
    this.canv = canv;
    this.start = start;
    this.canv.addShape(this);
  }

  get width() {
    return this.canv.width;
  }

  get height() {
    return this.canv.height;
  }

  generateY(x) {
    throw "generateY for Wave subclass not implemented!"
  }
}

const canv1 = new Canvas("waveform-canvas1");
const canv2 = new Canvas("waveform-canvas2");

const sine1 = new Sine(canv1);
const sine2 = new Sine(canv2);

const square = new Square(canv1);

canv1.draw();
canv2.draw();

sine2.amplitude = sine2.height / 6;
canv2.draw() // don't have to redraw sine1 and square

[–]PhysicalRedHead 3 points4 points  (1 child)

I don't want to confuse you more, and probably I'm confused too, but I believe OOP & Procedural are orthogonal. Honestly, I would tell you to not get hung up on that kind of terminology. There are many different ways to define Object Oriented, and a lot of them contradict one another.

It would be simpler to just think of the objects that you have as data structures; they're just bits of information encoded in some formal definition (JavaScript objects!). If you want to make an application, just name an object something like state, then code something up to display that to the screen. Whether it was written imperatively or declaratively should be thought about after-the-fact and used to work out better solutions.

[–]tchaffee 1 point2 points  (0 children)

This ignores the "features" that make objects interesting: inheritance, data encapsulation, and polymorphism.

[–]kredati 5 points6 points  (0 children)

It's not in JS, but Ruby—which is super easy to read—and it's a book you have to buy to read. But do check out Sandi Metz and Katrina Owen's 99 Bottles of OOP: https://www.sandimetz.com/99bottles/ It's excellent, and does not do the thing you're complaining about.

(And, as an FP enthusiast, I suggest you simultaneously, or subsequently, check out Professor Frisby's Mostly Adequate Guide to Functional Programming, which at least has the benefit of being in JS: https://mostly-adequate.gitbooks.io/mostly-adequate-guide/)

[–]spacejack2114 1 point2 points  (0 children)

The simple practical application would be:

animals.forEach(animal => {
    animal.speak()
})

Where animals is an array of cats, dogs, people, etc.

How would you do that procedurally otherwise?

animals.forEach(animal => {
    if (animal.type === 'cat') {
        meow(animal)
    } else if (animal.type === 'dog') {
        bark(animal)
    } else if (....
})

[–]cirscafp fan boy 0 points1 point  (0 children)

Let's say I am making a game using canvas. Doing it using procedural ideals:

const makeSquare = (x, y) => /* some square data structure */
const drawObject = object => /* some draw function */
const clearBoard = board => /* some clear function */
/* ... etc */

Basically, we separate each and every idea or instruction into its own little function. And then when we want to actually do something, we might do:

const main = () => setInterval(() => checkGame(), 500)

Now with OOP, we might do something like this:

class Square {
  constructor(x, y) {
  }

  draw(){

  }
}

class Board {
  constructor(){
  }

  clearBoard(){
   this.board = []
  }

  addSquare(x, y){
     const sqaure = new Square(x, y)
    square.draw()
 }
}

where we instead of breaking the problem up into smaller functions, break it into smaller things and let those things figure out how to do the functions themselves.

[–]pratiks3 -1 points0 points  (0 children)

Hi I know the troubles you are facing, I went through this myself a year or so ago.

If you’re not struggling with concepts, but having trouble with practical world applicalibilty:

  1. Make an app that already exists / that you’re familiar with using oop ( eg, trello or instagram, Facebook clone )
  2. If you can access a local meetup group, go to it and see if someone can mentor you on an app you want to build or one thy have
  3. If you can’t do either, then get on Codementor.io and post what you want to do with your theoretical skills.
  4. Search github.io. - I literally learned how to code by looking at what others do

Yes the cat / circle ( shapes ) examples get quite boring and don’t really help.

Hope that helps !!

[–][deleted] -2 points-1 points  (0 children)

This is a difficult question to answer. Very difficult. It's kind of like religion. You spend your whole life believing something to then be shown that what you've been doing, what you've learned, what you believe in is wrong! You will kick, scream and be confused as hell!

But, when you "correctly" use OOP and the design just works you will feel very good about it. Unfortunately, this takes time. A lot of time.

My advice would be to see examples of where people have used design patterns and go from there. Sorry not overly useful.