robotlegs / robotlegs-framework

An ActionScript 3 application framework for Flash and Flex
https://robotlegs.tenderapp.com/
MIT License
967 stars 261 forks source link

Thoughts on the architecture #114

Closed creynders closed 11 years ago

creynders commented 11 years ago

I've been working on 2 extensions, swapping between them whenever I got stuck, which happened a lot. Partly due to my vague understanding of how everything works, but in a few cases also because I think some things are missing in the architecture.

For instance, if you take a look at commands, there doesn't seem to be a consistent way how commands are executed. Every extension (eventCommandmap and messageCommandmap) is responsible for executing them themselves. There's a lot of overlap (verifying it's a command, handling guards, hooks, mapping and unmapping the command) I understand why though, since the eventCommandMap needs to do other "stuff" in between those phases compared to the messageCommandMap. However, I think it would be a MAJOR improvement if there was some kind of CommandExecutor which you could reuse when writing an extension and decorate/extend/hook into its execution sequence. Or for instance if the CommandCenterExtension would provide a centralized execute method just as the commandmap in RL1 did. It wouldn't even have to be exposed in the API, but it would be nice to have something like it.

Speaking of commands in general: is it necessary to enforce the implementation of an execute method? Might a command not just as well be a class that implements a PostConstruct tagged method? I'd drop the whole describeType(mapping.commandClass).factory.method.(@name == "execute").length() thing (it's pretty slow too) and make the calling of the execute method only happen if the class implements a ICommand interface. The whole seems more flexible that way.

Then, I really miss factories or some kind of way to define which classes are created by default. As I already commented in the commandCenter extension it seems like a waste that the only thing preventing the reuse of the CommandMapper class is the fact that it creates concrete CommandMapping instances. The same applies to the ScopedEventDispatcherExtension. It seems a pity that EventDispatcher instances are created by default w/o any option to define another implementation of IEventDispatcher. (Obviously it's pretty easy to copy/paste the code from ScopedEventDispatcherExtension and create your own custom extension, but it also violates DRY)

I have a feeling RL2 is way superior to RL1 when it comes to "normal" usage, but in my (granted limited) experience when creating extensions it seems you need to overcome a lot more hurdles than before.

darscan commented 11 years ago

What do you think of the idea of mix-and-matching the command maps? Is it worth checking out? I think it'll be pretty time-consuming that's why I ask up-front. Would be a shame to spend a lot of time on it, only to find out you hate the idea.

To be honest, I have no idea how this might work in reality. Struggling to grasp it.

creynders commented 11 years ago

No reason to force users to implement an execute method if we can let them choose which method to invoke.

Agree. Fully. I'll introduce it in my version too.

We don't need it anymore because you'll always get a mapper/unmapper back - even if one didn't previously exist. Also, the mappers/triggers are never removed - even when they're emptied.

I know, that was a conscious decision on my part at a certain moment. I agree it's a bit meh when looking at it from the purist side, but pragmatically it works IMO. There are basically 3 scenarios in which the system will try to unmap a non-existent mapping:

  1. a mapping was made but then the command which does the unmapping is triggered more times than it should
  2. a mapping used to be made, but requirements changed and the developer forgot to remove the unmapping
  3. a conscious decision to be lazy about the unmapping in general and just sprinkling unmappings here and there.

These are all 3 the responsibility of the developer. I agree we should provide robust unmapping, but IMO it would even be warranted to log a warning whenever the system tries to unmap a non-existing mapping. Anyway, only in 2. an unnecessary trigger is created, in the other 2 cases the trigger is used anyway.

Then. Creating the trigger is a relatively costly operation, so I thought it would be better to keep the triggers even if they have no more mappings.

I think if we kept that approach, employed the CommandMappingList and dropped the inheritance requirement, we'd end up with something pretty cool.

Ok, I'm convinced. I'm gonna extract the list again and find out how I can get the triggers to work w/o inheritance.

To be honest, I have no idea how this might work in reality. Struggling to grasp it.

Yeah, me too TBH. Gonna do some further sketching.

creynders commented 11 years ago

So. I did what I said I'd do:

And... I dunno. I had to open up a part of the trigger API otherwise the decorating triggers couldn't access the CommandMappingList instance. I also had to open up the EventCommandTrigger methods. Where possible I left the API's as they were and only made the concrete implementations' methods public.

I had to modify the sorting in CommandMappingList, since in my version mappings are created immediately, not after configuration is finished, so for instance in the PECM, it tried to sort before the priorities were configured.

Yeah, I dunno. I'm going to brood on this for a day or two.

(also, I believe that our solutions are converging quite nicely).

I agree that's really cool! It's like distributed asynchronous pair programming ;)

darscan commented 11 years ago

And... I dunno.

Heh, I kinda see what you mean :)

So I'm not quite sure why the mapping stuff has to flow through the trigger at all. But I'm very probably missing something. I'm going to take a stab at refactoring your branch to remove most of the delegation, and see where that goes.

darscan commented 11 years ago

p.s. If you have some time today, perhaps we could do a text chat on Skype? Might be quicker. My Skype handle is the obvious one :)

