stampit-org / stampit

OOP is better with stamps: Composable object factories.
https://stampit.js.org
MIT License
3.02k stars 102 forks source link

Standard way of identifying object instance origin #52

Closed jrf0110 closed 9 years ago

jrf0110 commented 9 years ago

Given an object instance, it'd be useful to know if it has certain behaviors:

var getADrink = function( cocktail ){
  if ( cocktail.has( 'drink', 'gin' ) ){
    /* Do something */ 
  }
};

This would require some explicit meta data about the stamp, but none-the-less, is useful.

I see JS's non-linear inheritance as being pretty similar to the Components pattern in OO. The problem is that there's nothing to say what a thing is with any guarantee because types suck in JS.

In C#/Unity, if you wanted to see if an object had certain behaviors, you would do something like this:

Gin ginComponent = cocktail.GetComponent<Gin>();

if ( ginComponent != null ){
  /* Cocktail has the gin component */
}

Having used stampit for a little while, here's what I'd like to see:

  1. An easy/standard way to identify what components an object has
  2. Access to the factory(s) that instantiated the object
    • Both access to the composed factory and the individual components
    • Something akin to instance.prototype.constructor, but better
talon commented 9 years ago

Yeah this would be nice. Perhaps something like stampit.is(stamp, obj) then the original stamp becomes the "type"?

jrf0110 commented 9 years ago

right.

Maybe even with some extra sugar:

var factoryA = stampit({ ... });
var a = factoryA();
a.is( factoryA );
factoryA.didStamp( a )
ericelliott commented 9 years ago

JS does not allow this sort of object metadata. Even using raw JS objects, instanceof isn't really type data. Instead, it simply checks the object's prototype identity against Constructor.prototype, which is very brittle.

The factory could keep an identity history of every stamp created, but that would create memory overhead that may not be appropriate for every use of Stampit (particularly games or event systems cranking out lots of objects).

The instances can't keep metadata without tacking properties onto the stamp that may interfere with the author's intentions.

Barring those options, here is what we could implement:

Since that's not actually a strong guarantee, is it worth putting it in Stampit core?

If you actually need this kind of thing, here are options you could do in user space:

  1. Tag all instances with a list of stamps that contributed to the instance. e.g. stampit.compose(a, b, c).state({hasBehaviors: [a, b, c]});
  2. Keep a collection with references to every instance you've generated. Compare the identity of the instances to see if they're in the collection.
  3. Forget about trying to type your instances. Instead, ducktype. Just try to use them, and throw a descriptive error if they're missing a behavior needed by some function. This is what I do 99.9% of the time, and it usually works great. If it doesn't work great, that's usually an indication that I've designed something wrong.
  4. For me, a typical use-case for stamps is creating utility libraries. I don't actually use stampit much to represent business objects like users, etc... For that, I use functional programming.

When you start getting into the need to identify the type of instance you're looking at rather than simply ducktyping, it may be a code smell that should push you in the direction of functional, reactive, or dataflow programming. See The Two Pillars of JavaScript Pt 2.

talon commented 9 years ago

Looks like probably your best bet is using this if you must: https://www.npmjs.com/package/is

-1 for putting it in stampit core

koresar commented 9 years ago

instanceOf or obj.is(Stamp) method

@jrf0110 I had a similar proposal. See #44. I know your pain. :)

Although, I'd agree with @ericelliott that this only makes things worse when we try to mimic classical inheritance. See this funny tweet.

If you really need your object to be recognizable then the best way would be to use ES6 Symbols. Stampit does not support it (yet?).

The other way around would be to create one more function in either your stamp

myStamp.hasCreated = function (obj) { /* ducktype it yourself */ };

or add a method to your objects' prototype:

var myStamp = myStamp.methods({
  createdBy: function (stamp) { return stamp === myStamp; }
});

However, in last 18 months I haven't found myself needing anything of the above. Ducktyping was always more than enough.

Changing stampit core

I believe that we should move rarely needed functions out of stampit main file, but put them to a subfolder. For example stampit.isStamp and the proposed stampit.is(). And use them like so:

var isStamp = require('stampit/is-stamp');

or

require('stampit/has-created')(stampit);
myStamp.hasCreated(someObj);
ericelliott commented 9 years ago

Changing stampit core

:+1:

sheriffderek commented 7 years ago

One and a half years later... What is the conclusion here? I'm not seeing how to tell if something is an instance of a particular stamp. Kindly point me to the right doc? : )

koresar commented 7 years ago

@sheriffderek There will never be a standard way to identify an object origin. First of all, we think that's a bad thing. Second, you can implement that yourself as a 3 lines composable behaviour (aka stamp). See here for inspiration - https://github.com/stampit-org/stampit/blob/master/docs/advanced_examples.md#attach-function-to-prototype-memory-efficient