How does one typically handle complex program flow?
I seem to be running into a weak spot in my C# journey - that being project management and workflow design.
Embarassingly, I'm in a bit of a head scratcher as to how to handle a moderately-complex flow of an application. I thought this would be a cakewalk - I've dealt with a lot of nitty gritty problems with C#, how hard can simple control flow be?
Apparently, it has hands.
I figured maybe a state machine could help me keep track of the program, but those seem less 'self-running' than maybe I understand, or require in any case. I essentially need a way to say "Okay program, you're at this point so you have XYZ capabilities right now" with those available-to-perform actions changing as the state of the program morphs. And the state of the program should be able to morph by itself at some points - e.g. connecting to server automatically transitions to running the script (or exits), which then transitions to a prompt
Doing this with a wackload of ifs and switches isn't necessarily impossibly complex, but it's just enough on the edge that I don't want to do that here. Especially if I want this to be scalable (e.g. Adding more conditions)
What do you fellas usually use for something like what the picture shows? (Avoid suggesting just biting the bullet and using tons of ifs/switches, I want to learn something new here)
23 Replies
For modelling state machines, you can roll your own with enums/etc, or use something like Stateless.
there are a few ways you can handle this, it depends what the single operations enties to, so for example if it requires user input or they are just programmatical
you could linearize a local ifs by doing a nop
or let's put it in another way
there are it seems 3 main states: key, file, runner
and every operation that touches that state there's an if
it could be a start to isolate those
However, that flow chart looks like most of that flow is synchronous? As in, there are only a couple of states that you'll hang around in waiting for something external to happen
So I think most of that will just boil down to normal program flow, with only one or two wait states
yeah it's a sort of sequence with optional end (cancellation) and loop
and apparently no recovery
it's mostly straightforward (except for the loop going back in the middle, not at the beginning)
Unknown User•3mo ago
Message Not Public
Sign In & Join Server To View
That's just something like Stateless, but with the addition of a message bus
Structuring it such that the server connects first might reduce the awkwardness of needing to traverse the "Is connected?" stage again if the user simply just wants to run another script.
When the lack of recovery is mentioned, does that mean the connection failure simply terminating the program without a "Retry?" case?
Unknown User•3mo ago
Message Not Public
Sign In & Join Server To View
I appreciate this btw
Unknown User•3mo ago
Message Not Public
Sign In & Join Server To View
That said, modelling things as states is normally fairly pointless if you're just going to be synchronously triggering the events from your state entry handlers. Normally state machines have value if the events come from somewhere else
Unknown User•3mo ago
Message Not Public
Sign In & Join Server To View
If you go full reductionist, every statement is its own state, and the "event" is "I finished the previous statement". Obviously that's fairly pointless, which means there's a line somewhere between what's worth breaking out into a state, and what isn't
A few of the states are linear A -> B -> C states, but at some point the program should loop back on itself with conditions that determine the next state.
Canton's way seems like a perfectly valid way to handle this for specifically this type of program, but I guess I'd say I definitely want something I can plug more states into to add more complex flow down the line if need be
My closest approximation thus far is kind of mimicking stateless's program structure such that I can make states, but each state is a function that conditionally returns the next state to transition to.
The problem with that being skipping states if certain conditions are met (e.g. not looping back to "Is Connected?" for example if the connection was already made)
So it becomes more of a "workflow machine" I guess
Still, I appreciate the helpful responses thus far.
Yeah, that's another way to do it
You think that might be a good method to the madness?
I was kind of second-guessing it and thinking maybe it was dumb and I'd have like a dozen C# experts saying "NOOOO" when they laid eyes upon it - I'm trying to do it a proper way and not hack/slash this time around, so maybe I'm getting a little paranoid about the "proper-ness" of my code - but if that's a valid pattern that people use I might see about exploring it
I've done it before
Not in C# that much, but I've done it a bunch in embedded C, when you deal with states in this way a lot more
In higher-level languages it's also common to have a bunch of subclasses of some abstract
State
class, all overriding an Entry
/etc virtual method, and your states are represented by these subclasses rather than enums
As with anything state-machiny like this, the tradeoff with breakout up your execution flow like this is that it becomes harder to track which states you've been through, and therefore which prerequisite bits of state (such as a loaded script) have been set at what point. With a bit of code in a method, definite assignment helps: you can't use your script
variable before you've assigned to it, but you lose that when chop things up into multiple methods which all rely on shared state@Cyro you mentioned a state machine, are you avoiding OOP compositional approaches for some reason?
For example, within the .NET base classes, it would be typical for a key provider, a file provider, a script runner, and a remote connection to all be modeled as separate classes that would each maintain their own state and logic related to operations.
You don't have to go to such lengths if you don't need to, but over time separating responsibilities into (somewhat) narrowly named classes can make it easier to find and modify code.
In a larger commerce system I worked on in the past, we had sales orders that when processed had hundreds of conditions and state changes. We eventually had a whole namespace filled with classes that would process various aspects of the order and validate business logic.
If we tried to build a system like that using only a single state machine, it would have become difficult or impossible to maintain after the first couple months of development.
Not strictly, I suppose I purpose my classes differently. My remote connection handler already has some internal logic, I just wanted to use a more robust workflow management system than if/else blocks all over, checking if things are disposed, if this step failed so now jump to this step but only if xyz condition is met, etc.
the thing about workflows is that they are flexible but harder to comprehend
the more you can restrict the degrees of freedom the easier it will be to write (and read)
Thanks for the insights guys. I managed to make a sort of 'workflow machine' like I mentioned. It ended up being pretty simple in the end. Just a defined 'start' and 'end' state, and the machine's states are just functions that return the next state. I just define each states' Next() function and it runs itself quite handily. I just have to readily handle exceptions so as to not jam it up.
Tangentially, I found that C#'s generic enums are surprisingly hard to compare, but some MVP on stackoverflow came up with a function that gets hardcore inlined into like 3 instructions due to how generic typing works. I extended the concept which I think makes this whole thing run in a fairly performant manner.