stampit-org / stampit

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

FAQ.md - collecting questions #143

Closed koresar closed 6 years ago

koresar commented 9 years ago

People are often asking similar questions. We need to have a docs/FAQ.md.

Here, in the issue, let's collect all the questions people usually ask.

koresar commented 9 years ago
  1. Scroll back through https://gitter.im/stampit-org/stampit and get few questions out of it.
ericelliott commented 9 years ago

:+1:

netaisllc commented 9 years ago

Ok, here's a couple of really basic ones:

  1. Duh, I'm new, conceptually what is the difference between refs and props?
  2. Gee, using Backbone, I am used to an explicit Events module. Where do I register events on y stamps?
ericelliott commented 9 years ago

:+1:

msageryd commented 9 years ago

I have a newbie question for the faq. I'm really interested in the answer =) Just found Stampit. I seems really useful. I'm trying to recover from an OO background. Please tell me if I'm in the right direction here. I'm building the model for an API.. Some thoughts:

DbLayer = stampit().static({
  findOne: function(id, callback) {query db... return this(result)}  //stamp a new object from the query result
}

User = stampit().methods({
  checkPassword: function(password) {return password === this.password}
}

DbUser = stampit().compose(DbLayer, User)
DbUser.findOne(123, function(user) {
  if user.checkPassword('abc')
  ...
})
ericelliott commented 9 years ago

Should I store the actual object properties in a sub property or in the root of my stamped object?

That depends on your needs. I do both, frequently. Many objects I compose are also event emitters. I frequently have someObject.events and a corresponding someObject.on that delegates to somObject.events.on. The reason I set up that delegation is because the EventEmitter uses several properties on the root object if you let it. I want to minimize the chance of property collisions.

I also frequently namespace stamps. I often follow the jQuery plugin standard: Use only one namespace per plugin. In this case, you can think of stamps as plugins.

For example, I sometimes model things like this: db needs to keep track of contacts, who may or may not be users of the system. Some contacts are also users. Some users are also employees. Each of these entities has their own properties that they keep track of. Sometimes there are users who don't have contacts added to the system yet. These are all people, but there is no corresponding "people/person" entity type in the system, so there's no root object that all of these inherit from... instead, we do it with composition:

// Create a new user who is also a contact and an employee.
// Don't init properties yet, use defaults.
const newUser = stampit.compose(user, contact, employee)();
typeof newUser.contact; // 'object'
typeof newUser.user; // 'object'
typeof newUser.employee; // 'object'

Does it make sense to separate the db layer and use static functions as kind of constructors?

IMO, it makes sense to completely separate the db layer from your domain model. I wouldn't even mix the db access stuff into the same object. Instead, I model the entire application state together. For example, check out Redux or Facebook's Relay.

If you really want to simplify your app, model db access and domain models as completely separate concerns.

Can I trust "this"? Will it always reference the stamped object?

That depends on what you mean by "trust". JavaScript has a dynamic this which you can manipulate from outside the object, so no, you can never really trust this, because anybody could do yourObject.yourMethod.call(anyObject, ...args);

But the default behavior when you invoke a method using dot notation (e.g. myObject.myMethod()) is to use the object that you invoke the method on as this. Stampit doesn't do anything fancy in that respect unless you explicitly model different behavior into your stamp (such as creating a differently-bound method with an initializer).

koresar commented 9 years ago

I assume Michael is doing node.js. Right? I was experimenting for some time and found a best (yet) approach. So, I'll share my story.

Here is my hapi.js handler:

    handler: (request, reply) => {
      return reply(
        Orders({
          logger: request.logger,
          credentials: request.auth.credentials
        }).getOrder(request.params.id)
      );
    }

The Orders is a stamp:

const Orders = stampit
  .compose(DbClient.use('Order', OrderSchema), OrderPricingCalc, OrderSchemaConverter)
  .methods({
    getOrder(id) {
      return this.db.findOne(id).then(this.applyPricing).then(this.convertFromDbToApi.bind(this));
    });

As you can see: 1) The HTTP and hapi.js contexts are abstracted away from the Orders implementation. This means I can change netwoking protocol at any time. Also, I can replace hapi.js with express.js easily. And also, by implementing a different DbClient the code can be reused on the frontend (browser) too. 2) The this.db is part of DbClient. The this.applyPricing is taken from OrderPricingCalc. And this.convertFromDbToApi is the OrderSchemaConverter thing.

Answering your qeustions:

Should I store the actual object properties in a sub property or in the root of my stamped object?

It's up to your will and needs.

Does it make sense to separate the db layer and use static functions as kind of constructors?

Yes, sure. It depends though. You can try few approaches and tell us the outcomes. It would be nice to hear one more opinion.

Can I trust "this"? Will it always reference the stamped object?

You can trust "this" in any object method or static method whilst using "dot notation" (see Eric's answer above).

Side things

Bon voyage!

koresar commented 9 years ago

I should probably elaborate on the implementation of those stamps: .compose(DbClient.use(OrderSchema), OrderPricingCalc, OrderSchemaConverter)

const DbClient = stampit().static({
  use(collectionName, mongooseSchema) {
    this.fixed.refs.db = mongoose.model(collectionName, mongooseSchema);
    return this;
  }
});

const OrderPricingCalc = stampit().methods({
  applyPricing(order) {
    ...
    return order;
  }
});

const OrderSchemaConverter = stampit().methods({
  convertFromDbToApi(order) {
    if (...) {
      this.logger.warn(`User ${this.credentials.id} have bad order ${order.id}`);
    }
    ...
    return order;
  }
});
msageryd commented 9 years ago

Thank you both. It's great to see real life examples. I see a golden shimmer over the composition concept vs inheritance. I feel free, not locked in by a class model. This is going to be awesome! I had a hunch that the prototype model had more to give, but I really needed Stamps to get going.

Using a static function to load a stamp with presets at composition time is a great solution. Fits my needs perfectly. This was a missing piece in my setup.

Q1: Why are you using fixed.refs to reference Mongoose? Could it as well be stored in props or refs?

I know Eric is a promise kind of guy. It seems like Vasyl is as well.. One day I'm going to try some promising. I've used Async everywhere until now.

Btw, I'm using Postgres for this project (converting to PG from a Mongoose spike). 99% of the access will be through an Express API from apps. Only some admin functions will be on a web site for now.

koresar commented 9 years ago

Q1 answer. See comments.

const DbClient = stampit().static({ // static methods "this" context is stamp itself
  use(collectionName, mongooseSchema) {
    this.fixed.refs.db = mongoose.model(collectionName, mongooseSchema);
    return this; // <- this is stamp, not an instantiated object!
  }
});

Does it answer your question? :)

msageryd commented 9 years ago

Yes, the comment about this is the stamp in static functions was a good point, thanks. But why "fixed"? Why not just this.db or this.refs.db?

koresar commented 9 years ago

We even have a doc for that (well hidden, far distant doc though) https://github.com/stampit-org/stampit/blob/master/docs/advanced_examples.md#hacking-stamps

msageryd commented 9 years ago

From the example in the doc:

Stamp.fixed.methods.data = 1;

Unfortunately, this makes it more unclear to me what to use. I get the fixed part now. But why does the example use methods to store state-like information. I thought methods should be used for methods.

msageryd commented 9 years ago

Wouldn't the instance look exactly the same if I used "fixed.refs.data = 1" instead?

koresar commented 9 years ago

The stampit v2 internal implementation should not be exposed, that's why the page says "hacking stamps".

Read fixed.methods as obj.prototype. Take a closer look at the four bullet points by the link.

ericelliott commented 9 years ago

Using methods for data is an anti-apttern because you could accidentally mutate data for all instances that should only have been mutated for a single instance.

The "hacking stamps" section is demonstrating that properties you set on the delegate prototype can behave like default values that will appear to be set even if the object has no own property with that name.

msageryd commented 9 years ago

@Vasyl, I'm using your concept with a static use() function to load my DbLayer with presets. I'm storing the presets in this.fixed.refs.dbPref. How can I reach those values from another static function?

I would like my DbLayer to have a static remove() function. I don't want want to make user instance just to be able to remove a user from the database. dbPref is populated in the instances as it should, but I need to reach it from the stamp as well.

koresar commented 9 years ago

Not sure what you want. :) 1) Define "preset". 2) I implemented the DbLayer wrong. It should return new stamp, instead in mutates the existent. Sorry, totally my fault. Here you go:

const DbClient = stampit().static({
  use(collectionName, mongooseSchema) {
    return this.refs({ db: mongoose.model(collectionName, mongooseSchema) });
  }
});

3) Would you please shorten your question?

msageryd commented 9 years ago

Thanks! Your correctly implemented method made it work better for me. Now I can have another static function which has access to this.fixed.refs. If I'd used mongoose it could look like this (simplified):

const DbClient = stampit().static({
  use(collectionName, mongooseSchema) {
    return this.refs({ db: mongoose.model(collectionName, mongooseSchema) });
  },
  remove(id) {
    this.fixed.refs.db.remove(id)
  }
});

Would this be an ok way to reach db from within another static function?

koresar commented 9 years ago

Avoid using static functions @michaelswe. Use regular stamps for do everything. This "remove" function looks misplaced and redundant as to my taste.

The sole idea of stampit is to be able to compose any behavior. Utilize "stampit().methods()" as much as you can. Answering your qestion: no, it is not.

tcrosen commented 9 years ago

Hey guys, I'm trying to use Stamps to build my Angular views and having trouble figuring out how to structure my objects. I'll list out as much detail as I can, hopefully you can provide some direction if you have the time. If not, no worries.

Here's a very simple example of what I'm trying to convert to stamps:

// If you don't know Angular - `$scope` is just the view model that exposes things to the DOM.
$scope.customer = {
  // These capitalized properties are what the server cares about.
  // They are 2-way bound to the UI.
  // Eg. <input ng-model="customer.FirstName" ... />
  FirstName: null,
  LastName: null,
  State: null,

  // A list of States needs to be available for a dropdown in the UI.  
  // The user selects from this list to fill in the State property above.
  // I don't want this property to be sent to the server.   
  stateOptions: ['AL', 'NY', 'OH', ...],

  // Customer information is edited in a modal dialog.   
  // This is bound to the DOM to indicate where or not the modal is shown
  // Eg. <customer-details-modal show="customer.modalShown">...</customer-details-modal>
  modalShown: false,

  showModal: function() {    
    // TODO: Need to store the current "state" of the property values in case the user cancels and we need to revert
    // Something like: this._original = { ... }
    this.modalShown = true;
  },

  modalSaved: function() {
    // TODO: Send data to the server.
    // Payload should be only this:
    // {
    //   FirstName: 'Bob',
    //   LastName: null,
    //   State: 'AL'
    // }

    // customerApi.save(/* what goes here? */)
  },
  modalCancelled: function() {
    // TODO: Revert any changes made 
  }
};

