got to be a senior engineer this week, leaving comments like "we should be storing the information, not the decision into the database"

it was about a flag to suppress alarms, which seems fine, but you end up not knowing why the flag was set, or unset, and bad things happen
in other words, you want to model state, not properties of the state

e.g. a task might be enqueued, running, failed, or complete, and you're better off storing that in a variable called 'state' over a set of flags like 'is_running'
in this instance, a flag like "should_alarm?" for a machine should be calculated from state like "installing" or "operational"

if you store the flag, and not the state, you'll never know if it's really safe to clear, or just one of the reasons no longer applies
the other footgun with modelling state is the powerset—some things have two distinct sets of state, two different lifecycles, and you're better off having two variables like "lifecycle" and "status"

over trying to encode every possible combination into one set of states
this sort of problem (encoding two distinct sets of state into one variable) happens all the time in game code

& then you get weird things like pushing two directions at once gives you a speed boost

it's funny in games but in more boring software it just causes nightmares
if this seems overly complicated, there's a simpler way to think about it—imagine the client code

`if state == RUNNING` versus `if is_running and !is_error and !is_complete`

or, the problem with booleans for state is that you write the most awful chains of logic to decode them
this sort of thing comes up every 9 months or so, and it feels like a small detail

but getting things right in your database schema early on can save months of refactoring, as well as prevent some horrible bugs from raising their heads
i guess the tl;dr is "don't use one thing for two purposes" and "don't split one purpose across two things"

there's similar arguments on "your key should be opaque and you should use a separate timestamp" but i am too tired to be mad about uuids today
one last example: for gamedev, if you split your code into 'deciding what to do' & 'doing it', life is easier

func onEvent(e) {
var state = {...}
var action = WAIT
if e == JUMP && state == FALLING {
action = FALL
}
...
if action = FALL {
nextState = {x=state.x + ....}
}
}
ok i lied about one last example: the single responsibility principle isn't the same thing, that's "a piece of code should do one thing only"

which i don't entirely agree with. you end up with things like "git checkout" creating new branches cos the code looks the same
it's more "a thing should be done in one place" rather than "one place should only do one thing"

because sometimes two things are very wrapped up together & disconnecting them is a mess

or sometimes you end up encoding properties of data "is_running?" & not the data "taskstate"
you shouldn't be asking yourself "is this readable/clean" but "am i less likely to write bugs if i do it this way" because the former leads to sweeping details under the rug & the latter hopefully leads to asking "how can this go wrong" and dealing with it
the single responsibility principle is a good idea in the small (a variable should encode one distinct piece of state) but in the large it ends up being "i'll throw another boolean argument onto this function so i don't have to duplicate these four lines of code"
at some point programmers will discover that the point of an abstraction is to share details, not hide them, to make changes in one place

in other words: tight coupling

and that loose coupling means "a little redundancy so that you can change things without affecting the whole"
programming is littered with nuance free adages demonstrated on four line text book examples, paving a path to hell

you should be asking "does this make it easier to maintain, or make it harder to write bugs"

and not "is this thing doing just one thing" and hoping for the best
you should be aiming to write code that reduces cognitive overhead

making it so you can work on local changes without total knowledge of the codebase

or making global changes without having to dig into every module

it's a balancing act, too
if you're still wanting a rule-of-thumb, the best i've got is

"things that get changed at the same time should be close together, in the same file/module"

rather than lumping things together because the code looks similar or does similar things
the other rule of thumb? "it's sometimes easier to write two libraries, one wrapping the other, than to do everything in the same place."

eg one library handles parsing & the library atop handles workflow or state

or, layer your code when details will be changed independently
once you have this idea of 'layering your code', you can bring back the 'don't repeat yourself' with a bit of nuance: don't repeat yourself in the same layer

because you'll need to repeat some details if you want to change them independently
layering policy/workflow code over mechanism is one example, but layering frameworks over components is another

which is a bit of waffle words to mean "frameworks are for your cross-cutting changes in your codebase, a thin layer of glue to hold the rest together, consistently"
in the end, most of this advice only helps when you have agency over your codebase

the reality is, most of the architectural work happens long before you get to maintain it & making structural changes, process changes are the hardest to make

it takes people out of comfort zone
even so, you don't have to write good code, but you can write code that isn't painful to refactor in the right direction & that's often the best you can hope for

you might not make things good but you can make things suck less for your replacement
see also: comments

comments are an easy way to lower cognitive overhead. not by explaining what the code does, but by explaining how it got that way.

elucidating the rational, decisions and constraints that are in play but not evident in the codebase
anyway, this is all stuff i learned early on in programming, but it was only much later that i gained the vocabulary to talk about it

you win a lot more arguments with big sounding words like 'cognitive overhead' than you do by going "this will be hard to maintain" sadly
like "things that get changed independently shouldn't be lumped together" seems obvious enough

but saying "In David Parnas' On the criteria to be used in decomposing systems into modules..." will make you sound smart and get people off your back a lot quicker
i dunno where i am going with this but it feels like i should say "all bugs are structural, and all engineering is social"

or, you shouldn't blame people for writing bugs, and you can't fix bugs without convincing other people they exist, first
at the end of the day, writing code isn't about doing the right thing, or the simplest thing, but trying your best not to paint yourself into a corner

or in other words, just trying your best to make things suck a little less for future you. be it code, documentation, or process
You can follow @tef_ebooks.
Tip: mention @twtextapp on a Twitter thread with the keyword “unroll” to get a link to it.

Latest Threads Unrolled: