Skip to content
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

Pass previous and current state to the callback #31

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions lib/micromachine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ def initialize(initial_state)
end

def on(key, &block)
if block.arity > 3
raise ArgumentError,
"Callback for #{key} have #{block.arity} arguments, but only 3 allowed"
end

@callbacks[key] << block
end

Expand Down Expand Up @@ -44,9 +49,21 @@ def states
private

def change(event)
@state = transitions_for[event][@state]
previous_state, @state = @state, transitions_for[event][@state]

all_arguments = [event, previous_state, state]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn’t this be @state?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope.

There interface to access the state: state method. Direct reads from the internal state representation are breaking encapsulation. Should the @state representation change, for example, to hold integer values internally, it will break code that reads ivars directly.

Speaking from the SOLID point of view, there too much responsibilities on the change method. Because not it only changes the state, but also notifies callbacks. Ideally, callback notification code should be extracted to the, um, notify method.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm. I tend to be very skeptical of claims of SOLID in the way that I’m also skeptical of claims of POLS for Ruby.

I can buy your argument for a notify method that must be triggered by change…and that means you have to basically call notify(event, previous_state, state)).

Your argument for the state reader means that the assignment at the top should be previous_state, @state = state, transitions_for[event][state]. I’m not sure there’s much increase in readability or reliability because state is just an attr_reader. IMO, direct access to an instance variable is exactly what a private method like change should be allowed and expected to do.

It doesn’t much matter to me, in the end, but there’s an inconsistency in use here.


callbacks = @callbacks[@state] + @callbacks[:any]
callbacks.each { |callback| callback.call(event) }
callbacks.each do |callback|
case callback.arity
when -1
callback.call(*all_arguments)
when 0..3
arguments = all_arguments.take(callback.arity)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the temporary variable?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep.

To reduce the number of array allocations during the each invocations. But it just came to me, that take will allocate new array for result, so yeah, this is no use.

Alternative is to expand the case to explicitly match each number of arguments, in this case we can avoid array allocation, but at the cost of the additional branching.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah; I’d done exactly that for #27 (increased branching). You’re guaranteeing at least n+1 Array allocations where n is the number of callbacks. This is probably small, but depending on how the state machine is used…it may be important to ensure optimization here. And no, I don’t actually have a clue as to whether the branching or allocations are more expensive in performance; that’s what benchmarks are for.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I'm thinking is if the feature could be solved by the application using this library by keeping the value of the previous state. The impression I have is that the use case is not very common, but of course I may be wrong! In any case, if the application side can solve it, maybe that's a good start and we can communicate that use case in the documentation.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I was looking at this, @soveran, is that one couldn’t necessarily count on it being solvable in the application. If you are injecting micromachine in a Sequel model, you have #changed? on the model; with an ActiveRecord model, you have a similar method. I injected micromachine in something that just does PORO and just wants micromachine to keep track of the problem.

That said, I think I was able to work around the problem…but it left me somewhat unsatisfied. Note that both #27 and this PR fix the problem I described in #26 in that the callback mechanism was broken for folks who upgraded.

callback.call(*arguments)
end
end

true
end
end
78 changes: 78 additions & 0 deletions test/callbacks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,31 @@
assert_equal "Pending", @current
end

test "raises error on wrong number of arguments on callback" do
machine = MicroMachine.new(:pending)
machine.when(:confirm, pending: :confirmed)

assert_raise ArgumentError do
machine.on(:confirmed) do |a, b, c, d|
end
end
end

test 'passing nothing to the callbacks' do
callback_called = nil

machine = MicroMachine.new(:pending)
machine.when(:confirm, pending: :confirmed)

machine.on(:confirmed) do
callback_called = true
end

machine.trigger(:confirm)

assert callback_called
end

test "passing the event name to the callbacks" do
event_name = nil

Expand All @@ -42,3 +67,56 @@

assert_equal(:confirm, event_name)
end

test "passing the event name and previous state to the callbacks" do
event_name, previous_state_name = nil

machine = MicroMachine.new(:pending)
machine.when(:confirm, pending: :confirmed)

machine.on(:confirmed) do |event, previous_state|
event_name = event
previous_state_name = previous_state
end

machine.trigger(:confirm)

assert_equal(:confirm, event_name)
assert_equal(:pending, previous_state_name)
end

test "passing the event name, previous and current state to the callbacks" do
event_name, previous_state_name, current_state_name = nil

machine = MicroMachine.new(:pending)
machine.when(:confirm, pending: :confirmed)

machine.on(:confirmed) do |event, previous_state, current_state|
event_name = event
previous_state_name = previous_state
current_state_name = current_state
end

machine.trigger(:confirm)

assert_equal(:confirm, event_name)
assert_equal(:pending, previous_state_name)
assert_equal(:confirmed, current_state_name)
end

test "passing the everything to the callbacks" do
event_name, previous_state_name, current_state_name = nil

machine = MicroMachine.new(:pending)
machine.when(:confirm, pending: :confirmed)

machine.on(:confirmed) do |*args|
event_name, previous_state_name, current_state_name = args
end

machine.trigger(:confirm)

assert_equal(:confirm, event_name)
assert_equal(:pending, previous_state_name)
assert_equal(:confirmed, current_state_name)
end