Managing States via Prototypal Inheritance
It’s been quite few years I’ve mentioned now and then a prototypal inheritance based pattern to handle generic objects or components state.
Every time I mention such pattern, developers look at me confused, since they’ve never read, thought about, or used prototypes to handle states, and it probably makes little sense to them.
It’s the end of the year, and I feel like I should finally talk about this pattern that’s “so difficult to understand“ … it literally fits in a tweet!
let State=s=>O.assign({},s),O=Object,p=O.getPrototypeOf
— Andrea Giammarchi (@WebReflection) December 22, 2016
State['next']=(a,b)=>O.setPrototypeOf(S(b),a)
State.prev=s=>(s=p(s),s==p({})?null:s)
… and in case you are guessing, the reason I wrote State['next']
in there, is that otherwise Twitter would think it’s a link, penalizing my chars count (how dare they)!
Wait … What ?
Please bear with me, and let me explain after a readable ES5.1 compatible example:
function State(setup/* optional */) { 'use strict';
Object.assign(this, setup);
}
State.next = function next(state, setup/* optional */) {
return Object.setPrototypeOf(new State(setup), state);
};
State.prev = function prev(state) {
var previous = Object.getPrototypeOf(state);
return previous === State.prototype ? null : previous;
};
Here few very simple concepts introduced by that simple State
constructor:
- enumerability is the source of truth per each state, anything else doesn’t matter
- inheritance enables rollbacks in a single
State.prev(current)
call - zero name clashes thanks to a clean prototype that could be even cleaner just adding one
State.prototype = Object.create(null);
line - branches are possible, starting from a single state, where every branch is eventually capable of rolling back to their initial matching point with other branches, or just the root
Is it still not so clear? OK, let’s have a practical example:
// imagine a time traveler capable state !!!
let now = new State({time: Date.now()});
let watch = setInterval(
() => now = State.next(now, {time: Date.now()}),
1000
);
// or ... imagine just a person ...
class Person {
constructor() {
this.state = new State({
age: 0,
name: ''
});
}
updateState(data) {
this.state = State.next(this.state, data);
// eventually notify a state update
}
birthday() {
this.updateState({
age: this.state.age + 1
});
}
}
// ... that needs to register as citizen ...
class Municipality extends Bureaucracy {
register(person, name) {
return new Promise((res, rej) => {
if (/^\S[\S ]*?\S/.test(name)) {
super
.register(person, name)
.then(res, rej)
;
} else {
rej(new Error(`Invalid name: ${name}`));
}
});
}
}
// so, imagine me at age 0 !
const me = new Person();
// born in Ancona, Italy
const Ancona = new Municipality();
// registering for the first time
Ancona
.register(me, 'Andrea')
.then((name) => {
me.updateState({name});
})
.catch(console.error)
;
How to retrieve all keys from a state ?
That’s a good question, and the answer is straight forward:
State.keys = function keys(state) {
var keys = [], k;
for (k in state) keys.push(k);
return keys;
};
Above solution will probably make some old style linter affectionate’s eyes bleed.
All I can say is that there is absolutely nothing wrong with that logic, but they could go on moaning forever, so here it goes the slowed down, for no reason whatsoever, alternative:
State.keys = function keys(state) {
var keys = [], set;
while (state !== State.prototype) {
// use Reflect.ownKeys if you think you need it
keys.push.apply(keys, Object.keys(state));
state = Object.getPrototypeOf(state);
}
return Object.keys(
keys.reduce(
function (o, key) {
o[key] = 1;
return o;
},
{}
)
);
};
Happy now ?
OK, but how can I diff changes ?
The only difference between an updated state and its previous one, is the amount of own properties.
Basically, all you need to diff between two adjacent states is Object.keys(updatedState)
, which will inevitably reveal only properties that have been updated.
If these two states are batched, meaning there are intermediate states in between, it’s still quite straight forward to diff properties meaningful for an update.
// how to know what to update ?
function diff(prev, curr) {
const keys = [];
while (curr !== prev) {
keys.push.apply(keys, Object.keys(curr));
curr = Object.getPrototypeOf(curr);
}
return new Set(keys).values();
}
// let's try it
var state = new State();
state = State.next(state, {one:1});
state = State.next(state, {two:2});
var two = state;
state = State.next(state, {three:3});
state = State.next(state, {four:4});
state = State.next(state, {five:5});
var five = state;
// is it really that simple ?
for (let change of diff(two, five)) {
console.log(change);
}
// logged out:
// five
// four
// three
What about immutability ?
If you want it, you can either create your own version:
function State(setup/* optional */) { 'use strict';
Object.freeze(Object.assign(this, setup));
}
or you could overwrite next
method only to return a frozen object.
State.next = (function (next) {
return function () {
Object.freeze(next.apply(this, arguments));
};
}(State.next));
Funny I am suggesting to mutate a method to have immutability … anyway …
As Summary
Using prototypal inheritance to solve in a different way states chainability seems worth for various use cases and also performance reasons.
Where this solution shines:
- occasional changes with history tracking
- many changes at once through single swap state
- many state updates with little changes that require time-traveling rollbacks (video games break-points and similar cases)
Where this solution fails
- deep changes tracking instead of shallow ones (shallows are the most common thought)
- fast states updates leads to hold too many objects which is not ideal, unless under control
The second point could have a simple helper, such:
// returns the proto chain length
State.size = function history(state) {
var i = 0;
do {
state = Object.getPrototypeOf(state);
} while (state !== State.prototype && ++i);
return i;
};
// flats out a chain
State.merge = function merge(state) {
return new State(
State.keys(state).reduce(
(o, key) {
o[key] = state[key];
return o;
},
{}
)
);
};
// from time to time ..
if (State.size(state) > 512) {
// reset history
state = State.merge(state);
}
As you can see, the proposed solution is so simple, anyone can improve it in all aspects.
I hope I’ve finally managed to describe and expose it properly
For those who can, have great holidays.
For those who cannot, respect and thanks for your hard work!
See you next year!