darscan commented 11 years ago

Instead of refactoring yours I brought your writeable ICommandMapping idea into mine:

https://github.com/darscan/robotlegs-framework/tree/crey-commands-trigger

p.s. This branch has the Priority stuff for the EventCommandMap - just to see what's involved. I know I should have another branch for that, but it's easier just to work on one branch right now :)

The changes required were pretty minimal. In fact the triggers didn't have to change at all.

I might still be missing something big, but I can't see it. From where I'm standing it looks like everything is covered.

creynders commented 11 years ago

Ah, ok. Now I see what really obvious difference there is between our versions. It was hidden to me due to your shortcut ;) and my acute myopia. It's the extending of the trigger functionalities. For instance if you take a look at the PayloadEventsExtension, its trigger decorates the EventsCommandTrigger. W/o the decoration-shizzle, extension devs will start inheriting from the present CommandTrigger classes and be asking for private to protected switches -or- they'll be copying trigger functionalities. But now I look at your (Priority)EventCommandTrigger, I'm starting to think it's ok. Probably I was aiming for too DRY, again. Sorry, probably this was totally obvious to you already.

On the other hand... Maybe we should take the fact that you chose to modify the EventCommandMap instead of creating a separate extension for the PECM as a warning of some sorts?

darscan commented 11 years ago

It's the extending of the trigger functionalities. For instance if you take a look at the PayloadEventsExtension, its trigger decorates the EventsCommandTrigger. W/o the decoration-shizzle, extension devs will start inheriting from the present CommandTrigger classes and be asking for private to protected switches -or- they'll be copying trigger functionalities.

Yup. And that's why I wanted to move all unrelated functionality out of the triggers. That way there is no reason to extend an existing one. It should be trivial to create a new trigger as all the heavy lifting is done elsewhere. All the trigger really has to do is respond to activate and deactivate.

For instance if you take a look at the PayloadEventsExtension, its trigger decorates the EventsCommandTrigger.

Yeh, but that decoration doesn't really do anything other than directly delegate a bunch of method calls and hijack activate and deactivate. (Ah, I can see now that it does indeed delegate to _base.handleEvent(event) in the case that the event is not a PayloadEvent).

On the other hand... Maybe we should take the fact that you chose to modify the EventCommandMap instead of creating a separate extension for the PECM as a warning of some sorts?

That was just because I was too lazy to create a new package and branch etc. It didn't have anything to do with the effort of creating a new trigger.

In terms of duplicate trigger code for wildly different triggers (events vs signals), the only duplication will be the creation of the CommandMappingList, which is a single line.

For very similar triggers (events vs priority events), the execution code will most likely be duplicated (the event handler itself). I'm not sure this is really a problem though. For one thing, why create such similar extensions in the first place? But then if there really is a need, the duplication is not too bad - it's mostly just the payload hooks that will be duplicated.

darscan commented 11 years ago

On an unrelated note: I had an idea for marking mappings as removed so that they can be skipped in the execution loop. It seems to me that people might expect a command not to fire if another command unwires it inside the loop. Of course, new mappings will not be executed until the next execution, but it seems it might be useful to skip over removed ones. What do you think?

darscan commented 11 years ago

BTW, I think I can see what you were shooting for with the decorator stuff. I just worry that it creates very hard to maintain relationships between Command Maps. They're forced to expose certain methods as public and then are committed to maintaining those signatures and behaviours. Personally, I think that cost outweighs the cost of a little code duplication.

creynders commented 11 years ago

the execution code will most likely be duplicated (the event handler itself). I'm not sure this is really a problem though.

Yeah, I realized and agree, it's not a problem.

[...] it seems it might be useful to skip over removed ones.

IMO it's a can of worms. Implicitly you'd be telling people it's ok to rely on the mapping order of commands, since unmapping the command before it's executed does something else than unmapping it afterwards (within the same sequence). It makes code brittle since developers will depend on it, then they switch two lines of code and sometime somewhere a command is executed while it wasn't before. This could be right in your face or hidden in a complex scenario involving many subsequent steps. It's also something that's hard to test since it requires extensive integration tests. No I'm actually happy that now that the linked list is dropped a more consistent behaviour can be presented: if it's in the list of to-be-executed commands it will be executed. Consistency is always easier to deal with than if-then-maybe, even if -granted- it may seem a little counter-intuitive in this case. IMO the message should be loud and clear: there are no guarantees when it comes to execution order, only that it will or will not be executed. Also, this kind of situations are partly what guards are for.

Of course, new mappings will not be executed until the next execution

On top of that, I'm not sure if this is evidently so if you do allow the above..? It is how EventDispatchers work. I think we should deviate from event dispatcher behaviour and be vocal about it. But if we choose we don't want to deviate I'd stick as close to normal event dispatcher behaviour as possible.

I just worry that it creates very hard to maintain relationships between Command Maps. They're forced to expose certain methods as public and then are committed to maintaining those signatures and behaviours.

That's exactly where the fog crept up and the wolves started howling :)


