Pairing with JavaScript developers this week, one of the things that struck me is how much many of us now rely on heavyweight frameworks to do sometimes quite simple things we used to do the old-fashioned way.
The example we were looking at is how Model-View-Controller is implemented in the web browser. MVC is a pretty simple design pattern, which typically builds on the even simpler Observer pattern. The goal is to have our user interface automatically update when changes are made to the state of our model so it can refresh.
We use observers to make it so that views can be called back when a model object’s state changes, without binding the model directly to the user interface.
So we split our logic into three distinct responsibilities: the model represents the internal data and logic of the application, independent of the user interface. Views represent that internal data and logic to the end user. And controllers respond to user actions and events and forward requests to the model. (In event-driven programming, we call these “event handlers”.)
In 2019, it’s customary for JavaScript developers to use a framework like React or Angular to wire MVC implementations together. But is it really necessary a lot of the time? Can we do MVC without them?
Well, yes we can. Quite easily.
Let’s look at a very simple example: a clock. Here’s our Clock model:
export function Clock() { | |
this.hours = 0; | |
this.mins = 0; | |
this.secs = 0; | |
this.observable = new Observable(); | |
this.reset = () => { | |
this.hours = 0; | |
this.mins = 0; | |
this.secs = 0; | |
this.observable.notify(); | |
} | |
this.tick = () => { | |
this.secs++; | |
if (this.secs == 60) { | |
this.secs = 0; | |
this.mins++; | |
} | |
if (this.mins == 60) { | |
this.mins = 0; | |
this.hours++; | |
} | |
this.observable.notify(); | |
}; | |
this.totalSeconds = () => { | |
return (this.hours * 60 * 60) + (this.mins * 60) + this.secs; | |
} | |
this.addObserver = (observer) => { | |
this.observable.addObserver(observer); | |
} | |
} |
Aside from the core logic, you’ll notice a little extra code to add observers and notify them after the clock’s state has changed. The Observable class takes care of how this is handled.
function Observable(){ | |
this.observers = []; | |
this.notify = () => { | |
for (var observer of this.observers) { | |
observer.update(); | |
} | |
} | |
this.addObserver = (observer) => { | |
this.observers.push(observer); | |
} | |
} |
The flexibility here is that we can add as many observers as we like, and Clock doesn’t need to know who is being notified. So we can have multiple views being updated on every state change.
In this example, we have two. One displays the clock’s state in hours, minutes and seconds.
export function HoursMinsSecsView(clock, element) { | |
clock.addObserver(this); | |
this.clock = clock; | |
this.element = element; | |
this.update = () => { | |
this.element.innerHTML = " <div class=\"row\">\n" + | |
" <div class=\"col-sm-4\">\n" + | |
" <p>Hours: </p> " + this.clock.hours + "\n" + | |
" </div>\n" + | |
" <div class=\"col-sm-4\">\n" + | |
" <p>Minutes: </p> " + this.clock.mins + "\n" + | |
" </div>\n" + | |
" <div class=\"col-sm-4\">\n" + | |
" <p>Seconds: </p> " + this.clock.secs + "\n" + | |
" </div>\n" + | |
" </div>\n" | |
}; | |
} |
And another displays the total number of seconds elapsed.
export function TotalSecondsView(clock, element) { | |
clock.addObserver(this); | |
this.clock = clock; | |
this.element = element; | |
this.update = () => { | |
this.element.innerHTML = " <div class=\"row\">\n" + | |
" <div class=\"col-sm-4\">\n" + | |
" <p>Total Seconds: </p> " + this.clock.totalSeconds() + "\n" + | |
" </div>\n" + | |
" </div>\n" | |
}; | |
} |
The views inject the inner HTML into a named placeholder in the web page.
<!– H:M:S view of clock –> | |
<div id="hms_view" class="container"> | |
</div> | |
<p></p> | |
<!– Total Seconds view of clock –> | |
<div id="total_seconds_view" class="container"> | |
</div> | |
<p></p> | |
<div class="container"> | |
<button id="reset" class="btn-primary" type="submit">Reset</button> | |
</div> |
And when it’s displayed, it looks like this:
Every time the clock ticks, both views are updated. This can allow us to build very reactive user experiences.
When the user clicks the Reset button, the clock is set back to 0:0:0 and starts ticking again. This is handled by a ClockController that is wired as a listener (another name for an observer) to the Reset button’s click event.
export function ClockController(clock){ | |
this.clock = clock; | |
this.reset = event => { | |
this.clock.reset(); | |
} | |
} |
Note that all the controller does is forward the user’s request to the Clock object. It’s important controllers (and services) don’t include any core logic. Their job is purely to foward the request to wherever that core logic is implemented.
It’s all wired together from the outside using dependency injection.
import {Clock} from "./model/clock.js" | |
import {HoursMinsSecsView} from "./views/hoursminssecsview.js" | |
import {TotalSecondsView} from "./views/totalsecondsview.js"; | |
import {ClockController} from "./controllers/clockcontroller.js"; | |
const clock = new Clock(); | |
const controller = new ClockController(clock); | |
document.getElementById("reset").addEventListener("click", controller.reset); | |
const hoursMinsSecsView = new HoursMinsSecsView(clock, document.getElementById("hms_view")); | |
const totalSecondsView = new TotalSecondsView(clock, document.getElementById("total_seconds_view")); | |
setInterval(clock.tick, 1000); |
Notice that all the implementaton dependencies are in this module. This is called Inversion of Control – the runtime order of implementation method calls in our MVC flow is determined by dependency injection, from above. This offers a lot of flexibility, as do the implicit abstracted invocations of the Observer pattern.
(Notice, too, how the document elements into which the view’s content will be injected are passed in as references to each view, allowing us to have multiple instances on the same web page. By this mechanism, we could also pass views into views, enabling us to create composite views.)
For additional flexibility, some developers use a Publish-Subscribe pattern instead of Observer. This can enable multiple threads and even multiple networked machines to receive notifications. Being asynchronous, it can scale MVC for high-volume distributed architectures. Observer has the limitation of being sychronous – typically (though it doesn’t have to be) – and of requiring observers to be in the same process as the observed. Having said all that, in 95% of applications, Observer is perfectly adequate (and considerably simpler).
And model observers don’t have to be views. I’ve used the same pattern in object persistence. If you think about it, a row in a database table is just another view of a model object. With a bit of adaptation, a Unit of Work – an object that captures such events – could be notified of a state change in a model object.
To sum up, then: MVC is pretty easy to do in vanilla JavaScript. Arguably it’s no easier when using the big front-end frameworks, and they can add a lot of extra code to your web page. You don’t need to buy the whole Mercedes if you just want the cigarette lighter.
(Okay, so if you look at the source code for my example, I have added Bootstrap, for prettifying my web page. Mea culpa!)