[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)