I'm going to shift my focus to a full review of the tests for the time being, I've already issued a few pull requests, but there's still a large number of tests I think are still missing, that nail down the to-be-expected behaviour. I'm starting to think it's better that I keep them all together and provide you with a single pull request, whenever I'm ready. A number of to-be-made decisions will popup and I think it's more clear to see them all at once, no? Especially because a number of them will be related. Or rather issue separate pull requests and keep the related ones together?

creynders commented 11 years ago

BTW why don't you use the Vector#sort here? AFAIK only the sorts w/o comparison functions are faster with Arrays ...? If not, there's one more small speed boost:

var length:uint = _mappings.length;
const mappings:Array = new Array(length);

Should be just a tiny little bit faster

creynders commented 11 years ago

Ah, and what did you think of the CommandCenter as a convenience singleton?

darscan commented 11 years ago

Hiya. Back from work.. In reverse order:

what did you think of the CommandCenter as a convenience singleton?

Yeh, I like it.

why don't you use the Vector#sort here?

I have no idea why I did that. Brain spasm.

I'm going to shift my focus to a full review of the tests for the time being

Awesome.

IMO it's a can of worms

You make some good points.. but I'm not convinced! I'll have to dwell on this for a bit.

creynders commented 11 years ago

Do you think it makes sense to offer this in the commandCenter?

commandCenter.execute( FooCommand )
    .withValueObjects( payload )
    .withGuards( HappyGuard )
    .withHooks( SomeHook )
    .withExecuteMethod( 'somethingElseThanExecute' );

I'd really like to wrap the command center up, get it off my list. And have a real first contribution to the code ;)

Related: I'm starting to think that maybe the CommandCenter and all of the other shizzle (executor, mapper, etc) should be in separate packages? And are you keeping it dsl-package-less? Or was that just for spiking convenience?


TBH I still think it makes most sense to pass a vector of mappings to the executor instead of a CommandMappingList. Unless I'm overlooking something.


In the tests it's clear that there's a preference of integration tests over unit tests. I'm a bit puzzled by the lack of sanity checks for some of the classes though. Apparently there are only integration test -or- unit tests and some tests border on being a hybrid (ContextTest for instance), but I don't want to be over-eager, nor fuck things up by going too strict and thereby messing up a purely pragmatic approach.

darscan commented 11 years ago

Hey! Sorry for delay and brief response.. still at work. Replies inline

On 5 Apr 2013, at 08:31, creynders notifications@github.com wrote:

Do you think it makes sense to offer this in the commandCenter?

commandCenter.execute( FooCommand ) .withValueObjects( payload ) .withGuards( HappyGuard ) .withHooks( SomeHook ) .withExecuteMethod( 'somethingElseThanExecute' ); Totally. Not sure how ValueObjects would work. Maybe we should expose "mapPayload()" etc instead.

I'd really like to wrap the command center up, get it off my list. And have a real first contribution to the code ;)

Indeed, new too! I feel bad about the attribution in terms of the Command Center rewrite. If I commit my branch, maybe you can add the above, and the missing test cases etc. Or, maybe I can bring in one of your branches and then modify it - that way your previous contributions will be recorded.

Related: I'm starting to think that maybe the CommandCenter and all of the other shizzle (executor, mapper, etc) should be in separate packages?

Interesting. What do you propose?

And are you keeping it dsl-package-less? Or was that just for spiking convenience?

A bit of both. I'm not sure how I feel about those DSL packages in general. They create bogus imports, and.. It's the wrong term IMO. They're fluent interfaces, not really Domain Specific Language. But maybe there is value in keeping the separate.. I'm easy.

TBH I still think it makes most sense to pass a vector of mappings to the executor instead of a CommandMappingList. Unless I'm overlooking something.

But the executor needs to be able to remove mappings.

In the tests it's clear that there's a preference of integration tests over unit tests.

I think there's a mix of approaches. I tend to prefer unit tests I'm a bit puzzled by the lack of sanity checks for some of the classes though.

You'll have to point out what you mean here.

Apparently there are only integration test -or- unit tests and some tests border on being a hybrid (ContextTest for instance), but I don't want to be over-eager, nor fuck things up by going too strict and thereby messing up a purely pragmatic approach.

I TDD'd everything I wrote for RL2 (mostly as an exercise), so my stuff falls on the "unit" side, with very little "integration". In reality we need both, so I say go wild.

— Reply to this email directly or view it on GitHub.

creynders commented 11 years ago

Totally. Not sure how ValueObjects would work. Maybe we should expose "mapPayload()" etc instead.

Mmmm. Then they have to do the mapping themselves... Maybe something like

commandCenter.execute( FooCommand )
     .withValueObjects( [foo],[Foo] )

? It's ugly, I admit.

If I commit my branch, maybe you can add the above, and the missing test cases etc.

Yep, sounds good.

Or, maybe I can bring in one of your branches and then modify it - that way your previous contributions will be recorded.

Sounds like more trouble than it's worth.

Interesting. What do you propose?

Not sure. But all the executor etc stuff is strictly facing extension developers, while command center would be facing framework users, that's why.

