piotrmurach / finite_machine

A minimal finite state machine with a straightforward syntax.
https://piotrmurach.github.io/finite_machine/
MIT License
808 stars 38 forks source link

Defining helper methods on machine, not on target #55

Closed shioyama closed 5 years ago

shioyama commented 5 years ago

Great gem! Really nicely designed with the level of customization I was looking for. :smile:

Now to my one little problem...

Problem

I have a lot of custom methods which I use in if conditionals, etc. which do not belong in the target and which in any case ideally I'd like to access directly in transition blocks. In the readme the recommendation is to write code like this:

on_enter_start do |event|
  target.turn_engine_on
end
on_exit_start  do |event|
  target.turn_engine_off
end

But as complexity is added to the machine, I would really prefer to just call turn_engine_on etc. here and define that method on the machine itself. Especially if (as in my case) turn_engine_on is not a method that only relates to the state machine logic and does not belong on the target.

I can do this by subclassing FiniteMachine::StateMachine, which works more or less, but it means that to define the machine itself I need to override initialize and define the block in there, which doesn't seem right. The recommended way is to use FiniteMachine::Definition, but doing that means that you can't define these local methods on the finite machine itself, since internally FiniteMachine::Definition.new hard-codes FiniteMachine here:

https://github.com/piotrmurach/finite_machine/blob/b956cf3b50870734c6a350e65141208e96a8c1b9/lib/finite_machine/definition.rb#L39-L44

What would be the recommended way to define methods for the machine which are locally accessible in block contexts? I feel this is an important issue when thinking about encapsulating machine-specific logic within the machine itself rather than in the target.

Thanks for reading :smile: And keep up the great work on the gem.

piotrmurach commented 5 years ago

Hi Chris,

What you described seems like a sensible request. The reason I designed FiniteMachine this way is to ensure separation and as little coupling as possible. The danger with including/inheriting from the machine and being able to add your methods is as with any inheritance really, you can overwrite interface that otherwise shouldn't be tinkered with.

You have to also recognise the ambiguity of your request. In the past I had some lookup rules when a method didn't exist on machine itself, then methods on target would be checked. Bear in mind that all events are also methods that can be called and in a way pollute the machine namespace. By always being explicit that we call a method on the target, we remove any ambiguity.

Having said that, I would be happy to consider an alternative implementation to FiniteMachine::Definition that allows extending finite machine with custom methods outside of target context. Do you have anything in mind, can you work up a PR?

shioyama commented 5 years ago

Thanks for the thoughtful reply!

Bear in mind that all events are also methods that can be called and in a way pollute the machine namespace. By always being explicit that we call a method on the target, we remove any ambiguity.

Yes I've noticed this issue, and in the end I feel that the immediate scope is probably not the best place to define helper methods.

I'm going to close this for now and see if I can work within the existing constraints.

Thanks again!