Smalltalk MVC Translated to JavaScript

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.

Comments

Have something to write? Comment on this article.

Bert Freudenberg January 5, 2015

Re “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.”

That is actually exactly how it works in Squeak. Class Model has its own dependents list. The implementation using a dictionary in Object exists only as a fallback so that you can use any object as model.

You will notice how the code carefully uses myDependents. This is overridden in Model with an instance-specific collection. Normally you will use a subclass of Model in an MVC application (you will notice that most Smalltalk tools are subclasses of Model). But you don't have to, that is what the myDependents implementation in Object allows.

Peter Michaux January 5, 2015

The question still remains for me: Why does the Object class use a class variable and dictionary of dependents for each instance. Why doesn't Object have an instance variable and array of dependents? If Model does it then Object could too.

Bert Freudenberg January 5, 2015

Object cannot have any inst vars because it is the superclass for all regular objects in the whole system. SmallIntegers, Floats, Strings, everything. Even if it worked, it would be a tremendous waste of space to permanently add a slot to each and every object in the system that is almost never used. Very few objects actually get into the role of being a model. And the vast majority of objects used in that role are Model subclasses.

Indeed, in a regular Squeak image, with a couple tools opened, more than 300 K objects total:

DependentsFields size
==> 0
Model withAllSubclasses inject: 0 into: [:sum :ea | sum + ea instanceCount]
==> 54
Peter Michaux January 5, 2015

Thank you for your insights.

In JavaScript, lazy initialization of a dependents property on the objects wouldn’t have a cost. There is no “slot” in JavaScript.

John January 5, 2015

Smalltalk stores the observer/observable relationship in a dictionary for descendants of Object, because it wouldn’t do to attach another variable to every object for a relationship that only exists for a few objects.

I believe many actual classes (subclasses of Model) do include the observer list as a collection stored in a variable on each object, optimizing for time instead of space.

Joey Guerra January 6, 2015

A team member at work sent me this post because I talk to the team about the MVC and Observer pattern often. Thank you. I thought I was the only person in the world who felt this way, at least from reading the all of the JavaScript frameworks code. In 2005, I was concerned when Ruby on Rails came out, claiming to implement the MVC pattern. And then when ASP.Net MVC came out in 2007 claiming the same thing. It seems like the Lemming Effect took hold and now MVC is associated with technologies that are NOT MVC. I suspect that since it’s a complicated design (seemingly) and I guess people couldn't just read code (Smalltalk) that the design was interpreted in many ways. It just must of not been very clear. I guess. Anyways, I really appreciate they how you directly translated the Smalltalk code to JavaScript. This was helpful with my understanding of the pattern.

Have something to write? Comment on this article.