Zeroes and Ones

Object-Oriented Programming 101. Building an OOP Stopwatch

In the last post, I wrote about objects in Object-Oriented Programming, and as I said in the last paragraph, now I'm going to show the app and go over some concepts with the code. Before anything, let me hit you with the whole app real quick so I can reference the code later easily, here it goes:

const StopWatch = function () {
  // Private
  let initialValue = 0
  let isStopped = true
  let internalDuration = 0

  const computeTime = () => {
    const latestValue = Date.now()
    const timePassed = (latestValue - initialValue) / 1000
    return Math.round(timePassed * 100) / 100
  }

  // Public
  this.start = () => {
    if (isStopped) {
      isStopped = false
      initialValue = Date.now()
    } else {
      throw new Error("The Stopwatch already started")
    }
  }

  this.stop = () => {
    if (!isStopped) {
      isStopped = true
      internalDuration = internalDuration + computeTime()
    } else {
      throw new Error("The Stopwatch already stopped")
    }
  }

  this.reset = () => {
    internalDuration = 0
    initialValue = 0
    isStopped = true
  }

  Object.defineProperty(this, "duration", {
    get: () => internalDuration,
  })
}

module.exports = StopWatch

OK, let's break this app apart. The requirements were that this should have a start method to run the stopwatch, this method shouldn't be called twice in a row. We also need a stop method to stop the count (also shouldn't be called twice in a row), a duration property to check the time that passed, and a reset method to start everything over again with default values. Also, we should be able to stop, start again, and remember the time that passed.

The first OOP decision was to create a constructor function, this enables us to create an object and encapsulate its properties and behavior there (although I could have just defined an object literal, but now I can create multiple independent instances of a stopwatch, also I couldn't have done some things will see later).

Right out the bat, I realized there are some values that I want to keep private, things important for the internal workings of the stopwatch like the computeTime method or the properties like the initialValue and keeping state of whether the watch has stopped or not is not relevant information for someone consuming the stopwatch object. This is a good example of abstraction, complexity is hidden. By the way, computeTime just calculates time that has passed and formats the number a bit.

Going Public

Now we have to take care of the methods that will be exposed, this will be defined like: this.start as opposed to the private version let initialValue, why and how this works was covered in the previous blog post.

The code is very self-explanatory so I won't go into it, I just will mention in the spirit of OOP, how the private methods are used in these public methods. Someone can create a stopwatch instance and run myStopwatch.start() and the object will start, inside it will check for the state of the watch, set an initial value, and more, but all of that although essential for the app, is hidden.

So all of the methods are defined, but something is weird... Now we need something like this.duration to check the time passed, so simple, we can create a public property and that's it... well not so fast cowboy. Someone could simply write this.duration = "a lot of time" and actually change the value of duration and possibly breaking the app. OK, so we declare it like let duration so it's private? Ehh, well, we want to read it so no. Oh, what was that weird thing again? Yes, a getter.

Getters

I actually was excited to have an excuse to use a getter for the first time. What was a getter? Well, it was that thing that helped us get a value ('read' in CRUD terms), but not modify it. So we do something like:

Object.defineProperty(this, "duration", {
  get: () => internalDuration,
})

The first argument of the defineProperty method is where is this property added (in the created object), the second argument is the name of the property, and finally the third argument is an object where get is a special keyword for getter and the method returning the property we want to show. In short and in simple, we say, when someone types this.duration show whatever is in let internalDuration, with this, they can not overwrite the property. Pure beauty.

Testing this thing

Just as an extra, I tested this app with Jest. Somethings are not ideal (my use of setTimeout for one), but it helped me to practice and also have some documentation of the specs of the stopwatch. Also, if I want to modify something in the future, this will help a lot.

const StopWatch = require("./index")

it("start, stop, and reset are public members of sw object", () => {
  const sw = new StopWatch()
  expect(sw.start).toBeDefined()
  expect(sw.stop).toBeDefined()
  expect(sw.reset).toBeDefined()
})

it("initialValue, isStopped, and computeTime are not accesible", () => {
  const sw = new StopWatch()
  expect(sw.initialValue).toBeUndefined()
  expect(sw.isStopped).toBeUndefined()
  expect(sw.computeTime).toBeUndefined()
})

it("duration returns 0 on initiation", () => {
  const sw = new StopWatch()
  const result = sw.duration
  expect(result).toBe(0)
})

it("duration is a getter, it can't be modified", () => {
  const sw = new StopWatch()
  sw.duration = 5
  expect(sw.duration).not.toBe(5)
})

it("you can't call start twice in a row", () => {
  const sw = new StopWatch()
  sw.start()
  expect(() => {
    sw.start()
  }).toThrow("The Stopwatch already started")
})

it("you can't call stop twice in a row", () => {
  const sw = new StopWatch()
  expect(() => {
    sw.stop()
  }).toThrow("The Stopwatch already stopped")
})

it("Duration should return the time passed after stop()", done => {
  const sw = new StopWatch()
  sw.start()
  setTimeout(() => {
    sw.stop()
    expect(sw.duration > 0).toBe(true)
    done()
  }, 100)
})

it("Reset should take stopwatch to initial state", done => {
  const sw = new StopWatch()
  sw.start()
  setTimeout(() => {
    sw.stop()
    sw.reset()
    expect(sw.duration === 0).toBe(true)
    done()
  }, 100)
})

So that was an OOP Stopwatch. Repo is here. The next stop is learning about Prototypes and Inheritance, once I chewed that information I'll blog it. Until then.

Developed by Cristobal Heiss