I'm trying to achieve the following:

  1. Expose primitive properties for 2-way binding in the DOM
  2. Attach methods for meta-data (eg. list of states) and some actions (eg. show/hide editing form modal)
  3. Retrieve only the primitive properties when it's time to send to the server

Possible stamps (not sure if I'm structuring these correctly):


// There are other modals in the same page, so I want to be able to compose customizable modal methods.
var Modal = stampit()
  .init(function() {
    this.shown = false;
  })
  .methods({
    show: function() {
      this.shown = true;
    },
    ok: function() {  ...  },
    cancel: function() {  ...   }
  });

var Customer = stampit()
  .props({
    FirstName: null,
    LastName: null,
    State: null
  });

var CustomerView = stampit()
  .methods({
    sendToApi: function() { 
      // This is the part I'm really hoping for an elegant solution to     
    },
    showEditModal: function() {
      // @ericelliott has mentioned his namespaces his Stamps, how is that done?
      // can I do something like this to avoid generic "show" properties on every object?
      // this.modal.show();
     }
   })
  .compose(Modal, Customer);

$scope.customer = CustomerView({ stateOptions: ['AL', 'NY', 'OH', ...] });

// As I mentioned earlier, there are other objects on the same page I need to handle in very similar ways.  For example:

$scope.order = OrderView({ ... });

Hopefully that isn't too much of a mess and you can understand what I'm asking. Cheers.

koresar commented 9 years ago

@tcrosen I read it through, with comment. Confirming. You got the idea of stamps right. So very right!

Few suggstions which you shouldn't blindly follow.

What elegant solution you are looking for the sendToApi? It should be straightforward IMO.

Namespacing in JS is easy to google. :)

can I do something like this to avoid generic "show" properties on every object?

I didn't get the question. Sorry.

Please, consider asking your questions here: https://gitter.im/stampit-org/stampit You'll get faster responses.

boneskull commented 8 years ago

Please provide an example of unit testing a Stamp which is composed.

const A = stampit({
  init() {
    // stuff
  },
  methods: {
    // some methods
  }
});

const B = stampit({
  init() {
    // other stuff
  },
  methods: {
    // other methods
  }
}).compose(A);

I've been hung up on this in one way or another for awhile. How can I avoid test code duplication (or simply running the same assertions multiple times) and avoid tight coupling at once?

ty @koresar for your help on Gitter.

jordidiaz commented 8 years ago

Hi @tcrosen! I'm very interested in your 'Angular Views with Stamps' aproach. I'm going to try it for sure. Thanks for the idea!

nathanmarks commented 8 years ago

@koresar @ericelliott

When researching stamps vs other JS object design techniques (constructor, etc), there is a lot of negativity directed towards stamps and anything anti-language-construct-that-masks-prototype-behaviour in general.

An example of a frequently seen argument is that V8 is now being optimized for the use of class (along with contrived jsperf setups benchmarking Stampit() or Xyz = Object.create(); Xyz.init() vs new Xyz(). Is the scare based on conjecture? Are there actually real world implications with the direction V8 is going or just arguments over micro-optimizations?

I don't have the time to setup and instrument an example that would simulate any real world implications but reading the back and forth is tiring. I much prefer using stamps as it lets me take advantage of certain design patterns and features that js is capable of more easily than pseudo-class syntax without having to write a lot of my own boilerplate. But I want to understand the performance arguments better so that I can make an educated decision.

On that note, are there any important best practices to follow when using stamps for optimal performance in V8? I don't know quite enough about the engine (working on it!!) which is why I am unable to draw a proper conclusion myself on the matter.

edit edited my explanation slightly to improve the description of what I'm looking to find out

koresar commented 8 years ago

Hi @nathanmarks

Great question. I did few benchmarks with stampit v1 and v2 in the past.

But first, let me remind few known statements: 1) Premature optimization is the root of all evil. (D. Knuth) 2) Fix performance only after you identified the bottleneck. (M. Fowler) 3) This is not your bottleneck. (E. Elliott)

Few months ago I did the "Performance in JS" presentation at a large meetup. Here are the very descriptive slides.

Answering your question. This is how stampit creates objects (roughly):

const instance = Object.assign({}, stamp.properties); // 1
instance.__proto__ == stamp.methods; // 2
stamp.initializers.forEach(initializer => { // 3
  instance = initializer.call(instance, { instance, stamp, args }) || instance;
});

No magic. :)

As you can see

Thus, the fewer state and fewer initializers you have - the faster the object creation. Number of methods does not influence object creation performance at all.

The last time I did a performance measurement of the stampit v2 it was significantly (x10 times or more) slower than the new keyword of a similar class. So, if you are creating millions of objects per second then you should consider not using stampit. But, if it is thousands or less then stampit is the right choice.

For more live communication please ask your following questions here: https://gitter.im/stampit-org/stampit

ericelliott commented 8 years ago

@nathanmarks

The v8 optimization stuff is about virtual classes, which is about optimizing property lookup.

The cheapest Chromebook on the market can access millions of properties per second. I've said it before, and I'll say it again -- unless you're building a perf-critical general utility library like Lodash or RxJS, object creation and property access is not your bottleneck.

Get your pageload under 2 seconds on 3G. Get your click responses under 100th of a second. Get all your animation frames under 16ms. ONLY THEN should you come and talk to me about the perf of Stampit vs new.

And then guess what I'll tell you... The fastest way to instantiate objects and trigger virtual classes is not new -- it's an object literal. If you're worried about property access times, ditch the prototype chain altogether and just spit out fully formed object literals.

No matter what technique you use, there will be a tradeoff between flexibility and perf, but that tradeoff is so tiny as to be almost entirely insubstantial.

Can you tell the difference between .0000000001 seconds and .000000001 seconds? Neither can I, but I sure can tell the difference between loading 10 small icons or loading one web font, instead! ~ "Common Misconceptions About Inheritance in JavaScript"

nathanmarks commented 8 years ago

@ericelliott Those were my thoughts exactly -- it's hard to know what to believe with so much contradictory material.

In all my performance debugging I've never identified object instantiation as a bottleneck in my applications. It's just confusing when there are so many people insisting that new vs Object.create is an argument worth having due to performance.

Personally, until finding Stampit I've stuck to a lot of Object.create + Object.assign.

I'm not worried about property access times, it's honestly just the fear mongering that got me worried. I've spent the last 7 days doing a lot of reading and setting up some of my own basic tests -- my takeaway is that it was all a bit of a waste of time as I've never run into performance issues related to object instantiation :)

davewallace commented 7 years ago

@koresar @ericelliott

Hey fellas.

I am currently experimenting with stamps to construct types of character objects for a hobby RPG rebuild, I have what is probably a simple use case: I want to be able to create a Character that is composed of a Race (eg Elf, with props), a Type (eg Mage, with props) and a Type specialisation (eg Ice-Mage, with props). I'd like to be able to switch out that type spec or type, or even Race, with another. They should all be discrete. I looked at stamps because they've made encapsulating each set of associated props reusable.

