I'm learning OOP and also trying to learn and implement some design patterns. This question is a follow-up of Stopwatch interface OOP (Vanilla JS)
The app is available in https://nacho-p5.github.io/
Here's the code:
Repo: https://github.com/nacho-p5/nacho-p5.github.io
The community gave me good feedback and I made the following changes to my first codebase:
- Changed syntax to ES6
- Methods added to prototype rather than the instance
- Names changed to be descriptive
- Replaced error handler logic when start/stop are clicked twice
- Time tracker logic changed. The count is calculated using Date.now() rather than counting setInterval iterations
- Script tag position changed to head using defer
- The stopwatch class should not know about the DOM -> Observer pattern implemented
As I wrote in the last point, I refactored the code using the observer pattern. I had two goals doing that:
- Implement the observer pattern
- One class - One thing: separation of concerns in stopwatch, UI and controller class
I know that trying to implement design patterns in a simple problem like this seems like overwork. This is purely for learning purposes.
What do you think about the code? Any feedback will be appreciated :)
stopwatch.js
class Stopwatch {
_timer = 0;
isRunning = false;
startTime = 0;
elapsedTime = 0
observers = []
get timer() {
return this._timer
}
set timer(val) {
this._timer = val
this.notifyController(val)
}
registerObserver(observer) {
this.observers.push(observer);
};
notifyController(val) {
this.observers.forEach(observer => {observer.update(val)})
}
updateTime() {
const newTime = Date.now() - this.startTime + this.elapsedTime;
this.timer = newTime;
};
start() {
if (!this.isRunning) {
this.isRunning = true;
this.startTime = Date.now();
this.setIntervalID = setInterval(this.updateTime.bind(this), 100);
};
};
stop() {
if (this.isRunning) {
clearInterval(this.setIntervalID);
this.isRunning = false;
this.elapsedTime = this._timer;
};
};
reset() {
clearInterval(this.setIntervalID);
this.isRunning = false
this.elapsedTime = 0;
this.startTime = 0;
this.timer = 0;
};
};
class UI {
constructor(displayID, btnStartID, btnStopID, btnResetID) {
// HTML Components
this.buttons = {
start: document.getElementById(btnStartID),
stop: document.getElementById(btnStopID),
reset: document.getElementById(btnResetID)
},
this.display = document.getElementById(displayID)
};
resetAllButtonsStyle() {
Object.values(this.buttons).forEach(e => e.classList.remove('activeBtn'))
};
showButtonAsActive(btn) {
this.resetAllButtonsStyle();
btn.classList.add('activeBtn')
};
updateDisplay(value) {
this.display.innerText = value;
};
}
class Controller {
constructor(sw, ui) {
this.sw = sw;
this.ui = ui;
// Add event listeners
this.ui.buttons.start.addEventListener('click', function() {
sw.start();
ui.showButtonAsActive(this);
});
this.ui.buttons.stop.addEventListener('click', function() {
if (sw.isRunning) {
sw.stop();
ui.showButtonAsActive(this);
};
});
this.ui.buttons.reset.addEventListener('click', function() {
sw.reset();
ui.resetAllButtonsStyle();
});
}
update(val) {
ui.updateDisplay((val/1000).toFixed(3))
}
}
// Initialize classes
const ui = new UI('sw-display', 'btnStart', 'btnStop', 'btnReset');
const sw = new Stopwatch();
const controller = new Controller(sw, ui);
// Register controller in sw
sw.registerObserver(controller);
////////////////////////