[HN Gopher] State Machines in Ruby: An Introduction
       ___________________________________________________________________
        
       State Machines in Ruby: An Introduction
        
       Author : unripe_syntax
       Score  : 100 points
       Date   : 2022-06-22 11:06 UTC (1 days ago)
        
 (HTM) web link (blog.appsignal.com)
 (TXT) w3m dump (blog.appsignal.com)
        
       | kdasme wrote:
       | I love this, thank you!
       | 
       | After playing around different JS frameworks I'm back to Rails
       | and I've been able to iterate quickly. That gem solves a few pain
       | points.
        
       | fortysixdegrees wrote:
       | A lot of the comments here are rails oriented and anti state
       | machine. In the context of rails that may make sense, but I do a
       | lot of non rails ruby, and non object oriented Dev style, and
       | state machines are a great way to write solid systems where the
       | allowed state and transitions are explicit and easy to reason
       | with.
        
         | Toutouxc wrote:
         | Would you mind sharing a little example of what you mean by
         | non-OOP Ruby code with state machines?
        
         | bowsamic wrote:
         | Unfortunately almost everyone treats rails and Ruby as
         | synonymous
        
         | Sinjo wrote:
         | Honestly state machines are fantastic in Rails too. My last
         | company built Statesman
         | (https://github.com/gocardless/statesman/) and being able to
         | lean on it to prevent you getting into invalid states is
         | fantastic. You also get the bonus of tracking the history of
         | states your resources went through (which is especially useful
         | when you're dealing with payments).
         | 
         | At some point you'll have to think about query performance on
         | the state transition table, but it'll go further than you think
         | and is firmly in the realm of problems of success.
        
         | craigkerstiens wrote:
         | A pretty big +1 to this. My team (at Crunchy Data building
         | Crunchy Bridge[1]) and similar teams at prior Citus Data and
         | Heroku did a lot of Ruby and a lot of state machines. In all
         | cases it was actually less Rails, but that may be because the
         | team really prefers sequel as the ORM to ActiveRecord, though
         | Rails does a bunch that doesn't really apply to such a
         | workflow. We wrote about some of how we used state machines to
         | power our database as a service and the full control plane
         | previously[2]. Another big piece of the approach and stack that
         | helps today is leveraging Sorbet[3] and our approach to test
         | coverage, we probably should blog a little more on some of this
         | but it helps quite a bit.
         | 
         | [1] - https://www.crunchydata.com/products/crunchy-bridge
         | 
         | [2] - https://www.citusdata.com/blog/2016/08/12/state-machines-
         | to-...
         | 
         | [3] - https://brandur.org/fragments/large-scale-ruby-
         | refactoring
        
       | ianbutler wrote:
       | I worked at a business (in ruby) where the entire core logic of
       | the app was a state machine, as an idea the thought was good, a
       | state machine modeled order flows pretty well, but in practice it
       | was very slow and the amount of side effects that had to be
       | considered as the business grew just compounded this.
       | 
       | It got to the point where this state machine approach was a real
       | bottleneck and though I left before it was resolved last I heard
       | they had to put real engineering time in to move off of it.
       | 
       | Be very careful when deciding to go this route.
        
         | duffyjp wrote:
         | On the flip side, I've built several production apps with AASM
         | or Workflow that made adapting to changing business logic a
         | breeze. If you're given a flow-chart you can translate that 1-1
         | and when that chart changes it's easy to adjust.
         | 
         | It was also really easy to write tests for.
        
           | SaltyBackendGuy wrote:
           | This has been my experience as well with AWS Step Functions.
           | Obviously your use case is critical to the architecture
           | decision. For us it's made automating workflows a breeze.
        
           | ianbutler wrote:
           | For sure, this isn't an indictment against state machines, my
           | message was exactly to be careful when making the decision to
           | use them. They fit a lot of workflows and I've also worked in
           | environments where they were not at all a bottlenecks, and
           | regardless of the performance bottleneck it wasn't hard to
           | reason about or adjust which for a long time meant more than
           | performance for that business.
           | 
           | I guess I could amend my statement to "be careful about which
           | library or implementation of a state machine you go with"
        
       | beckingz wrote:
       | State machines are harder to update than regular code as your
       | understanding of the domain changes, which often leads to
       | suffering.
        
       | Toutouxc wrote:
       | I get how these state machine gems can sound like a great idea if
       | what you need is taming some state changes that are just a little
       | too much to handle by hand, but they can also delay the
       | realization that you're doing something wrong.
       | 
       | State changes are hard and most involve more than object, there
       | are usually associations that need to be taken care of, validated
       | or maybe also transitioned to a different state, there can be a
       | myriad of conditions and things can go wrong at any moment. And I
       | feel that being able to define state transitions this easily,
       | almost as an afterthought, with a simple DSL, doesn't do all this
       | justice (just like those friendly before/after callbacks can turn
       | into massive spaghetti abominations).
       | 
       | The way I do it is that any state transition more complex than
       | checking two attributes and updating a third one is an object.
       | Transitioning a Campaign object from :reservation to :order?
       | That's a new PORO called CampaignOrder, with a #perform! public
       | method or something. All the conditions, assumptions and side
       | effects have their private methods (check_showings!,
       | update_occupancy!). Passed a dirty record into something
       | delicate? That's a raise, I want something clean and valid to
       | work with. Is this something where we expect a dirty record? Go
       | right ahead, we'll save it later in the transition, plus it will
       | be documented right in the constructor. Does it need to raise? It
       | will. Custom error messages on any record? Right here in this
       | private method. Does any model need to know anything about the
       | state transition or own any related code? Nope.
        
       | jack_riminton wrote:
       | Nice article. Are state machines used widely in Rails apps?
        
         | beckingz wrote:
         | Unfortunately
        
         | rubyist5eva wrote:
         | In my experience, state-machines are widely used in all
         | business applications whether they use a "library" for it or
         | not.
        
         | reillyse wrote:
         | Yes
        
       | rowan_mcd wrote:
        
       | jeremycw wrote:
       | I've seen a few large Rails codebases that included a state
       | machine library like mentioned in the article. Every single one
       | was worse because of it. It pushes the code down a path where
       | hidden hooks run when magic methods are called. At this point in
       | my career I'm just done with that type of "clever" code.
       | 
       | The article starts with examples that just define the state
       | machine by hand. This is a much better approach and scales to
       | larger code much better. You can grep "def start!" and just read
       | what the method does. A state machine DSL is really not providing
       | much value and eventually just gets in the way.
        
         | tomc1985 wrote:
         | Funny, I have spent a ton of time around Rails code where I
         | dearly wished we had a state machine, because the alternative
         | was an unorganized cluster of home-grown "state transition"
         | glue without any consistent way of handling it, with all the
         | weird-ass edge cases and split-brain BS that come with it.
        
           | powersurge360 wrote:
           | I've been on the receiving and perpetuating end of state
           | machines and it's painful to have to deal with one that
           | started years before you got involved and easy to think that
           | you can do better. I've got a modest one in a side project
           | and due to it being a side project I touch it once every
           | couple of weeks and already it is making me uncomfortable.
           | And it's dirt simple and I wrote it!
           | 
           | There's a take on state machines in a railsconf talk further
           | down in the thread and it seems like an awesome way to reap a
           | lot of the benefits while keeping a grip on the complexity.
           | Sometimes complexity can't be buried safely and you kind of
           | need to frame it instead. I'd argue that state machines bury
           | that complexity but breaking those states into ruby objects
           | w/ their own validation frames and delineates them instead.
        
           | a4isms wrote:
           | The quip I like best for this is: _"Any sufficiently
           | complicated model class contains an ad-hoc, informally-
           | specified, bug-ridden, slow implementation of half of a state
           | machine."_
           | 
           | Lots of models I've encountered eschew being organized as
           | state machines in favour of having "if salad" strewn
           | throughout their code. That being said, refactoring to a
           | simple state machine and trying to maintain it as such in
           | perpetuity isn't always the correct solution.
           | 
           | Sometimes, a hierarchal state machine is needed, and if it is
           | expressed as a simple state machine, it's just as messy.
           | 
           | Sometimes, a portion of it needs to be a state machine, and
           | the right thing to do is to delegate some of the methods to a
           | strategy that reflects the current state, but not all of
           | them.
           | 
           | Sometimes, the whole thing is just too fat, and a state
           | machine won't save it, the right thing to do is to get very
           | aggressive about refactoring to a composite object.
           | 
           | Any time you have a big, messy model, it's very easy to write
           | a blog post espousing a single solution, like this one:
           | 
           | http://raganwald.com/2018/02/23/forde.html
           | 
           | HN discussion: https://news.ycombinator.com/item?id=16468280
           | 
           | But the reality is that a big, messy model is always going to
           | be some kind of problem, and unless you can break it down
           | into parts, you're going to have a problem. A state machine
           | is conceptually a way to break a big thing into parts based
           | on its "state," but that's just one approach to factoring.
           | 
           | p.s. Another problem is that even if a simple state machine
           | is the right answer, "rolling your own" usually isn't. Grab a
           | well-tested and documented library already. This isn't your
           | passion project, this is industrial programming. Rolling your
           | own is one of the best ways to learn how state machines work.
           | Once you've learned how, reach for a professional tool.
        
       | knodi123 wrote:
       | Counterpoint: State machines in Ruby should be used sparingly,
       | and only when necessary, because they start out quick and easy,
       | but almost always grow into things that are big, ugly, and
       | difficult to refactor or remove.
       | 
       | Here are somebody's notes from a Railsconf talk that made this
       | point, plus a link to the video:
       | 
       | https://gist.github.com/benoittgt/05521e272e13c7b7c9c89eb4a9...
       | 
       | But the short version is the takeaway quote:
       | 
       | > State machines are magnetic : they attract more states,
       | transitions, events, callbacks and they accumulate callback logic
       | where our bugs hide
        
         | systems_glitch wrote:
         | Indeed, a coworker at a previous job was fascinated/fixated
         | with state machine patterns. After spending two weeks trying to
         | force a process we were paired on into a state machine pattern
         | and failing, I rewrote it in an afternoon without the state
         | machine stuff.
         | 
         | It started out small and easy, when there were two states. A
         | third state was manageable but to me already looked like it was
         | going the wrong direction. The fourth state (out of IIRC 6) was
         | never finished.
        
         | iasay wrote:
         | Of all the things I've seen in my time writing software I can
         | qualify that some of the most fucked up things involved state
         | machines. The worst of which was a 200 state hand written one
         | in C for an embedded measurement system. There was no
         | documentation, no comments and the states were numbered rather
         | than named. The guy who wrote it originally was "anti RTOS" and
         | had died after riding his motorbike recklessly leaving this
         | shit for everyone to deal with.
         | 
         | It took me two weeks to do a trivial change because I had to
         | reverse engineer a ton of it. And the only debugger I had was a
         | couple of LEDs on the board I could wiggle on and off.
        
           | contingencies wrote:
           | Sounds like a confused author or potentially an
           | implementation-driven architectural decision (eg. regulatory
           | or policy requirement, direct map from exposed states of a
           | prior implementation that had been black box reverse
           | engineered, etc.). AFAIK the normal way to implement state
           | machines in embedded since ages yore is to use interlocking
           | networks of state machines per isolate subsystem. At least,
           | that's what I do and that's what seems natural in C and
           | assembly where concerns such as manual memory management,
           | code re-use and programmer cognitive load would all tend
           | toward such an architecture. What was the hardware?
        
             | iasay wrote:
             | Was Coldfire + IAR. The design was driven entirely by
             | idiocy and showboating. I was hired just to limp it along a
             | bit further while they spun a new board and software as the
             | MCU was obsolete anyway.
        
           | languageserver wrote:
           | Send this by mail to thedailywtf, would love to read the
           | expanded version
        
         | powersurge360 wrote:
         | Thanks for sharing this. For other people who are hovering over
         | this and unsure if they want to watch a 40 minute video, it's
         | time stamped and the two parts I found super valuable were the
         | 'Anti-Pattern: Custom Methods' and the 'Anti-Pattern: State
         | Machines' sections. The TL;DR there is it's alright to create a
         | controller that manipulates a single field on a single model.
         | If it is enough of a resource to expose it by a custom url
         | (think update password) it is enough of a resource to get its
         | own controller.
         | 
         | And going beyond that, to the state machine, if each state (or
         | really, value of a particular field) is important enough to
         | have it's own contextual validation, transition points, etc,
         | then it's important enough to expose each state as a resource
         | with it's own controller.
         | 
         | Super cool stuff and I've got some thinking to do about some
         | side projects now.
        
       ___________________________________________________________________
       (page generated 2022-06-23 23:01 UTC)