alexpeachey / end_state

A State Machine implementation
MIT License
9 stars 5 forks source link

Proposal: Builder Interface vs. Subclasses #24

Open alexpeachey opened 9 years ago

alexpeachey commented 9 years ago

Right now, to create a machine we have the developer subclass EndState::StateMachine and then just use a bunch of class macros to define the machine. This works just fine except, as was pointed out in a recent pull request, we have the StateMachine class responsible for 2 large things, the first is the configuration/definition part, and the second is the actual working machine that acts as a delegator wrapping the stateful object.

I find the subclassing here odd because the subclass doesn't override anything in the super class and gets all of it's behavior and function from the macros. So your typical StateMachine class is just several calls to the transition macro. On the other hand the Guard and Concluder subclasses make sense because you are basically providing an implementation and overriding a default method in each. They also likely have additional build out that has all the logic for how to do what they need to do to guard or conclude the transition.

An alternative way we can handle the machine generation would be using a builder that actually creates a machine class for us. The classes created would still actually be subclasses of a StateMachine class, but rather than create a class like you do now it would be more like this:

MyMachine = EndState::Machine.build do |machine|
  machine.transition first: :second, as: :shift_up do |transition|
    transition.guard SomeGuard
    transition.concluder SomeConcluder
  end
end

With this setup, MyMachine is still a subclass of StateMachine but it doesn't have all those class methods it does now. You still do MyMachine.new(stateful_object) and everything else works the same. The difference is a new thing EndState::Machine has a build method that yields a new StateMachineConfiguration object that has an instance method transition on it that is used to define the transitions. That object then holds all the transition maps, event-aliases, etc. The build method then creates a new class that is a subclass of StateMachine and has it's configuration set to what was built using the builder.

Thoughts?

alexpeachey commented 9 years ago

I probably should tag @brianvh and @charlierudolph so they know I put this here.

charlierudolph commented 9 years ago

:+1: Helps encapsulate a lot of things and simplifies the class we hand off to the user.

brianvh commented 9 years ago

I think I'm on board with this change, for the most part. I think it would make things much clearer if I could see how the EndState::Machine class was implemented.

I assume the plan would be to deprecate the existing EndState::StateMachine inheritance interface? Also, is this method reminiscent of how the Naught Builder class operates?

alexpeachey commented 9 years ago

Yes, I kind of based this off of what Naught did. I kind of like it being more of a class Generator/Builder.

In my mind EndState::Machine would be fairly simple. It's job is to new up a EndState::StateMachineConfiguration that it yields in the build method. It then grabs it after yielding and dynamically creates a new class. Right now my thought is this class would be a subclass of EndState::StateMachine but it doesn't have to be. I'm thinking the EndState::StateMachine is where all the method missing stuff and instance level transition of the current one live so it's good to have these in a class that could be subclassed. However, it doesn't need to be that way and build could actually dynamically build the whole class (likely with some heredocs). We could also turn EndState::StateMachine into a module that we just extend the class we are building with.

There's a lot of room for exactly how this gets executed but that's some more rambling on what I'm thinking.

Also, since I imagine it's possible some people may actually want some custom functionality in their generated class we could add another method onto the configuration similar to what I did with ActiveNull. And we just create a module that we dump the block into and mix into the class we are building.

MyMachine = EndState::Machine.build do |machine|
  machine.transition first: :second, as: :shift_up do |transition|
    transition.guard SomeGuard
    transition.concluder SomeConcluder
  end

  machine.custom do
    def some_special_method
      # Cool stuff unique to my machine.
    end
  end
end