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.
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.
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
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.
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.
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.
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 inObject
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 inModel
with an instance-specific collection. Normally you will use a subclass ofModel
in an MVC application (you will notice that most Smalltalk tools are subclasses ofModel
). But you don't have to, that is what themyDependents
implementation inObject
allows.