A bit of both. I'm not sure how I feel about those DSL packages in general. They create bogus imports, and.. It's the wrong term IMO. They're fluent interfaces, not really Domain Specific Language.

Yeah. It bothers me a little TBH, but on the other hand it definitely makes sense to keep them in a separate package, since it keeps the api packages clean and as clear points of entry. Renaming them to fi isn't an option either ;) How 'bout fluent? But I'm not sure whether they really need to be renamed. Maybe it's ok like it is.

But the executor needs to be able to remove mappings.

I used a withCommandClassUnmapper method

In reality we need both, so I say go wild.

You'll regret that ;) No, I'll keep all nicely separated and cherry-pickable.

creynders commented 11 years ago

Thinking about the dsl package some more... Wouldn't it make more sense to have it inside api? It is part of the API.


And maybe for the execute method we could restrict it to one payload object+class? Like in RL1.

commandCenter.execute( FooCommand )
     .withPayload( foo,Foo )
creynders commented 11 years ago

Just to clarify

Thinking about the dsl package some more... Wouldn't it make more sense to have it inside api?

I meant moving the dsl packages to be sub-packages of their api packages. Not just move the contents.

darscan commented 11 years ago
commandCenter.execute( FooCommand )
    .withPayload( foo,Foo )

One problem here is that execute() pretty much has to be the last link in the chain.. otherwise you don't know when the user has finished "configuring". Perhaps:

commandMap.withPayload(foo, Foo)
    .execute(FooCommand, "execute")

The Class and ExecuteMethod parameters are both optional. And in the case where no payload is needed:

commandMap.execute(FooCommand)

Or:

commandMap.forCommand(FooCommand)
    .withPayload(foo, Foo)
    .withExecuteMethod("execute")
    .execute()

Regarding the DSL package.. of all the options I think sticking with the original approach is probably the best at this point.


But all the executor etc stuff is strictly facing extension developers, while command center would be facing framework users

Yeh, that makes sense. In the examples above I called it commandMap, but that's likely to be annoying for RL1 users who will mistakenly inject it when wanting the EventCommandMap.


I used a withCommandClassUnmapper method

Yeh, I remember thinking a little about that approach. It's a bit funny because it's optional.. but then fireOnce is no longer reliable. Perhaps if it became a required construtor argument I'd feel better about it. I might be way off the mark here, but I imagine that you'd like to remove the MappingList from the Executor so that the Executor can be used, for example, in the new "XCommandMap" (or whatever we're going to call it)?


BTW, I'm still uncomfortable with the naming of the "CommandCenter". It would be great if we could come up with something that better represents what it is (a building block for Command Map Extension developers).

"CommandMapCore"? "CommandTriggerMap"?

creynders commented 11 years ago

commandMap.withPayload(foo, Foo).execute(FooCommand, "execute")

This however would mean I need to expose the several with* methods in the commandCenter. It would be nice to have a single point of entry.

And if we run with the other one, this

commandMap.forCommand(FooCommand)
    .execute()

looks weird.

Ok, let's ask ourselves how and when will people be using this?

  1. A command delegation/adapter/relay mechanism of some sort. There's already a command that does what you want, but then you have a different scenario in which you need to perform an extra step, so you create a command which does this and executes the already existing one.
  2. A macro command. A bunch of logically grouped commands that need to be executed in sequence.

Maybe it's an idea to approach this from a different angle and allow command sequencing into the XCommandMap? Don't ge me wrong, we need to KISS here. Just a very simple command sequence execution mechanism accessible from the XCommandMap. It's just that all components already exist to allow synced command sequence execution, all we have to do is find a sensible, clear API.

There's a number of directions we can go with this (I've been thinking about this for the sequencing extension I'd started on) Maybe something like this?

//we can execute a command w/o configuration
commandCenter.execute( SimpleCommand );

//or use a separate method to create one
var buildEarthConfig : ICommandConfig = commandCenter.configureCommand(BuildEarthCommand)
    .withPayload(builders,Vector.<EarthBuilder>)
    .withGuards(QuestionMissing)
    .withHooks(NotifyTheMice)
    .withExecuteMethod('build');

//and let execute accept configs as well
commandCenter.execute( buildEarthConfig );

//or a sequence mixing both
commandCenter.execute(
    CalculateAnswerToLifeUniverseAndEverythingCommand,
    buildEarthConfig
)

Regarding the DSL package.. of all the options I think sticking with the original approach is probably the best at this point.

Probably you're right. I just wonder whether we won't feel ...umm... a bit sheepish about it in a year or two. A bit like seeing pictures of yourself from when you were 15 and had a Skrillex-hairdo, wearing a skirt and a "Welcome to Santa land" t-shirt. (True story) Man, growing up w/o Facebook definitely has some advantages.

Aaaanyway.


Yeh, that makes sense. In the examples above I called it commandMap, but that's likely to be annoying for RL1 users who will mistakenly inject it when wanting the EventCommandMap.

I agree commandMap would be annoying. And misleading. I'd stick with commandCenter for the facade and use commandMapBase or commandMapCore for the extensions building block. IMO *base is a better fit. I'd also keep ICommand together with the facade, since it's facing the framework user too.


but then fireOnce is no longer reliable. Perhaps if it became a required construtor argument I'd feel better about it.

I think that's a good idea. After all it is required.

I might be way off the mark here, but I imagine that you'd like to remove the MappingList from the Executor so that the Executor can be used, for example, in the new "XCommandMap" (or whatever we're going to call it)?