So yeah, I can very easily compose all sorts of objects which is awesome, but how do I decompose so that an Elven Ice Mage can become an Elven Fire Mage, or Human Barbarian Fighter at the click of a few buttons?

I am sure there's lots of things that could pollute the example above and I'm sure this isn't the code heavy example you're probably used to - I'm no JS master but I see the value in what's going on here. So to keep things generic so I can understand, how could this be achieved using stamps?

Finally, am I even approaching this correctly?

danielkcz commented 7 years ago

@davewallace I think that such RPG game is definitely great use case for stamps. 👍

Decomposing is no easy matter. There are no internal records of what stamps were used in composition. Essentially you would need to do such tracking by yourself with infected compose.

However in the end you will have to create fresh new stamp based on information you have collected before. In my opinion it might be somewhat easier to actually compose from the scratch if you have a list of things you want to compose together. I assume you store entity data in regular properties. Then you can easily compose necessary stamp and do stamp.props(oldEntity) which would copy properties of the old entity to a new one.

Unfortunately there will probably some leftover data when from its previous specialization. That could be solved by actually passing props to a factory function call and every initializer in those small stamps could grab whatever it needs and store it on the instance.

Edit: Perhaps I should also mention one important thing. Once you have actual instance of some stamp, you cannot essentially compose more stamps into it dynamically. I am not saying it's impossible, but will be rather hacky because you would need to modify prototype of that object in runtime. That might have unforeseen side effect that all instances coming from that stamp would be affected too.

koresar commented 7 years ago

@davewallace Sound like you read that article. 😄

So, basically @FredyC said everything I wanted to say. 👍

It is very interesting to me to see what you (will) come up with as a result. Please, keep us updated. Cheers!

koresar commented 7 years ago

Also, we have few advanced examples here. They are for stampit v2. But can be easily converted to the stampit v3.

I'll fantasize a bit below:

const HaveAbilities = stampit()
.methods({ 
  addAbility(name, value) { ... }, 
  getAbility(name) { ... }
})
.init(function (opts, {stamp}) {
  // Iterating over the abilities which the character can actually have
  stamp.compose.configuration.abilities.forEach((ability) => {
    this.addAbility(ability, opts.getAbility(ability));
  });
})
.statics({
  predefined(abilityNames) {
    // storing the abilities in configuration
    return this.configuration({ abilities: abilityNames.filter(_.isString) });
  }
});

const Elven = HaveAbilities.predefined(['archer']);
const Mage = HaveAbilities.predefined(['spell-caster']);

const SpellBook = stampit()
deepProps({ spells: {} })
.methods({ 
  addSpell(name, value) { this.spells[name] = value; }, 
  getSpell(name) { return this.spells[name]; } 
})
.init(function (opts) {
  // The character never looses any known spell (unlike abilities)
  this.spells = opts.spells;
})
.statics({
  predefined(name, value) {
    // storing the spells in deepProps because these CAN be accumulated
    return this.deepProps({ spells: { [name]: value } });
  }
});

const Ice = SpellBook.predefined('ice-arrow', ...).predefined('freeze', ...);
const Fire = SpellBook.predefined('fireball', ...).predefined('sparks', ...);

const Hero1 = stampit(Elven, Ice, Mage);
let hero = Hero1();
const Hero2 = stampit(Elven, Fire, Mage);
hero = Hero2(hero);

Have never tested that. :)

Tell me if you need any explanations.

davewallace commented 7 years ago

You guys are awesome, thank you :)

@koresar I hadn't read that article, but I'd read yours as part of the Fun with Stamps on medium.com: https://medium.com/@koresar/fun-with-stamps-episode-8-tracking-and-overriding-composition-573aa85ba622#.m3tvahswy

I am going to try out your approach above this weekend hopefully.

Also, I was actually thinking of making this game ultra flexible since I'm rebuilding it, I always liked the idea of being able to pick and choose game mechanics from a UI to construct my own flavour of a game, for example switching from turn-based to real-time at the flick of a switch. I figure using stamps and having very flexible mechanics will hopefully allow this.

When I've finished choosing the technologies to rebuild, as am learning in the process, I will publicise the repo and post a link here, you're all free to pick it apart and smack me for getting stamps wrong until I get them right 🎱 :)

koresar commented 6 years ago

Closing this for now. Not visited for a year. Feel free to open any new issues.