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