Yeah. However, if we do allow command sequence execution, I'll probably need to create a concrete ICommandMappingList implementation for the new XCommandMap anyway. On the other hand, we need to allow for single command execution as well and then instantiating a ICommandMappingList just to be able to execute it may be a bit overkill?

In general though, I'm a bit squeamish about the circular dependency: EventCommandTrigger > CommandExecutor > CommandMappingList > EventCommandTrigger

I know, it's not a hard-coded one in the sense that the circular dependency is not declared in the interfaces, but it does make my purist heart bleed a little.

darscan commented 11 years ago

Just thinking about this: http://knowledge.robotlegs.org/discussions/robotlegs-2/1337-commands-with-parameters-using-signals

Not the execution part, but the ability to return objects from execute methods. In Parsley it's very common to return an AsyncToken. Parsley catches this object and automatically adds listeners to it. This pins the command instance in memory, and when the token fails/succeeds Parsley invokes failure/success methods on the command passing through the value or error from the token.

I'm not suggesting we do exactly the same thing, but it might be good to collect the commands that we create and return them from the Executor's execute method. The command maps themselves can decide what to do with those instances.

darscan commented 11 years ago

So, I'm now thinking about async commands (as described above), the ability to pass the payload value(s) to execute method instead of injecting them (if desired), and possibly turning the CommandExecutor into a stateless singleton that the Command Maps use (which can also be invoked directly).

darscan commented 11 years ago

It would be nice to have a single point of entry.

Yes, definitely.

I just wonder whether we won't feel ...umm... a bit sheepish about it in a year or two

Haha, yes. Although dsl is nice and short.. and it's close enough (conceptually). Besides, given enough time I feel sheepish about all code I write anyway.

In general though, I'm a bit squeamish about the circular dependency

Ditto. Will fix that.

creynders commented 11 years ago

ok, as we skyped yesterday:

  1. creynders/robotlegs-framework@017d75e89edd45ff2cd604ac2d6b01d1fc771193 : moved the mapping of the payload to the executor
  2. creynders/robotlegs-framework@95f70a10985a218684c686a3270c3cec29452a8e : a bunch of missing unit tests for the command executor. There's obviously some overlap with the tests in the EventCommandMap integration test. I'll clean the ECM IT up later on, if the overlap bothers you. I think it makes sense to keep them in though.
  3. creynders/robotlegs-framework@675b0c82031904876d7a4819262744c1a5899920: added a separate CommandPayloadConfig VO class to pass the payload to the executor. Benefits: lockstep guaranteed and more clean, especially when used in the ECM, as you'll see when you compare with the previous commit.
  4. creynders/robotlegs-framework@7b26914ea26b4a40d87be4169659cff1f3e02cc8 : Then. The passing of payload to the execute method. There's a problem, or maybe you already realized it, and don't consider it a problem. I implemented it as basic and strict (hence 'unforgiving') as possible for the moment. Which means (and here comes the problem) that the parameter order of the execute method needs to exactly match the order of the payload values passed to the executor. I had to do this, since I have no way of knowing the expected parameter types, w/o reflection. It's something we need to think about: are we going for a strict match? Or are we going for as loose as possible: optionally different parameter order, optionally different number of parameters as in payloadValues.

There's benefit/disadvantage in both I think. Loose is... well yeah, loose, which makes it more flexible. But. You'll need to use reflection which is expensive (it needs to be done for each and every command!) AND it might turn into a bit of a nightmare, there's quite a number of directions you can go with that, depending on how loose you want to go, i.e. potentially complex and/or lots of code involved.

I didn't add warnings or checks to the executor/CommandPayloadConfig class for two reasons, I think it's the responsibility of the extension dev to make sure the values and classes match. And also it all will depend a little on what happens with the parameter passing. (Obviously the XCommandMap should provide checks + warnings)

Anyway, you should be able to easily cherry-pick commits or drop them while rebase -i'ing. Obviously the last commit will suffer a little if you drop the config class commit before it, but it's only a really small modification to fix it.

creynders commented 11 years ago

Forgot to ask, workflow strategy for now:

? Or did I miss something?

Oh and, you'd rather I issue a pull request creynders:WIP > darscan:command_executor ? Or will you fetch&rebase? I suppose the latter.

creynders commented 11 years ago

Updated the SCM too. It's a lot cleaner now w/o the (un)mapPayload withPayload(Un)Mapper-thingy

darscan commented 11 years ago

Awesome!

I'm fine with "strict" (ordered) payload passing.

I'll clean the ECM IT up later on, if the overlap bothers you. I think it makes sense to keep them in though.

