Open creynders opened 13 years ago
The first "bug" is indeed reflection
I dont understand 'overseeing the complete wiring' ? I don't ever work that way so I'm never bothered by it. I expect an event to drive a command because it's the correct thing to do, else why would you use commandMap.execute? Why not just call it explicitly? If you really wanted to do it, you'd do [Inject] controller:MyController; controller.doThing().
Still, i've never found such a thing beneficial. As for having explicit reference to the event, I find it's one too many levels of indirection (there's already one level, you can pick a different event on either end). The mediator/actor/service has to emit an event, the event needs to be explicit in it's intent, and the event needs to be wired to a controller. Rather than configure the event to fire a command, I configure the controller to accept an event. It's slightly tighter than by-configuration, but it's never an issue for me. I'd like to see some code examples of where it's a nuisance - I expect I've missed something essential here, so I'll investigate it some more myself too.
Although it's semantic, there's two ways i look at events and controllers:
Events which controllers expect (event and controllers share namespace) - The controller offers the event you would use to issue a command. Event which controllers connect to (event shares namespace of other actor) - The controller is wired to react to an event the actor emits.
For mediators, you use the first. For model the second.... For services? I always used the second, but I'm questioning myself... I think rather than Services having 'onSuccessEvent' next to them, perhaps just having the service raise the commans 'updateListOfFoo' or 'updateWidgetModel' etc..
You've got me thinking :D
Now to your last three q's:
The only time i've ever sequenced commands is in configuration, where i think i'd be better off without commands and just use plain-old-async, the controller is not your only way to obtain indirection. Also I tend to place async results into modelling every single time. I model almost everything tho, it makes a huge difference... asyncResult->model->modelUpdateEvent->command->asyncResult..etc
I hadn't thought about manual and context mediation. I think we can simply add a MediatorExtension.mediate.. I will look into it when i get a moment. Automattic mediation of the 'root' should be straightforward.
Performance. The detection is on mapping, and i try to be as efficient as possible with cheap-early optimisation. Meaning i evaluate a class on it's simplest attributes first. Reflection is no more expensive than say in RL or any other framework, and e4x for the obvious ( has "controller" in namespace for example) is pretty quick.. The slowdown will generally be when the pp/module comes on line. The BAD part is it all has to happen in one frame :/ I dont think i can get around that either. Probably worth trying to evaluate the cost per class. Another consideration i have, is that every detector will add 'some' slowdown to all mappings. I will try and get some data on this when i'm done with the TDD rewrite of the steam 'detectors'.
Thanks for your input! Really helpful and gave me some things to prioritise :D
Re: overseeing the complete wiring, in my PrepControllerCommands, I can see how the application/functional domain is wired, no need to dive in-and-out of commands to find out on which events they execute. It reads like a summary of what happens when.
I use the macrobot utility a lot, which calls commands in sequence or concurrently. I've encountered a lot of situations in which I've got a command mapped to an event, but later on when requirements change I need to "squeeze" in another command before that. Rather than mapping that before the other one, in which case mapping order is determining (which should be avoided as much as possible) I create a separate sequence command, map the event to it, unmap the first one and let the sequencing command call both. No need to refactor my first command at all. In general my commands NEVER use the event or signal they are mapped to, only the payload.
In my experience command chaining becomes a mess if not centralized. It's not a problem when you set it up, but it can become very chaotic when use cases change, commands need to get squeezed in or moved in the sequence. My aim is to always add and remove classes and wirings, and never needing to refactor classes to change sequence (except obviously the sequencer) since IMO it goes against SRP.
Yet another question, as you can see I've setup the MarkersController as listening to an async service, while at the moment the service is synced. However, if I'm not mistaken the controller's freed for GC when its event listener has finished execution, right? This would mean that could give potential problems, when I switch my sync service implementation to an async one, won't it?
again i see sequence management etc as 'modelling' - so I write the model that drives behaviours, perhaps is configured to emit specific events in specific order. It's the same thing, but the framework doesn't give it to you. You need to write the component you want and configure it. You could easily add such a component to the framework if its a common place tool.
Controllers are GC'ed after the lifetime is told to 'die' (not sure if i've coded the teardown correctly yet, but this is the intent)
So a controller is instanced as soon as it's mapped, which some might say is too early, but i like not having injections being satisfied for each 'event' in the system... The controller then stays in memory until teardown time (end of lifetime).
I'll point back to the start of the first article. "This is my highly opinionated take on how to code up an app."
When I first got into RL, the dynamic map/remap of the controllers made me gush. I thought, that is so powerful, I have all this flexibility!
As it turns out, any time I tried to change the mapping of commands dynamically, the system became more confusing. If i then just hard wired all my commands, and controlled flow with modelling, the confusion went away. Explicit logic modelling meant i was never guessing what the current mappings were, what they meant, and wether they would trigger or not.
So here it is, you configure steam by convention and the convention causes it to be wired up early. You can defer wiring by not mapping dependencies in the config classes, But i wouldn't suggest it.
I would consider your "overview" of mappings a design smell. We often traced our mappings out to the console for the same "overview" (in our RL apps) and I found that quite 'smelly' too. We look at those a lot less now that we focused on accurate, conventional, meaningful language. Class names and namespaces are powerful if you take your time to keep the language accurate.
I'll try to demonstrate this sometime. I'll see if the example you pushed provides enough complexity for me to demonstrate how it could be 'clarified through convention'.
Right now im putting a full suite of tests on the 'Detectors' to ensure we detect conventions correctly, and demonstrate how to write extensions. When thats done I'll set up a priority list of things to address.
Ok, I understand re: sequence management as modeling. I should have mentioned I use a FSM many times as well to achieve the same (I just don't call it a model)
I still don't see how meaningful language (alone) would reflect wiring overview. To me meaningful naming, which I think I do, is making sure that commands/controller meths define intent, mediator events define user requests and service or model events define service/model state change. However at some point a translation of service state change to system action needs to happen and they are not always directly related. That's where naming alone doesn't suffice to give an overview of application logic. Somewhere you have to be able to see: when this happens, do that. Not? Probably I just don't "get" how it would work otherwise, so an example is always welcome.
I only say its not needed because we don't rely on it. Our RL controller mappings are namespaced and broken up anyway. It's these seperations that looked to me, exaclty like a 'controller' - that is, the namespace controller/users would have:
In boiler these would become the UserController, or the UserEditingController and the UserListController (because I usually model editing seperately).
In fact we found a way to make just about everything we do, selection, editing, collecting VO's etc all work with the same modelling patterns. This means we quickly identify the controller, model and service classes required.
When we have to jump outside our own conventions, we try to seperate those too, a good example in our code is the 'NavigateAway' pattern. This is the pattern where we know the user is leaving something in an incomplete state and ask them if they really want to discard their work (Apple's model of saving as you go doesn't necessarily work in our model, because most of our edits result in notifications to other users).
So i look at a pattern like NavigateAway as the exception, and In boiler I realise I can establish an easy-to-include, per-module "NavigateAwayExtension' that by convention or configuration expects a certain model to be in a not-dirty state before the user can leave. I havent tried this yet, but it's my goal. I would like to mimic something i find nice in Rails, which is the 'Acts-as-' mix in of system/module behaviour.
What this means is that while there are lots of simple and creative ways to deal with coding problems in RL, I want Boiler to promote only conventional, 'Bus Factor' friendly solutions. Where deviation from convention is made easy only through conventional hooks which promote establish new, easy convention that can be reused. I wish I could be more eloquent about this, but for now i'll just keep chugging away at getting samples together.
When I look at our app, although some of the modules achieve some rather interesting UX methods, such as drag and drop scheduling on a streamed timeline, large scale mapping with clustered data, client side data processing for presentation etc, It is, by and large, all the same patterns over and over again. I don't need flexible, seperately-configurable wiring at all, I need a way to quickly express the same patterns in a short amount of time. Writing a new resource editor shouldn't really need thinking about. Not until I'm dealing with the best way to provide reinforcement and simplicity to the user. Even if the patterns are less than optimal, so long as they're quick and reliable and well understood, I can focus on greater user experiences.
In short, Boiler is not for the coder who loves to cut code as much as it is for the coder who's job is to create usable product. It's a very business like mechanism for shifting focus. I'm both types of coders, but I want something I can depend on when I need to be the Product-Focused coder.
I tell you what, I really look forward to how RL2.0 emerges, because if they deal to extensibility well, then perhaps Boiler could be recoded over the top of RL to add convention and low-polymorphic architecture. It is only intended as a way to shift design practice from invention to reuse, but to focus on reuse you need to use inversions of control that do not stimy mixed inheritance. This is what I've learnt from Ruby anyway, and why I love Boiler is that I think i've found proof that it's possible in a strict language that doesnt have traits (in strict mode).
Setup went pretty smooth, it just took a few tries to find out what dependencies are required (Boiler, Steam, HookableSuspenders,...) which obviously won't be a problem when using a .swc
I'm still not sold on ducktyping, but that's just a matter of preference, of course.
In general, it took me very little to adapt my mindset to working with Boiler, coming from RobotLegs. To me having a controller instead of commands is totally acceptable and actually makes more sense if you write service event handlers/callbacks in your controllers (as I do in RL, which I've always considered not clean, yet far more practical than having another command)
Noticed a small bug (?) there, the controller's constructor is called twice even though I only had one event mapped to it. Probably it's instantiated once for reflection?
I like having a separate mechanism for configuration, it feels more clean and makes the application more readable. However I missed overseeing the complete wiring, it's one of the main benefits of RL that you can see at a glance what events are mapped to which commands. In fact in RL, I sometimes use mediatorcommands instead of mediators, simply for having all of the wiring centralized. Unless if I'm mistaken it's not possible to map several events to one controller method, right? I know, obviously I could map them to separate methods and relay them. I don't like that the controller is required to have a reference to the dispatched event, in RL the commands can be completely agnostic to whether they are mapped to an event or called directly by commandmap.execute, which is more flexible.
Then I had several questions: