- Magento 2: Defaults, uiElement, Observables, and the Fundamental Problem of Userland Object Systems
- Magento 2: Javascript Primer for uiElement Internals
- Tracing Javascript’s Prototype Chain
- Magento 2: uiElement Standard Library Primer
- Magento 2: Using the uiClass Object Constructor
- Magento 2: uiClass Internals
I was chatting with Vinai Kopp in my Patron Slack channel the other day, and via a conversation about the listens
default we ended up stumbling across some strange behavior in Magento 2’s uiElement
object system. Beyond being a useful bit of information that working Magento 2 developers need to be aware of, it’s also a great example of the problem with “userland” object systems as well as the perfect framing device for launching our next in-depth series.
Userland Object Systems
First, a quick uiElement
refresher. The uiElement
object system allows end-user-programmers to create javascript constructor functions that inherit from one another. this small program (created and ran in the javascript console) covers the basics.
//normally you'd pull in the RequireJS modules via define
UiElement = requirejs('uiElement');
//here we define a constructor function that inherits everything
//from the UiElement constructor function, with an additional
//default
OurConstructorFunction = UiElement.extend({
'defaults':{
'foo':'bar'
}
});
//here we instantiate an object from
//our new constructor function
object = new OurConstructorFunction;
//here we see the foo property has
//a default value of "bar"
console.log(object.foo);
This is an example of what I like to call a userland object system. While the original concept of userland was invented to talk about OS level programming and kernel memory vs. user memory, over the years its use has expanded to draw a distinction between what sort of things the users of a system can do vs. what’s reserved for the system developers. For example, if you try to look at the constructor function of the base Object
object in javascript
console.log(Object.constructor)
Function() { [native code] }
Your debugger will return the text “native code
“. This native code
is implemented in the javascript engine (V8 in chrome) and is not accessible to userland users.
Javascript, however, is very liberal in what it allows you to do with objects. It’s flexible enough to build new object systems without recompiling a custom V8 javascript runtime (which is fortunate, since it would be hard to get users to install that runtime!).
Magento 2’s uiElement
object system is an example of a userland object system. Magento 1 developers will be familiar with PrototypeJS’s Class
system as another.
The Observables Problem
While userland object systems are neat, useful, and are tools all modern software developers need to be aware of, userland object systems can often lead to unexpected results. Here’s the one Vinai and I ran into.
UiElement = requirejs('uiElement');
ko = requirejs('ko');
//here we define a constructor function that inherits everything
//from the UiElement constructor function, with an additional
//default
OurConstructorFunction = UiElement.extend({
'defaults':{
'foo': ko.observable('A default value')
}
});
object1 = new OurConstructorFunction;
object2 = new OurConstructorFunction;
//view the default observable values
console.log(object1.foo());
console.log(object2.foo());
//change the value of the `object1` observable
//but not the `object2` observable
object1.foo("Changed Value");
//view the observable values, and see that **both** have changed
console.log(object1.foo());
console.log(object2.foo());
If you follow along with the comments, you can see that the foo
default value is, for some reasons, linked between objects. Folks working in languages like Java, C#, or PHP are probably scratching their heads wondering how this could happen. Folks working in languages like Ruby and Python are probably scratching their head, wondering why I’d think the behavior should be otherwise.
The problem here is this
console.log(object1.foo === object2.foo);
true
When used with objects, the ===
operator checks if the two variables refer to the same object. The problem here isn’t a linking of object properties, it’s that the foo
property contains the same object. That’s because the observable is instantiated when we define the new constructor function
'defaults':{
'foo': ko.observable('A default value')
}
When Magento’s core uiElement
object system code assigns defaults, the syntax boils down to object.foo = defaults.foo
. For numbers and strings, this doesn’t present a problem. For objects, it means every assigned default will refer to the same object.
As to whether this is a bug or the right system behavior — that’s hard to say, and it’s one of the biggest problems with userland object systems. Coming from “boilerplate” languages like Java, C#, or PHP, this seems like the wrong behavior. The intent of creating an observable in the object defaults seems to be
Make a new observable for each instantiated object
rather than the behavior we see above.
However, in languages like ruby or python, where class and object creation is a more dynamic affair, the above behavior would seem correct and the blame would lay with the end-user-programmer for forgetting the observables are objects.
Wrap Up
The solution to this fundamental problem is as timeless as it is boring: Good documentation, and a clear understanding of the philosophy of the system creator. While Magento has improved their UI Component documentation, the results read like a desperate translator trying to capture the essences of a fast talking native speaker who’s unaware they’re being observed. This process creates useful artifacts, but seems unlikely to produce the clear understanding programmers require.
That’s why this is the first article in a new series that will dive deeply into the bowels of the uiElement
object system. The benefit of open source userland object systems is that, regardless of their originating culture, anyone willing to spend the time and effort can understand and explain them. That’s what we hope to do with this series.