-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Controlling the Stamp Composition (Proposal) #63
Comments
I have a working proposed solution here. https://github.com/stampit-org/stampit/tree/composers What are your thoughts? |
I'm open to this. =) PR? |
I thought there would be more discussion :D . Do you think it is ready for a PR? |
Seems pretty straightforward. I think a PR is a good next step. If we need more discussion, we can certainly discuss the PR. =) |
The https://github.com/stampit-org/stampit/tree/composers branch is a completely different approach to the proposal above. The proposal says about var staticQueueComposer = {
// replaces base composer
name: 'base',
order: 0,
compose: function (self, other) {
In other words, it's overcomplicated IMO. Although, I like the idea of replacing the composition logic itself. Let's think of a simpler solution. |
I might confuse something, but here is one more note.
In the new implementation I don't what to choose which composers are "base" composers. |
This is intended to cover much more than the This is intended to provide the flexibility to accommodate the stamp composition functionality cases we have seen requested and have discussed on gitter. After spending a fair amount of time playing with different ways to do this I quickly found the problem is not simple or similar enough to work the same as I agree with the functional mixin comment but it does not apply. A stamp does have an essential 'base' part: composability. It will always have the base / default compose function. That is what makes it a stamp. I should have provided more documentation with my branch. Reasons for named composer feature:Prevent duplicationMy first version did not have the name feature. With every call to Composers on the source stamp replace composers with the same name on the target stamp. Preventing duplication where not desired as is the case with the base composer. Prevent Double ExecutionYou have a composer that is intended to only be run once (such as the base composer). You then compose 2 stamps that both already have that composer, it will be executed 2 times. You would need to add a way to identify the duplicate and decide which one will execute and which one will skip within the composer itself. I tried to do this and realized nothing should be designed to work this way 😣 . Allow replacementWhen removing composers with the same name I decided that it should be "last in wins" to be consistent. If you ever needed to replace a composer when composing a specific stamp, you would need a way to identify the the stamp to replace, remove it, then add the new one within your composer. Setting them to have the same name seemed like a much simpler way to do that. This also allows the ability to completely replace the base composer with your own. DebugingI found I needed to identify specific composers anyway to follow and debug how things worked. The feature is opt in (optional)Composers do not have to be named. You can have anonymous composers that will never collide. Reasons for the composer execution order functionalityThe base composer is order: 0. Setting a composer to a negative order value ensures it executes before and a positive number after. Execute before or after the default (base) composer behaviorWith every composer use case I have seen it is essential that it is executed before or after the default compose behavior (usually before). If your composer executes after, there is no way to access properties on Order added in code
|
This is beginning to strike me as an over-engineered solution. Before we proceed further, let's back up and clarify some things:
When answering these questions, answer with simplicity and clarity in mind, in a way that would be fairly easy for a stamp newbie to grasp reading a stamp README or getting started guide. If we can't explain the problem and the solution clearly to non-experts, it's not ready to be added to the spec. It seems like you are focusing use-cases on collision mitigation. I've said it before and I'll say it again, collision mitigation is not required in the stamp specification because it can be handled at the stamp composition step, essentially any of the following techniques, without adding API overhead to the spec:
These are all available design-time solutions that do not require any additional code. I would rather see some clear examples of each of these approaches than additions to the spec, if that is an option. @koresar Has mentioned the desire to be able to track and maintain essentially a composition log: What happened to each of the source properties as composition occurred. I agree that it might be a nice debugging tool, but I'd like to emphasize that if you feel you need that, I suggest that you may be over-using stamps, or over-engineering your solutions. When it comes to object-oriented design, Keep it Stupid Simple should be your mantra. That means flat inheritance hierarchies (you should not have stamps that inherit from stamps that inherit from stamps) , small component parts (which are unlikely to collide in the first place), relatively few components per stamp, etc... Ask yourself very seriously: Am I keeping it stupid simple? Is there a chance we could solve these problems in simpler ways? |
Ok guys. I'm fully on both of your sides. @unstoppablecarl described my thoughts exactly as I saw them a year ago. I fully agree that we need that kind of advanced feature set. Same time @ericelliott is 100% right too. My proposal:
Agree? |
I have not been paying as close attention to stampit development lately I didn't realize there was a 3.0 branch yet. I was working on a project and pulled down the latest build (master) and could not get it to concatenate a static array on composition. I saw that a proposal was still being discussed in this issue and played with some solutions. I wanted to better understand the problem by working out a prototype solution. I then set out to create a solution that would allow you to do anything you may need when adding custom composition behavior. This is probably too much and the scope should be more limited. This is why I was surprised when there was little discussion at first. My personal use caseI am working on a game project that uses real time communication see https://github.com/primus/primus. Messages containing json are sent back and forth between client and server. I want a way to define a function on a stamp that converts its relevant data from or to json to communicate between the client or server. I need this behavior converting to or from json to be composable so that when I compose 2 stamps with defined to / from json behavior it is merged. The code I tried to get to do this: var Jsonable = stampit()
.init(function(settings){
var stamp = settings.stamp;
this.toJSON = function(){
return stamp.toJSON(this);
};
})
.static({
// converter functions
_toJsonConverters: [], // needs to be concatenated on compose
_fromJsonConverters: [], // needs to be concatenated on compose
// add a to json converter function
addToJsonConverter: function(fn){
this._toJsonConverters.push(fn);
return this;
},
// convert object instance to json
toJSON: function(obj){
return this._toJsonConverters.reduce(function(json, func, index, array){
// return new or mutate json object
return func(obj, json) || json;
}, {});
},
// add a from json converter function
addFromJsonConverter: function(fn){
this._fromJsonConverters.push(fn);
return this;
},
// prepares options object before calling factory
fromJSON: function(json){
var preparedOptions = this._fromJsonConverters.reduce(function(options, func, index, array){
// return new or mutate options object
return func(options, json) || options;
}, {});
return this(preparedOptions);
},
});
var StampA = stampit({
refs: {
foo: 'bar',
},
})
.compose(Jsonable)
.addToJsonConverter(function(obj, json){
json.foo = obj.foo;
return json;
})
.addFromJsonConverter(function(options, json){
options.foo = json.foo;
return options;
});
var StampB = stampit({
refs: {
stampA: null,
id: 99,
}
})
.compose(Jsonable)
.addToJsonConverter(function(obj, json) {
json.stampA = obj.stampA.toJson();
json.id = obj.id;
return json;
})
.addFromJsonConverter(function(options, json){
options.stampA = StampA.fromJson(json.stampA)
options.id = json.id;
return options;
}); After thinking about it for a while every case I can think of or that we have seen so far could be solved by static property concatenation instead of replacement. Maybe we should just implement a way to do that? |
I just thought of one solution similar to the original Add This would allow you to have composable static initialization but would not give @koresar the debugging ability he wants. |
@unstoppablecarl there is the new Let me rewrite your case using the new "composables" syntax. const Jsonable = compose({
static: {
addToJsonConverter(func) {
const index = _.keys(this.compose.configuration.toJSONConverters).length;
return this.compose({ configuration: { toJSONConverters: { `$(index)`: func } } });
},
addFromJsonConverter(func) {
const index = _.keys(this.compose.configuration.fromJSONConverters).length;
return this.compose({ configuration: { fromJSONConverters: { `$(index)`: func } } });
}
},
initializers(opts, {stamp}) {
const toJSONConverters = _.values(stamp.compose.configuration.toJSONConverters);
this.toJSON = function () { /* using the toJSONConverters here */ };
const fromJSONConverters = _.values(stamp.compose.configuration.fromJSONConverters);
this.fromJSON = function () { /* using the fromJSONConverters here */ };
}
});
const StampA = compose({ refs: WHATEVER })
.compose(Jsonable)
.addToJsonConverter(WHATEVER)
.addFromJsonConverter(WHATEVER); The code above does not use stampit v3 or else. |
Alright, here is an alternative solution which requires only a tiny change - one line to be moved 3 lines up. Allow users to override import compose from 'a-compose-implementation';
const LoggingCompose = compose({static: {compose(){ // override "compose" method
console.log(arguments);
return compose.apply(null, arguments);
}}});
compose(LoggingCompose, compose()) // not yet overridden
.compose() // overridden compose function
.compose({ methods: { F(){} }, properties: { a: 1 } }); // overridden compose function
// will log twice:
//
// {}
//
// { '0': { compose: [Function: compose] },
// '1': { methods: { F: [Function: F] }, properties: { a: 1 } } } That's a simple and elegant solution IMO. P.S. I'm pumped currently. Hard to control emotions, but I'm pretty sure that's simply the best way to go. It solves EVERYTHING we discussed above |
Thanks for writing the example. The I am not sure I completely understand how the configuration object works. What does it do differently from This is a lot of trouble just to avoid having to concat an array though. I think having a
what? is that a threat 😕 I am not sure overriding |
Yes, it would, but using
My proposal to allow overriding the
I think I'm missing something. Could you please point why do you think so? (I'm really confused, it's 1 am after all. :) ) P.S. Really appreciate your opinion @unstoppablecarl |
What exactly is the
As you show in the comments the overridden compose function is only called when directly chained from the stamp. The following cases will not call the overridden compose function: import compose from 'a-compose-implementation';
const LoggingCompose = compose({static: {compose(){ // override "compose" method
console.log(arguments);
return compose.apply(null, arguments);
}}}); // custom compose not called when LoggingCompose is created
compose(LoggingCompose, compose()) // custom compose not called
LoggingCompose
.compose() // overridden compose function IS called surprising and confusing everyone :P
compose(compose(), LoggingCompose); // then later not called I think it is pretty clear that this does not accomplish what we need. I'm going to try implementing |
You're seeing it all. IF the role of configuration is vague could you please point in the README the passage about it which was not clear. Maybe we should add more info on it.
Good point... crap. :) I need to think. |
Well in the readme heading "Configuration Example" it describes behavior that is not in the example code and links to an empty repo. So no it is not clear what it is for. It is just for the collision example? |
Yes, that's just an example. Not sure why it exists. :) The essence of |
Ah I get it now. At first I thought functionally |
After discussing with @koresar The only complete way to intercept all calls to It is not reasonable to have the Stampit should have an api like |
moved concatenation discussion to #66 |
Override merged. |
For the future reference this was implemented in #71 |
Original idea: Christopher
(IIRC I was talking about it somewhere sometime too.)
Carl's proposal: stampit-org/stampit#188 (comment)
I use stamps. Very often (literally very) there is the need to debug/analyze a stamp's composition process/chain/order. See full list here.
Also, this will allow us to implement the last feature Traits have but stamp doesn't. It can be done in a flexible fashion - as a composable stamp/behavior.
In addition to extending the specification we should also develop few utility stamps which help with debugging/analysis of a stamp.
trackComposition
stamptrackOverlap
stamptrackDuplication
stampSample code:
The text was updated successfully, but these errors were encountered: