The original Smalltalk MVC is an elegant way to structure an application. Unfortunately, the JavaScript community takes more inspiration from Dr. Frankenstein than from Smalltalk. The community appears to have done its best to saw off many good parts from MVC and bolt on unnecessary ugly bits to create framework monsters unsuitable for building real-world JavaScript applications.

Over the past several years, I've tried to understand why the JavaScript community has done this. My only surviving theory is that no one took the time to go back to the Smalltalk code to actually see how MVC was implemented there. Had they had the interest and patience to do so, rather than jumping in with their own largely unproven ideas, they would have seen a easy path to translating Smalltalk’s MVC into beautifully organized JavaScript applications.

I hope this article will remove the necessary historical digging to read and understand the implementation of Smalltalk MVC. Also, I hope it inspires better JavaScript frameworks and applications.

This article does not teach Smalltalk. You don’t need to be able to read much Smalltalk to read this article. You can make it through the article skipping the Smalltalk and reading the JavaScript translations only. If you are looking for an introduction, the first 89 pages of Smalltalk-80: The Language and its Implementation are a joy to read.

The Smalltalk code is taken directly from the Squeak All-In-One image. I have elided a very few lines of Smalltalk that are unrelated to the MVC aspects of the methods shown. In place of these lines you’ll see a Smalltalk comment "..." .

A nice way to read this article is on a large screen with two browser windows both displaying this article. You can position the Smalltalk and JavaScript versions of the code side-by-side to compare lines easily.

Models with Smalltalk's Object Class

Quite high in Smalltalk’s class hierarchy is the Object class. Almost all classes in Smalltalk inherit from the Object class. Therefore almost all objects in a Smalltalk application have the instance methods defined in Object .

The observable-related methods (i.e. dependents , addDependent: , removeDependent: , changed , changed: , changed:with: ) are defined in Smalltalk’s Object . This means almost all objects in Smalltalk can participate in the application as model objects. That is, they can be observed.

The observer-related methods (i.e. update and update:with: ) are also defined in Smalltalk’s Object class. This means almost all objects in Smalltalk can participate in the application as observers. Typically this ability is exploited by an application’s view objects.

Having both observable-related methods and observer-methods on almost all objects in an application means that almost all objects can communicate in the very decoupled way that the observer pattern enables. This indirect style of communication can be lead to code that is frustratingly difficult to debug. “Where is the call that initiated this error?!” Good choices about which objects should use this communication mechanism are required of the application programmer. The communication between models and views is the main use of the observer pattern in an MVC application. Models observing other models is common too. Some applications might (also) have controllers observing the model but this is much less common.

The Object class has a class variable DependentFields that references an instance of WeakIdentityKeyDictionary . The class WeakIdentityKeyDictionary is not shown here but it is just a dictionary that maps objects to objects and has methods for adding and removing key-value pairs of the dictionary.

So here they are: The fundamental MVC-related parts of Smalltalk’s Object class taken from Squeak. (If the Smalltalk really scares you too much, you can safely skip down to the discussion below and catch up with the JavaScript translation that follows.)

ProtoObject subclass: #Object instanceVariableNames: '' classVariableNames: 'DependentsFields' poolDictionaries: '' category: 'Kernel-Objects'

class methods

class initialization

initialize "Object initialize" DependentsFields ifNil:[self initializeDependentsFields]. initializeDependentsFields "Object initialize" DependentsFields := WeakIdentityKeyDictionary new.

instance methods

dependent access

myDependents "Private. Answer a list of all the receiver's dependents." ^ DependentsFields at: self ifAbsent: [] myDependents: aCollectionOrNil "Private. Set (or remove) the receiver's dependents list." aCollectionOrNil ifNil: [DependentsFields removeKey: self ifAbsent: []] ifNotNil: [DependentsFields at: self put: aCollectionOrNil] dependents "Answer a collection of objects that are 'dependent' on the receiver; that is, all objects that should be notified if the receiver changes." ^ self myDependents ifNil: [#()] addDependent: anObject "Make the given object one of the receiver's dependents." | dependents | dependents := self dependents. (dependents includes: anObject) ifFalse: [self myDependents: (dependents copyWithDependent: anObject)]. ^ anObject removeDependent: anObject "Remove the given object as one of the receiver's dependents." | dependents | dependents := self dependents reject: [:each | each == anObject]. self myDependents: (dependents isEmpty ifFalse: [dependents]). ^ anObject

updating

changed "Receiver changed in a general way; inform all the dependents by sending each dependent an update: message." self changed: self changed: aParameter "Receiver changed. The change is denoted by the argument aParameter. Usually the argument is a Symbol that is part of the dependent's change protocol. Inform all of the dependents." self dependents do: [:aDependent | aDependent update: aParameter] changed: anAspect with: anObject "Receiver changed. The change is denoted by the argument anAspect. Usually the argument is a Symbol that is part of the dependent's change protocol. Inform all of the dependents. Also pass anObject for additional information." self dependents do: [:aDependent | aDependent update: anAspect with: anObject] update: aParameter "Receive a change notice from an object of whom the receiver is a dependent. The default behavior is to do nothing; a subclass might want to change itself in some way." ^ self update: anAspect with: anObject "Receive a change notice from an object of whom the receiver is a dependent. The default behavior is to call update:, which by default does nothing; a subclass might want to change itself in some way." ^ self update: anAspect

It is particularly odd to me that the dependents of all instances are stored in a dictionary that is a class variable. I do not understand the motivation for this. I would have made it so that each instance has its own private array of dependents.

A Simple Translation to JavaScript

The code that follows is a translation of the above Smalltalk Object observer-pattern-related methods to plain old JavaScript. By “old JavaScript” I mean ECMAScript 3. This translation could actually be used in a real JavaScript application.

In Smalltalk, almost all objects have the addDependent: , changed , update: , etc methods because almost all classes inherit from Smalltalk’s Object class. In JavaScript, we could add similar properties Object.prototype.addDependent , Object.prototype.changed , Object.prototype.update , etc so that almost all objects in JavaScript have these methods. Unfortunately, since ECMAScript 3 did not allow us to add non-enumerable properties to an object, adding these properties to Object.prototype would likely break for-in loops in our applications. Instead of all those headaches, we create a new Model constructor function. Either prototype-based inheritance or mixins can be used to add the model functionality to any other constructor that needs it.

We need a dictionary object of some type assigned to Model.dependentsFields to store all dependents of each Model instance. We cannot use an empty JavaScript object as the dictionary because JavaScript objects only allow string keys. This dictionary needs object keys. We will assume the presence of an IdentityKeyDictionary constructor function. (Note that the name has changed slightly from Smalltalk’s WeakIdentityKeyDictionary because ECMAScript 3 didn’t have any kind of weak references.) Implementation of this IdentityKeyDictionary constructor would be relatively straight forward and use an array to store object keys and their associated object values.

function Model() {} Model.initialize = function () { this.initializeDependentsFields(); }; Model.initializeDependentsFields = function () { this.dependentsFields = new IdentityKeyDictionary(); }; Model.prototype.myDependents = function (aCollectionOrNil) { if (arguments.length < 1) { return Model.dependentsFields.atIfAbsent(self, function () {}); } else { if (aCollectionOrNil) { Model.dependentsFields.atPut(self, aCollectionOrNil); } else { Model.dependentsFields.removeKeyIfAbsent(self, function () {}); } } }; Model.prototype.dependents = function () { var dependents = this.myDependents(); return dependents ? dependents : []; }; Model.prototype.addDependent = function (anObject) { var dependents; dependents = this.dependents(); if (!dependents.includes(anObject)) { this.myDependents(dependents.copyWithDependent(anObject)); } return anObject; }; Model.prototype.removeDependent = function (anObject) { var dependents; dependents = this.dependents().reject(function (each) {return each === anObject;}); this.myDependents(dependents.isEmpty() ? null : dependents) }; Model.prototype.changed = function (aParameter) { if (arguments.length < 1) { aParameter = this; } var dependents = this.dependents(); for (var i = 0, ilen = dependents.length; i < ilen; i++) { dependents[i].update(aParameter); } }; Model.prototype.changedWith = function (anAspect, anObject) { var dependents = this.dependents(); for (var i = 0, ilen = dependents.length; i < ilen; i++) { dependents[i].updateWith(anAspect, anObject); } }; Model.prototype.update = function (aParameter) { return this; }; Model.prototype.updateWith = function (anAspect, anObject) { this.update(anAspect); };

Note that somewhere in the program before using the Model class, there must be a call Model.initialize(); . Given this awkward requirement, we would almost surely write the JavaScript differently. The main goal of any other strategy would be to make an initialization call unnecessary.

First option: We could remove the definitions of the Model.initialize and Model.initializeDependentsFields functions. Then add a simple assignment that runs when the code is initially loaded and evaluated.

Model.dependentsFields = new IdentityKeyDictionary();

Second option: We could remove the definitions of the Model.initialize and Model.initializeDependentsFields functions. Then change Model.prototype.myDependents to lazily create the Model.dependentsFields dictionary.

Model.prototype.myDependents = function (aCollectionOrNil) { if (!Model.dependentsFields) { Model.dependentsFields = new IdentityKeyDictionary(); } if (arguments.length < 1) { return Model.dependentsFields.atIfAbsent(self, function () {}); } else { if (aCollectionOrNil) { Model.dependentsFields.atPut(self, aCollectionOrNil); } else { Model.dependentsFields.removeKeyIfAbsent(self, function () {}); } } };

Views with Smalltalk’s View Class

Smalltalk’s View class is a meaty one because it participates in the three main design patterns of the MVC architecture. It participates in the observer pattern by observing its model. It participates in the strategy pattern by delegating decision making to its controller. It participates in the composite pattern by being a super view and having sub views.

Object subclass: #View instanceVariableNames: 'model controller superView subViews transformation viewport window displayTransformation insetDisplayBox borderWidth borderColor insideColor boundingBox' classVariableNames: '' poolDictionaries: '' category: 'ST80-Framework'

instance methods

initialize-release

initialize "Initialize the state of the receiver. Subclasses should include 'super initialize' when redefining this message to insure proper initialization." self resetSubViews. "..." release "Remove the receiver from its model's list of dependents (if the model exists), and release all of its subViews. It is used to break possible cycles in the receiver and should be sent when the receiver is no longer needed. Subclasses should include 'super release.' when redefining release." model removeDependent: self. model := nil. controller release. controller := nil. subViews ~~ nil ifTrue: [subViews do: [:aView | aView release]]. subViews := nil. superView := nil

model access

model "Answer the receiver's model." ^model model: aModel "Set the receiver's model to aModel. The model of the receiver's controller is also set to aModel." self model: aModel controller: controller

controller access

controller "If the receiver's controller is nil (the default case), answer an initialized instance of the receiver's default controller. If the receiver does not allow a controller, answer the symbol #NoControllerAllowed." controller == nil ifTrue: [self controller: self defaultController]. ^controller controller: aController "Set the receiver's controller to aController. #NoControllerAllowed can be specified to indicate that the receiver will not have a controller. The model of aController is set to the receiver's model." self model: model controller: aController defaultController "Answer an initialized instance of the receiver's default controller. Subclasses should redefine this message only if the default controller instances need to be initialized in a nonstandard way." ^self defaultControllerClass new defaultControllerClass "Answer the class of the default controller for the receiver. Subclasses should redefine View|defaultControllerClass if the class of the default controller is not Controller." ^Controller model: aModel controller: aController "Set the receiver's model to aModel, add the receiver to aModel's list of dependents, and set the receiver's controller to aController. Subsequent changes to aModel (see Model|change) will result in View|update: messages being sent to the receiver. #NoControllerAllowed for the value of aController indicates that no default controller is available; nil for the value of aController indicates that the default controller is to be used when needed. If aController is neither #NoControllerAllowed nor nil, its view is set to the receiver and its model is set to aModel." model ~~ nil & (model ~~ aModel) ifTrue: [model removeDependent: self]. aModel ~~ nil & (aModel ~~ model) ifTrue: [aModel addDependent: self]. model := aModel. aController ~~ nil ifTrue: [aController view: self. aController model: aModel]. controller := aController

subView access

subViews "Answer the receiver's collection of subViews." ^subViews resetSubViews "Set the list of subviews to an empty collection." subViews := OrderedCollection new firstSubView "Answer the first subView in the receiver's list of subViews if it is not empty, else nil." subViews isEmpty ifTrue: [^nil] ifFalse: [^subViews first] lastSubView "Answer the last subView in the receiver's list of subViews if it is not empty, else nil." subViews isEmpty ifTrue: [^nil] ifFalse: [^subViews last]

subView inserting

addSubView: aView "Remove aView from the tree of Views it is in (if any) and adds it to the rear of the list of subViews of the receiver. Set the superView of aView to be the receiver. It is typically used to build up a hierarchy of Views (a structured picture). An error notification is generated if aView is the same as the receiver or its superView, and so on." self addSubView: aView ifCyclic: [self error: 'cycle in subView structure.'] addSubView: aView ifCyclic: exceptionBlock "Remove aView from the tree of Views it is in (if any) and add it to the rear of the list of subViews of the receiver. Set the superView of aView to be the receiver. It is typically used to build up a hierarchy of Views (a structured picture). An error notification is generated if aView is the same as the receiver or its superView, and so on." (self isCyclic: aView) ifTrue: [exceptionBlock value] ifFalse: [aView removeFromSuperView. subViews addLast: aView. aView superView: self]

subView removing

removeSubView: aView "Delete aView from the receiver's list of subViews. If the list of subViews does not contain aView, create an error notification." subViews remove: aView. aView superView: nil. "..." removeSubViews "Delete all the receiver's subViews." subViews do: [:aView | aView superView: nil. "..."]. self resetSubViews removeFromSuperView "Delete the receiver from its superView's collection of subViews." superView ~= nil ifTrue: [superView removeSubView: self]

superView access

isTopView "Answer whether the receiver is a top view, that is, if it has no superView." ^superView == nil superView "Answer the superView of the receiver." ^superView topView "Answer the root of the tree of Views in which the receiver is a node. The root of the tree is found by going up the superView path until reaching a View whose superView is nil." superView == nil ifTrue: [^self] ifFalse: [^superView topView]

private

superView: aView "Set the View's superView to aView and unlock the View (see View|unlock). It is sent by View|addSubView: in order to properly set all the links." superView := aView. "..." isCyclic: aView "Answer true if aView is the same as this View or its superView, false otherwise." self == aView ifTrue: [^true]. self isTopView ifTrue: [^false]. ^superView isCyclic: aView

updating

update "Normally sent by the receiver's model in order to notify the receiver of a change in the model's state. Subclasses implement this message to do particular update actions. A typical action that might be required is to redisplay the receiver." self update: self update: aParameter "Normally sent by the receiver's model in order to notify the receiver of a change in the model's state. Subclasses implement this message to do particular update actions. A typical action that might be required is to redisplay the receiver." ^self

A Simple Translation to JavaScript

Let’s rewrite Smalltalk’s View class in JavaScript.

function View() {} View.prototype.initialize = function() { this.resetSubViews(); }; View.prototype.release = function () { this._model.removeDependent(this); this._model = null; this._controller.release(); this._controller = null; if (this._subViews) { for (var i = 0, ilen = this._subViews.length; i < ilen; i++) { this._subViews[i].release(); } } this._subViews = null; this._superView = null; }; View.prototype.model = function (aModel) { if (arguments.length < 1) { return this._model; } else { this.modelController(aModel, this._controller); } }; View.prototype.controller = function (aController) { if (arguments.length < 1) { if (!this._controller) { this._controller(this.defaultController()); } return this._controller; } else { this.modelController(this._model, aController); } }; View.prototype.defaultController = function () { return new (this.defaultControllerClass())(); }; View.prototype.defaultControllerClass = function () { return Controller; }; View.prototype.modelController = function (aModel, aController) { if (this._model && (this._model !== aModel)) { this._model.removeDependent(this); } if (aModel && (aModel !== this._model)) { aModel.addDependent(this); } this._model = aModel; if (aController) { aController.view(this); aController.model(aModel); } this._controller = aController; }; View.prototype.subViews = function () { return this._subViews; }; View.prototype.resetSubViews = function () { this._subViews = []; }; View.prototype.firstSubView = function () { return this._subViews[0]; }; View.prototype.lastSubView = function () { return this._subViews[this._subViews.length - 1]; }; View.prototype.addSubView = function (aView) { this.addSubViewIfCyclic(aView, function () { throw new Error('cycle in subView structure.'); }); }; View.prototype.addSubViewIfCyclic = function (aView, exceptionBlock) { if (this.isCyclic(aView)) { exceptionBlock(); } else { aView.removeFromSuperView(); this._subViews.push(aView); aView.superView(this); } }; View.prototype.isCyclic = function(aView) { if (this === aView) { return true; } if (this.isTopView()) { return false; } return this._superView.isCyclic(aView); }; View.prototype.removeSubView = function (aView) { for (var i = 0, ilen = this._subViews.length; i < ilen; i++) { if (aView === this._subViews[i]) { this._subViews.splice(i, 1); break; } } aView.superView(null); }; View.prototype.removeSubViews = function () { for (var i = this._subViews.length; i--; ) { this._subViews[i].superView(null); } this.resetSubViews(); }; View.prototype.removeFromSuperView = function () { if (this._superView) { this._superView.removeSubView(this); } }; View.prototype.superView = function (aView) { if (arguments.length < 0) { return this._superView; } else { this._superView = aView; } }; View.prototype.isTopView = function () { return !!this._superView; }; View.prototype.topView = function () { return this._superView ? this._superView.topView() : this; };

Controllers with Smalltalk's Controller Class

The final member of the MVC class trio, is Controller . A controller’s behavior is almost completely application dependent and so the class is quite small with simple model and view properties and accessors. It is up to the application programmer to flesh out things in subclasses.

Object subclass: #Controller instanceVariableNames: 'model view sensor deferredActionQueue lastActivityTime' classVariableNames: 'MinActivityLapse' poolDictionaries: '' category: 'ST80-Controllers'

instance methods

initialize-release

release "Breaks the cycle between the receiver and its view. It is usually not necessary to send release provided the receiver's view has been properly released independently." model := nil. view ~~ nil ifTrue: [view controller: nil. view := nil]

model access

model "Answer the receiver's model which is the same as the model of the receiver's view." ^model model: aModel "Controller|model: and Controller|view: are sent by View|controller: in order to coordinate the links between the model, view, and controller. In ordinary usage, the receiver is created and passed as the parameter to View|controller: so that the receiver's model and view links can be set up by the view." model := aModel

view access

view "Answer the receiver's view." ^view view: aView "Controller|view: and Controller|model: are sent by View|controller: in order to coordinate the links between the model, view, and controller. In ordinary usage, the receiver is created and passed as the parameter to View|controller: and the receiver's model and view links are set up automatically by the view." view := aView

A Simple Translation to JavaScript

Translating Smalltalk’s Controller class to JavaScript we have

function Controller() {} Controller.prototype.release = function () { this._model = null; if (this._view) { this._view.controller(null); this._view = null; } }; Controller.prototype.model = function (aModel) { if (arguments.length < 1) { return this._model; } else { this._model = aModel; } }; Controller.prototype.view = function (aView) { if (arguments.length < 1) { return this._view; } else { this._view = aView; } };

Summary

So there you have the three main classes in the MVC architecture as they were written in Smalltalk decades ago and translated to JavaScript. As you can see, these are relatively simple classes. With experience over the past few years, JavaScript versions of these classes in the Maria framework have shown me why MVC is deservingly the most famous architecture for applications with user interfaces. I think it is unfortunate that most JavaScript developers have only been exposed to one of the three MVC design patterns, the observer pattern, in popular MV* frameworks.

If you enjoyed this article, I think you might also like the following.