I'm fine with the test overlaps.

Workflow: yup, let's do that.

Or will you fetch&rebase?

Yeh, I'll fetch from your branch. I'm not going to rebase anything we've pushed though - I only rebase my local working branch when it falls behind our shared, public branch.

darscan commented 11 years ago

I've pushed some updates into: https://github.com/darscan/robotlegs-framework/tree/command-executor

Just some minor refactorings. Also, I've made payload injection optional - will give a little speed boost to those who don't want the payload values injected.

creynders commented 11 years ago

first sketch for the convenience command center

The class names are utter crap.

  1. we definitely need to extract this into its own package.
  2. It's a real pity I need to create a Vector to execute a single command
  3. The required removeMapping-parameter bites my ass
  4. Not sure what to do with this one The getMapping-method is a non-interfaced method of the OnceCommandConfig class. I'd like to keep it, instead of letting OnceCommandConfig implement the ICommandMapping iface, but on the other hand it's an assumed, non-declared contract.
  5. The idea is definitely valid, IMO.
creynders commented 11 years ago

I also reviewed the framework tests. I split a few where there were double assertions. Added some loop-labels, not out of puristy-psychopathy, but because I genuinely didn't catch the flow for a few minutes, so purely for legibility. All real minor changes, see what you think fits.

Next on the task list

darscan commented 11 years ago

we definitely need to extract this into its own package.

Agreed. Still need to figure out our naming strategy though..

It's a real pity I need to create a Vector to execute a single command

Fixed. Although I haven't updated your code to use it.

The required removeMapping-parameter bites my ass

What manner of ass biting is this? Not sure I see the problem.. I'm probably missing something. Should we make it an optional param? I might be OK with that idea now.

Not sure what to do with this one

Yeh, I see what you mean. Maybe the problem is that we're accepting "configs" instead of ICommandMappings. Perhaps it should accept Command Classes OR ICommandMappings instead.

darscan commented 11 years ago

I'm toying with the name: "DirectCommandMap". Maybe we leave the CommandCenter as is, and create a new DirectCommandMapExtension. So, that config would become DirectCommandConfig. Thoughts?

creynders commented 11 years ago

Fixed. Although I haven't updated your code to use it.

I'll pull yours in.

Should we make it an optional param? I might be OK with that idea now.

I'd prefer it, since the mappings I'm using there are immutably once(true). I only return the mappings from the meths at the moment, because I had to pass some kind of callback into the Executor#execute and I didn't want to use a stub.

Perhaps it should accept Command Classes OR ICommandMappings instead.

I'd like to keep the ICommandMapping hidden away from the regular user. That's also part of the ass-biting of the above point.

I'm toying with the name: "DirectCommandMap".

Maybe it's a bit too cute, but what about Commander? It is a commander and nicely ties it to the commands in a more general way. There's no real command map is what I'm getting at. Apart from this, which sounds like a bad idea to me, but I have only glanced it, there's no real pattern attached to the name AFAIK.

darscan commented 11 years ago

I'll pull yours in.

I just pushed another little update (with optional removeMapping).

I'd like to keep the ICommandMapping hidden away from the regular user

Fair enough.

Maybe it's a bit too cute, but what about Commander? There's no real command map is what I'm getting at.

Yeh, whilst I totally see what you're getting at, there's something nice about the naming symmetry of:

EventCommandMap (ECM), SignalCommandMap (SCM), DirectCommandMap (DCM)

Also, it might be nice to save Commander for some other extension.

But I'm not too convinced either way.

creynders commented 11 years ago

ok, so after the above I thought it might be an idea to make it a real DirectCommandMap then.

It worked out real nice IMO, with a very versatile fluent interface:


//simple
dcm.map(FooCommand).execute();

//sequence
dcm.map(FooCommand)
    .map(BarCommand)
    .map(QuxCommand)
    .execute();

//the whole shebang
dcm.map(FooCommand)
        .withExecuteMethod('foo')
    .map(BarCommand)
        .withPayloadInjection(false)
        .withGuards(HappyGuard)
    .map(QuxCommand)
        .withHooks(SomeHook)
    .execute(payload);

The DCM is mapped toType, not singleton, but the executed commands are injected with the configuring DCM instance, not a new one, to save on instantiation stress.

[Inject]
public var dcm : IDirectCommandMap;

public function execute() : void{
      dcm.detain(this);
}

Which means that as long there are commands that are pinned, they maintain that instance of the DCM, until they release themselves from the Pin, in which case the DCM is released for GC too. Do you foresee any possible shizzle with that approach?

Funny thing... now I don't need the last modifs you made anymore... sorry. If you want to rollback, let me know.

creynders commented 11 years ago

Ah yes, and since it uses the CommandMappingList obviously you don't have to chain everything together, this works just as well:

dcm.map(FooCommand)
    .withExecuteMethod('foo');
dcm.map(BarCommand)
    .withPayloadInjection(false)
    .withGuards(HappyGuard);
dcm.map(QuxCommand)
    .withHooks(SomeHook)
    .execute(payload);

Now that I think about it, maybe it's best if DirectCommandMap still does implement a execute method too, so you can do:

dcm.map(QuxCommand)
    .withHooks(SomeHook);
dcm.execute(payload);
darscan commented 11 years ago

Looking good! I've pushed it into our working branch:

https://github.com/darscan/robotlegs-framework/tree/command-executor

I've also add the command response stuff. Not 100% convinced on the approach, but seems simple and lightweight:

https://github.com/darscan/robotlegs-framework/commit/37ac8a5ce78fbfaf7e8c629a06b7276733862f81

pixels4nickels commented 11 years ago

This thread makes me happy. Lots of great reading and ideas. Rock on! On Apr 10, 2013 1:24 PM, "Shaun Smith" notifications@github.com wrote:

Looking good! I've pushed it into our working branch:

https://github.com/darscan/robotlegs-framework/tree/command-executor

I've also add the command response stuff. Not 100% convinced on the approach, but seems simple and lightweight:

darscan@37ac8a5https://github.com/darscan/robotlegs-framework/commit/37ac8a5ce78fbfaf7e8c629a06b7276733862f81

— Reply to this email directly or view it on GitHubhttps://github.com/robotlegs/robotlegs-framework/issues/114#issuecomment-16199577 .

creynders commented 11 years ago

@pixels4nickels :)

@darscan I re-added the execute meth to the DCM. And did a clean up of the DCM, ECM and CC tests. Didn't really touch the tests themselves (added a few though) but primarily extracted duplicated internal support classes to public classes. If you don't like it, feel free to drop it. Ah... should've kept the few added tests in a separate commit of course... sigh

There's one thing that's a bit ... awkward ... with passing the payload to the execute method: commands mapped to a typed event are passed the event twice: generic and typed I added a test to lock it down, but maybe we need to think about this.

Not 100% convinced on the approach, but seems simple and lightweight.

Looks fine by me. Yeah ... maybe a VO would be a little more clean ..?

creynders commented 11 years ago

@darscan Pushed a few minor changes.

and @pixels4nickels I was looking at the SCM again and realized that non-strict signals' payload is never injected into commands, which probably makes sense. But on the other hand, it would be pretty easy to map the payload values to their concrete classes and allow for injection into commands. Do you think that's a good idea? Or could it be pandora's box?

creynders commented 11 years ago

@darscan

So now that command payloads are baked into RL, it got me thinking... Wouldn't it be really cool (and useful, and flexible [!]) to be able to do this:

//FooCommand.as
[Inject]
public var foo : FooVO;

public function execute() : void{
    foo.bar();
}

//FooView.as
var foo : FooVO = new FooVO('ow','yeah');
dispatcher.dispatchEvent( new FooEvent( FooEvent.FOO, foo));

As you know I already developed a PayloadEvents extension which allows this, but it uses a PayloadEvent class that the events must inherit from:

//FooEvent.as
public class FooEvent extends PayloadEvent{
    static public const FOO : String = 'foo';
    public function FooEvent( type : String, foo: FooVO ){
        super( type, foo );
        withValueClasses( FooVO );
    }
}

While it does the job, it's a pity that events are tied to that specific class/interface. So I started phantasizing how this could be done w/o the necessity to inherit from a specific event class. There's a few directions:

  1. Explicit wiring

    [Inject]
    var payloadMap : IPayloadMap;
    
    payloadMap.map(FooVO).toType(FooEvent);

    Could be interesting. The question is how do we know which member to poll for the FooVO instance?

    Obvious one:

    payloadMap.map(FooVO).toType(FooEvent).withMember('fooPayload');

    But I don't really like having to declare the member name as a String. It's even worse than requiring to inherit from a specific event class.

    So, another option would be to tie it to an interface with a getPayload method, or so. Meh.

    But maybe this could be real cool:

  2. Meta-tag

    //FooEvent.as
    public class FooEvent{
       static public const FOO : String = 'foo';
    
       private var _foo:FooVO;
       [Extract]
       public function get foo() : FooVO{
           return _foo;
       }
       public function FooEvent( type : String, foo: FooVO ){
           super( type );
           _foo = foo;
       }
    }

    When the event is dispatched, it's processed and the Extract-tags are read. The extractionpoint descriptions would obviously be cached. The value object would be mapped in the injector to the type it's declared with in the event. This would be complementary functionality, i.e. the EventCommandMap would provide all of the functionality as it does now as well of course.

    I've been rummaging through SwiftSuspenders and I actually believe this should be feasible w/o too much hassle. It's A LOT easier than what Till had to do for the injections, there's a lot less to deal with: basically it's either a variable, accessor or method and they all have/return just one possible value.

    Should we notice that it's a bit on the CPU-intensive side still, we can provide a configuration-time parsing of the extraction points; payload events must/could be mapped up-front, a bit like RelaxedEventMap#rememberEvent

    I understand this seems like quite a heavy change, but regardless of possible practical shizzle, wouldn't it be cool?

creynders commented 11 years ago

As I thought the parsing part is actually real easy, I just spiked it: (obviously non-optimized etc. etc.)

var extractionPoints : Array = [];
for each (var node : XML in description.*.
    (name() == 'variable' || name() == 'accessor' || name() == 'method').metadata.(@name == 'Extract')){
    var fullNode : XML = node.parent();
    if( ! (fullNode.hasOwnProperty('@access') && fullNode.attribute('access') == 'writeonly')){
        var name : String = fullNode.attribute('name');
        var type : String = ( fullNode.hasOwnProperty( '@returnType' ) )
            ? fullNode.attribute( 'returnType' )
            : fullNode.attribute( 'type' );
        extractionPoints.push( {name:name, type:type} );
    }
}
trace( ObjectUtil.toString( extractionPoints ) );

That is basically it. I've tested it with instances, classes, inheriting classes (with an extract-tagged member in the base class) and even interfaces.

creynders commented 11 years ago

In my excitement I forgot to ask the question:

Is this something we want to provide out-of-the-box? Or should I isolate it to my PayloadEvents extension?

creynders commented 11 years ago

So I finished my first sketch of the PayloadEventCommandMap which can be configured with Extract-tagged members

I added extract-tag-ordering too, to be able to declare in which order they'll be passed to execute methods that have parameters.

And now that I've gone down that road there's just one more thing to add to make RL really super flexible and easy to wire up: the Execute-tag

//LoginRequestedEvent.as

[Extract]
public var loginData : LoginVO;
//RemoteAuthenticationService.as

[Execute]
public function authenticate(loginData : LoginVO):void{
    //authenticate
}
//AuthenticationConfig.as

[Inject]
public var eventCommandMap : IPayloadEventCommandMap

eventCommandMap.map(LoginRequestedEvent.LOGIN_REQUESTED).toCommand(RemoteAuthenticationService);

This should be very little work to implement.

I do understand adding two new metadata-tags is a bit too much, but IMO this is functionality we should provide to the framework users in the MVCSBundle, it allows for superfast development.

creynders commented 11 years ago

execute-tag implemented

darscan commented 11 years ago

Woah! Epic. I'm still not really online. Will check it all out in a couple of days.

darscan commented 11 years ago

Aaaand.. I'm still not really back! But I've found a little more internet so..

There's one thing that's a bit ... awkward ... with passing the payload to the execute method: commands mapped to a typed event are passed the event twice: generic and typed

Ah.. yes, that is bothersome. Will think on it. My initial (certainly bad) thought was to remove sequential duplicate instance from the array before applying. But that is super nasty.

Really the issue is that we're only mapping both because of the tiny use-case where someone want to create a kind of "base" command that can deal with any event. Perhaps the solution is to expand the event mapping configuration to make this opt-in.

eventCommandMap.map("x").toCommand(X).injectGenericEvent();

Forgetting that for a second, the good thing about giving the EventCommandMap it's own mapping classes is that we're free to evolve it in the future (separately from the Command Center). On the other hand, the EventCommandMap also exists as an example for extensions developers, and doing this will make it look like you have to create all your own mapping classes (which totally blows).

Getting back to the issue at hand, and throwing some ideas around wildly.. what if the abstract event is only mapped if you supply Event as the second param to map()? If you supply the param, it maps to that. If you leave it out it maps to the event's concrete type.

Any which way we go, I've never been happy with that double-mapping thing.

Looks fine by me. Yeah ... maybe a VO would be a little more clean ..?

That was my initial approach, but I don't want to be creating all those throw-away instances.

execute-tag implemented

Good stuff. Will need much more time to dig in properly.

creynders commented 11 years ago

what if the abstract event is only mapped if you supply Event as the second param to map()?

I like it. It would probably cause some minor migration issues though, when porting RL1 projects to v2. While we're at it, could we reach a decision on #119 ? As I see it now:

eventCommandMap.map( SupportEvent.TYPE1 ).toCommand( CallbackCommand );
dispatcher.dispatchEvent( new SupportEvent( SupportEvent.TYPE1 ) ); //event mapped as SupportEvent
assertThat( wasTriggered, equalTo( true ) );
eventCommandMap.map( SupportEvent.TYPE1, Event ).toCommand( CallbackCommand );
dispatcher.dispatchEvent( new SupportEvent( SupportEvent.TYPE1 ) ); //event mapped as Event
assertThat( wasTriggered, equalTo( true ) );

The master-branch fails the last assertion, but if we let it pass in the new version, then it is consistent with the EventMap.

but I don't want to be creating all those throw-away instances.

Yeps.

Good stuff. Will need much more time to dig in properly.

No probs.

creynders commented 11 years ago

Ok, I updated the EventCommandMap with single event injections.

I interpreted it as follows:

eventConstructor    | _eventClass       | | payload
---------------------------------------------------------
---------------------------------------------------------
ConcreteEvent       | falsey            | | ConcreteEvent
ConcreteEvent       | ConcreteEvent     | | ConcreteEvent
ConcreteEvent       | Event             | | Event
Event               | falsey            | | Event
Event               | Event             | | Event
Event               | ConcreteEvent     | | -