I think most programmers agree that simpler solutions (generally matching "lower cognitive load") are preferred, but the disagreements start about which ones are simpler: often a lower cognitive load comes with approaches one is more used to, or familiar with; when the mental models one has match those in the code.
For instance, the article itself suggests to use early/premature returns, while they are sometimes compared to "goto", making the control flow less obvious/predictable (as paxcoder mentioned here). Intermediate variables, just as small functions, can easily complicate reading of the code (in the example from the article, one would have to look up what "isSecure" means, while "(condition4 && !condition5)" would have shown it at once, and an "is secure" comment could be used to assist skimming). As for HTTP codes, those are standardized and not dependent on the content, unlike custom JSON codes: most developers working with HTTP would recognize those without additional documentation. And it goes on and on: people view different things as good practices and being simpler, depending (at least in part) on their backgrounds. If one considers simplicity, perhaps it is best to also consider it as subjective, taking into account to whom it is supposed to look simple. I think sometimes we try to view "simple" as something more objective than "easy", but unless it is actually measured with something like Kolmogorov complexity, the objectivity does not seem to be there.
> one would have to look up what "isSecure" means, while "(condition4 && !condition5)" would have shown it at once
You would feel the need to look up a variable called isSecure, but would not need to look up condition4 or condition5? I think the point TFA was making is that one could read isSecure and assume what kind of implementation to expect, whereas with condition4 I wouldn't even know what to look for, or I'd even struggle to hold any assumption.
/* this one needs to make sense in the end */
isSecure = user.role == 'admin'
/* these two do not */
condition4 = user.id <= 4
condition5 = session.licenseId == 5
> and an "is secure" comment could be used to assist skimming
Those are exactly the kind of comments I'd rather see written out as intermediate variables. Such comments are not explaining to you any of the Why?s anyway, and I also tend to trust the executing code much more than any non-type-related annotating code, as comments are rarely helpful and sometimes even describe wishful thinking by straight-up lying.
> You would feel the need to look up a variable called isSecure, but would not need to look up condition4 or condition5?
I assume that those "conditions" are placeholders, not to be read literally in the example (since the example is not about poorly named variables, but about complex conditions), so I did not mean them literally, either. Supposedly those would be more informative names, such as "channel_encrypted", "checksum_verified".
> [...] describe wishful thinking by straight-up lying
This was what I had in mind upon seeing that "isSecure" bit, too: could easily be a lie (or understood differently by different people). But taking a little more effort to check then, and/or having to remember what those variables actually mean. It is a commonly debatable topic though, where the good balance is, similarly to splitting code into small functions: people tend to balance between spaghetti code and extreme splitting.
My point though is not to argue with those particular points here, but that we have no such practices/rules universally considered simple and formally stated/verifiable.
I would rather name intermediate variables to match the statement rather than some possible intent, it's basically a form of semantic compression. For example isAdminUser = user.role == 'admin' - here we are hiding away the use of roles which is not relevant for the conditional, but isSecure can mean anything, we don't want to hide the concept of being an admin user, just the details of using roles to determine that. At least that's my take.
Fast response from JBEE SPY TEAM 10/10 accuracy, finally I can see my spouse phone now I got all prove i needed in court I recommend JBEE SPY TEAM on instagram you can still reach them on telegram +44 7456 058620, Email conleyjbeespy606@gmail.com
Cognitive load depends on the mental models one has, yes.
> The problem is that familiarity is not the same as simplicity. They feel the same — that same ease of moving through a space without much mental effort — but for very different reasons. Every “clever” (read: “self-indulgent”) and non-idiomatic trick you use incurs a learning penalty for everyone else. Once they have done that learning, then they will find working with the code less difficult. So it is hard to recognise how to simplify code that you are already familiar with. This is why I try to get “the new kid” to critique the code before they get too institutionalised!
This was explored in the latter part of the article.
> programmers agree that simpler solutions...are preferred, but the disagreements start about which ones are simpler
Low ego wins.
1. Given: The quality of a codebase as a whole is greatly affected by its level of consistency + cohesiveness
2. Therefore: The best codebases are created by groups that either (1) internally have similar taste or (2) are comprised of low ego people willing to bend their will to the established conventions of the codebase.
Obviously, this comes with caveats. (Objectively bad patterns do exist.) But in general:
Low-ego
→ Following existing conventions
→ They become familiar
→ They seem simpler
I don't think this necessarily is accurate I've come into a lot of projects that no one understands well, but everyone continues to follow the same bad conventions that already exist which just adds to the problems. Ex: deep nesting, no early exit, deep object inheritance.. this happens because a lot of developers don't want to rock the boat, AND because they don't have the skills to unwind the complexity in a manageable amount of time without also causing serious problems.
Unfortunately there is no one definition of simple, because developers call simple "whatever they are used to" rather than an objective measure such as "the least branching" or "the fewer lines of code".
The best I can come up with is "code that causes the least amount of scrolling" which is hard to measure and may be mitigated with IDE help.
Likewise, some people prefer ternary statements for short checks; I want to agree because ternaries are one of the first things you learn after if/else/while/for, but at the same time... they're a shorthand, and shorthand is short but not necessarily more readable.
For one-off things like value = condition ? a : b I don't mind much, but I will make an issue as soon as it spans more than one line or if it's nested.
I prefer it as long as there’s no side effects. You get tighter semantics which I think helps readability (and I trust compilers to be able to handle it optimally). I find the following format to be very nice:
I particularly don't like ternaries with side-effects or control flow. In particular with control flow I prefer it always tabbed in otherwise sometimes I miss it -- if statements are much better for this.
Sometimes an established pattern is easier to understand than the improved version. In that case convention is better, for example comparing http codes directly instead of giving them names, since those are easy to read for anyone who's ever done web dev.
A lack of nuance about this kind of thing is part of what enrages me when ChatGPT tries to tell be a planned change or design is going to be “elegant” or “simple”. It’s like… maybe yes, maybe no, but those are not binary terms and throwing them around like that makes it sound like an enthusiastic intern sucking up to his sensei rather than a digital brain whose thoughts are formed by having ingested billions of lines of real life code.
your rage suggests that you think ChatGPT is making value judgements about elegance or simplicity. You surely know where it picked up those concepts don't you?
> For instance, the article itself suggests to use early/premature returns
I like premature returns and think they reduce complexity, but as exclipy writes (I think quoting Ousterhout)
'complexity is defined as "how difficult is it to make changes to it"'.
If premature returns are the only premature exit your language has then they add complexity in that you can't then add code (in just one place) that is always executed before returning.
A good language will also have "break" from any block of code, such that the break can also carry a return value, AND the break can be from any number of nested blocks, which would generally mean that blocks can be labelled / named. And also of course that any block can have a return value.
So you don't actually need a distinguished "return" but only a "break" that can be from the main block of the function.
A nice way to do this is the "exit function", especially if the exit function is a first class value and can also exit from called functions. (of course they need to be in a nested scope or have the exit function passed to them somehow).
It is also nice to allow each block to have a "cleanup" section, so that adding an action to happen on every exit doesn't require wrapping the block in another block, but this is just a convenience, not a necessity.
Note that this is quite different to exception handling try / catch / finally (Java terms) though it can be used to implement exception handling.
> A good language will also have "break" from any block of code, such that the break can also carry a return value, AND the break can be from any number of nested blocks, which would generally mean that blocks can be labelled / named. And also of course that any block can have a return value.
Even in a language that is not "good" by your definition... you have basically just described a function. A wrapper function around a sub-function that has early returns does everything you want. I use this pattern in C all of the time.
Personally I wouldn't agree with this. I've adopted a pattern where I try to only ever return the success value at the end of a function. Early returns of success value don't feel clear to me and make the code hard to read. I think that sort of code should only be used if you need high performance. But for clarity, it hurts.
Instead I think you should generally only use early returns for errors or a null result, then they're fine. Ditto if you're doing a result pattern, and return a result object, as long as the early return is not the success result (return error or validation errors or whatever with the result object).
So I feel code like this is confusing:
function CalculateStep(value) {
if(!value) return //fine
///a bunch of code
//this early return is bad
if(value < 200) {
//a bunch more code
return [ step1 ]
}
///a bunch more code
return [ ..steps ]
}
The early return is easy to miss when scanning the code. This is much less confusing:
function CalculateStep(value) {
if(!value) return //fine
///a bunch of code
let stepsResult : Step[]
if(value < 200) {
//a bunch more code
stepsResult = [ step1 ]
} else {
//a bunch more code
stepsResult = [ ..steps ]
}
//In statically typed languages the compiler will spot this and it's an unnecessary check
if(!stepsResult) throw error
//cleanup code here
return stepsResult
}
It makes the control flow much more obvious to me. Also, in the 2nd pattern, you can always have your cleanup code after the control block.
This was my main takeaway from A Philosophy Of Software Design by John Ousterhout. It is the best book on this subject and I recommend it to every software developer.
Basically, you should aim to minimise complexity in software design, but importantly, complexity is defined as "how difficult is it to make changes to it". "How difficult" is largely determined by the amount of cognitive load necessary to understand it.
The problem is no set of rules can replace taste, judgement, experience and intuition. Every rule can be used to argue anything.
You can't win architecture arguments.
I like the article but the people who need it won't understand it and the people who don't need it already know this. As we say, it's not a technical problem, it's always a people and culture problem. Architecture just follows people and culture. If you have Rob Pike and Google you'll get Go. You can't read some book and make Go. (whether you like it or not is a different question).
The approach that I am trialing with my team now, so far to good results, is as follows.
* Our coding standards require that functions have a fairly low cyclomatic complexity. The goal is to ensure that we never have a a function which is really hard to understand.
* We also require a properly descriptive header comment for each function and one of the main emphases in our code reviews is to evaluate the legibility and sensibility of each function signature very carefully. My thinking is the comment sort of describes "developer's intent" whereas the naming of everything in the signature should give you a strong indication of what the function really does.
Now is this going to buy you good architecture for free, of course not.
But what it does seem to do is keep the cognitive load manageable, pretty much all of the time these rules are followed. Understanding a particular bit of the codebase means reading one simple function, and perhaps 1-2 that are related to it.
Granted we are building websites and web applications which are at most medium fancy, not solving NASA problems, but I can say from working with certain parts of the codebase before and after these standards, it's like night and day.
One "sin" this set of rules encourages is that when the logic is unavoidably complex, people are forced to write a function which calls several other functions that are not used anywhere else; it's basically do_thing_a(); do_thing_b(); do_thing_c();. I actually find this to be great because it's easy to notice and tells us what parts of the code are sufficiently complex or awkward as to merit more careful review. Plus, I don't really care that people will say "that's not the right purpose for functions," the reality is that with proper signatures it reads like an easy "cliffs notes" in fairly plain English of exactly what's about to happen, making the code even easier to understand.
> I like the article but the people who need it won't understand it
That's true. One doesn't change his mindset just after reading. Even after some mentorship the results are far from satisfying. Engineers can completely agree with you on the topic, only to go and do just the opposite.
It seems like the hardest thing to do is to build a feedback loop - "what decisions I made in past -> what it led to". Usually that loop takes a few years to complete, and most people forget that their architecture decisions led to a disaster. Or they just disassociate themselves.
I feel this in my soul. But I'm starting to understand this and accept it. Acceptance seem to lessen my frustration on discussing with architects that seemingly always take the opposite stance to me. There is no right or wrong, just always different trade offs depending on what rule or constraint you are prioritizing in your mind.
Which is why I consider DRY (Don't Repeat Yourself) to be an anti-rule until an application is fairly well understood and multiple versions exist. DO repeat yourself, and do not create some smart version of what you think the problem is before you're attempting the 3rd version. Version 1 is how you figure out the problem space, version 2 is how you figure out your solution as a maintainable dynamic thing within a changing tech landscape, and version 3 is when DRY is look at for the first time for that application.
DRY isn't about not reimplementing things; it's about not literally copying and pasting code. Which I have seen all the time, and which some might find easier now but will definitely make the system harder to change (correctly) at some point later on.
I think it's a pretty good compromise. I have tried in the past not to duplicate code at all, and it often ends up more pain than gain. Allow copy/paste if code is needed in two different places, but refactor if needed in three or more, is a pretty good rule of thumb.
DRY means something completely different. It means that there should be just one source of truth.
Example: you have a config defined as Java/Go classes/structures. You want to check that the config file has the correct syntax. Non-DRY strategy is to describe its structure in an XSD schema (ok, ok JSON schema) and then validate the config. So you end up with two sources of truth: the schema and Java/Go classes, they can drift apart and cause problems.
The DRY way is to generate the classes/structures that define the config from that schema.
Some people use a gardening metaphor for code, and I think that since code is from and for humans, that’s not a terrible analogy. It’s organic by origin if not by nature.
When you’re dealing with perennial plants, there’s only so much control you actually have, and there’s a list of things you know you have to do with them but you cannot do them all at once. There is what you need to do now, what you need to do next year, and a theory of what you’ll do over the next five years. And two years into any five year plan, the five year plan has completely changed. You’re hedging your bets.
Traditional Formal English and French gardens try to “master” the plants. Force them to behave to an exacting standard. It’s only possible with both a high degree of skill and a vast pool of labor. They aren’t really about nature, or food. They’re displays of opulence. They are conspicuous consumption. They are keeping up with the Joneses. Some people love that about them. More practical people see it as pretentious bullshit.
I think we all know a few companies that make a bad idea work by sheer force of will and overwhelming resources.
I think more than a few people have recommended waiting until the 3rd or 4th X before you say OK, Don't Repeat Yourself we need to factor this out. That's where my rule of thumb is too.
Deliberately going earlier makes sense if experience teaches you there will be 3+ of this eventually, but the point where I'm going to pick "Decline" and write that you need to fix this first is when I see you've repeated something 4-5 times, that's too many, we have machines to do repetition for us, have the machine do it.
An EnableEditor function? OK, meaningful name. EnablePublisher? Hmm, yes I understand the name scheme but I get a bad feeling. EnableCoAuthor? Approved with a stern note to reconsider, are we really never adding more of these, is there really some reason you can't factor this out? EnableAuditor. No. Stop, this function is named Enable and it takes a Role, do not copy-paste and change the names.
It’s a pain in the ass to source a copy of this book without giving Jeff Bezos all the money. If anyone reading this thread knows John, could you bring this to his attention?
I even tried calling the bookstore on his campus and they said try back at the beginning of a semester, they didn’t have any copies.
My local book store could not source me a copy, and neither IIRC could Powell’s.
That sucks. Ordinarily although a weird volume there's no demand for won't be fast a bookshop should be able to get anything in print. Is there some reason it's specific to this book do you think?
That's best book on the topic! The article was inspired by this exact book. And John is a very good person, we discussed a thing or two about the article.
I've long given up on trying to find the perfect solution for Software. I don't think anyone has really "cracked the code" per se. The best we have is people's wisdom and experiences.
Ultimately, context, industries and teams vary so greatly that it doesn't make sense to quantify it.
What I've settled on instead is aiming for a balance between "mess" and "beauty" in my design. The hardest thing for me personally to grasp was that businesses are indeterministic whereas software is not, thus requirements always shifts and fitting this into the rigidity of computer systems is _difficult_.
These days, I only attempt to refactor when I start to feel the pain when I'm about to change the code.. and even then, I perform the bare minimum to clean up the code. Eventually multiple refactoring shapes a new pattern which can be pulled into an abstraction.
There is no objective "perfect", because the "perfect" is in the eyes of the reader.
Also, people confuse familiar with simple, they tend to find things simple if they are familiar, even if they are complex (interwine a lot of different things).
I'm struggling with the amount of complexity. As an inexperienced SWE, I found it difficult to put everything into my head when the # of function calls (A) + # of source code files (B) to navigate reach N. In particular, if B >= 3 or A >= 3 -- because, B equals the number of screens I need to view all source code files without Command+Tab/Alt+Tab, and cognitive load increases when A increases, especially when some "patterns" are involved.
But I'm not experienced enough to tell, whether it is my inexperience that causes the difficulty, or it is indeed that the unnecessary complexity tha causes it.
Humans have a very limited amount of working memory. 3-5 items on average. A savant might be at something like 12. It is trivially easy to blow that with code. OO with code inheritance is a prime example of combinatorial explosion that can lead to more possibilities than atoms in the universe, let alone one persons ability to reason.
You should not need to read every line of code in every file and function to understand what’s going on to the level you need to solve a particular problem. You must make a decision to NOT look deeper at some point on any non- trivial code base. A good program with good names and comments in the appropriate places is what allows you to do exactly that more easily . When you see sort(usernames) in the middle of a function do you need to dive into sort to be able to understand the code in that function?? Probably not, unless you are fixing a bug in how usernames are sorted!
With that said , get good at jumping into definitions, finding all implementations, then jumping back where you were. With a good IDE you can do that at the speed of thought (in IntelliJ that’s Cmb+b, Cmd+Alt+b, Cmd+[ on Mac). I only open more than one file at the same time when comparing them. Otherwise it’s much easier to jump around back and forth (you can even open another function inline if you just want to take a Quick Look, it’s Alt+Space). Don’t use the mouse to do that, things you do all the time can be made an order of magnitude faster via shortcuts. Too many developers I see struggle with that and are embarrassingly slow moving around the code base!
Experience helps to recognize intent sooner. That reduces cognitive load. Getting lost 5 levels deep seemingly never stops being a thing, not just you.
Only half joking: I don’t think I trust a book from an author who has inflicted decades of TCL pain on me (and on the entire community of EDA tool users.)
I know you're only half joking, but I don't think you can pin the blame on John or TCL. Osterhaut's thesis, as I recall, was that there is real benefit to having multiple programming languages working at different levels of the domain (e.g. a scriptable system with the core written in a lower level language). Of course now this is a widespread practice in many domains (e.g. web browsers, numerical computing: matlab, numpy). It's an idea that has stood the test of time. TCL is just one way of achieving that aim, but at the time it was one of few open-source options available. I think scheme/lisp would have been the obvious alternative. AutoDesk went in that direction.
I remember using TCL in the 90s for my own projects as an embeddable command language. The main selling point was that it was a relatively widely understood scripting language with an easily embeddable off-the-shelf open source code base, perhaps one of the first of its kind (ignoring lisps.) Of course the limitations soon became clear. Only a few years later I had high hopes that Python would become a successor, but it went in a different direction and became significantly more difficult to embed in other applications than was TCL -- it just wasn't a primary use case for the core Python project. The modern TCL-equivalent is Lua, definitely a step up from TCL, but I think if EDA tools used Lua there would be plenty of hand-wringing too.
Just guessing, but I imagine that at the time TCL was adopted within EDA tools there were few alternatives. And once TCL was established it was going to be very hard to replace. Even if you ignore inertia at the EDA vendors, I can't imagine hardware engineers (or anyone with a job to do) wanting to switch languages every two to five years like some developers seem happy to do. It's a hard sell all around.
I reckon the best you can do is blame the vendors for (a) not choosing a more fit-for purpose language at the outset, which probably means Scheme, or inventing their own, (b) or not ripping the bandaid off at some point and switching to a more fit-for-purpose language. Blaming (b) is tough though, even today selecting an embedded application language is vexed: you want something that has good affordances as a language, is widely used and documented, easily embedded, and long-term stable. Almost everything I can think of fails the long term stability test (Python, JavaScript, even Lua which does not maintain backward compatibility between releases).
Unsurprisingly, minimising the amount of cognitive complexity is how you get the most out of LLM coding agents. So now have a theoretically repeatable way to measure cognitive load as contextualised to software engineering.
I'm probably one of the "smart developers" with quirks. I try to build abstractions.
I'm both bothered and intrigued by the industry returning to, what I call, "pile-of-if-statements architecture". It's really easy to think it's simple, and it's really easy to think you understand, and it's really easy to close your assigned Jira tickets; so I understand why people like it.
People get assigned a task, they look around and find a few places they think are related, then add some if-statements to the pile. Then they test; if the tests fail they add a few more if-statements. Eventually they send it to QA; if QA finds a problem, another quick if-statement will solve the problem. It's released to production, and it works for a high enough percentage of cases that the failure cases don't come to your attention. There's approximately 0% chance the code is actually correct. You just add if-statements until you asymptotically approach correctness. If you accidentally leak the personal data of millions of people, you wont be held responsible, and the cognitive load is always low.
But the thing is... I'm not sure there's a better alternative.
You can create a fancy abstraction and use a fancy architecture, but I'm not sure this actually increases the odds of the code being correct.
Especially in corporate environments--you cannot build a beautiful abstraction in most corporate environments because the owners of the business logic do not treat the business logic with enough care.
"A single order ships to a single address, keep it simple, build it, oh actually, a salesman promised a big customer, so now we need to make it so a single order can ship to multiple addresses"--you've heard something like this before, haven't you?
You can't build careful bug-free abstractions in corporate environments.
So, is pile-of-if-statements the best we can do for business software?
> So, is pile-of-if-statements the best we can do for business software?
You’ll enjoy the Big Ball of Mud paper[1].
Real world systems are prone to decay. You first of all start with a big ball of mud because you’re building a system before you know what you want. Then as parts of the system grow up, you improve the design. Then things change again and the beautiful abstraction breaks down.
Production software is always changing. That’s the beauty of it. Your job is to support this with a mix of domain modeling, good enough abstraction, and constructive destruction. Like a city that grows from a village.
I'm not sure the author or most people that write these types of academic theory papers ever really see actual ball-of-mud-spaghetti code in real world scenarios.
I think anyone that thinks mudball is OK because business is messy has never seen true mudball code.
I've had to walk out of potential work because after looking at what they had I simply had to tell them I cannot help you, you need a team and probably at minimum a year to make any meaningful progress. That is what mudballs leads to. What this paper describes is competent work that is pushed too quickly for cleaning rough edges but has some sort of structure.
I've seen mudballs that required 6-12 months just to do discovery of all the pieces and parts. Hundreds of different version of things no central source control, different deployment techniques depending on the person that coded it even within the same project.
I believe you can build great abstractions in this kind of software, but if you want them to survive you gotta keep them any of that away from anything involving the business logic itself. You can only do this on product-like things: authn/authz, audit logs, abstractions over the database (CQRS, event sourcing), content/translation management, messaging infrastructure, real infrastructure. As soon as you allow anything from the business itself to affect or dictate those abstractions, you get shit again.
You're right that the business logic is gonna be messy, and that's because nobody really cares, and they can offload the responsibility to developers, or anyone punching it in.
On the other hand, separating "good code" and "bad code" can have horrible outcomes too.
One "solution" I saw in a fintech I worked at, was putting the logic in the hands of business people itself, in the form of a decision engine.
Basically it forced the business itself to maintain its own ball of mud. It was impossible to test, impossible to understand and even impossible simulate. Eventually software operators were hired, basically junior-level developers using a graphical interface for writing the code.
It was rewritten a couple times, always with the same outcome of everything getting messy after two or three years.
You can't depend on the business people to understand the business logic clearly enough to explain it to the implementers. That will never happen. They may understand it themselves, but they're not coders and they can't write requirements for code.
Instead, at least one implementer needs to get hands dirty on what the application space really is. Very dirty. So dirty that they actually start to really know and care about what the users actually experience every day.
Or, more realistically for most companies, we insist on separate silos, "business logic" comes to mean "stupid stuff we don't really care about", and we screw around with if statements. (Or, whatever, we get hip to monads and screw around with those. That's way cooler.)
IMO you touch on the real heart of the issue at the end - the real world and business is messy and really _is_ just a pile of if statements.
When the problem itself is technical or can be generalised then abstractions can eliminate the need for 1000s of if-statement developers but if the domain itself is messy and poorly specified then the only ways abstractions (and tooling) can help is to bake in flexibility, because contradiction might be a feature not a bug...
In most assembly languages, the instructions are essentially load and store, arithmetic operations, and branch and jump. Almost everything is abstractions around how to handle branching and memory.
I was recently having a conversation with some coworkers about this.
IMO a lot of (software) engineering wisdom and best practices fails in the face of business requirements and logic. In hard engineering you can push back a lot harder because it's more permanent and lives are more often on the line, but with software, it's harder to do so.
I truly believe the constraints of fast moving business and inane, non sensical requests for short term gains (to keep your product going) make it nearly impossible to do proper software engineering, and actually require these if-else nests to work properly. So much so that I think we should distinguish between software engineering and product engineering.
> a lot of (software) engineering wisdom and best practices fails in the face of business requirements
They fail on reality. A lot of those "best" practices assume, that someone understands the problem and knows what needs to be built. But that's never true. Building software is always an evolutionary process, it needs to change until it's right.
Try to build an side project, that doesn't accept any external requirements, just your ideas. You will see that even your own ideas and requirements shift over time, a year (or two) later your original assumptions won't be correct anymore.
This is how I feel as well. What's even worse is that it seems like the academics doing research in the area of software engineering don't really have up to date experience that's practical.
Add to the fact that they're the professor of many software engineering courses and you start to see why so many new grads follow SOLID so dogmatically, which leads to codebases quickly decaying.
Been playing with Codex CLI the past week and it really loves to create a fix for a bug by adding a special case for just that bug in the code. It couldn't see the patterns unless I pointed them out and asked it to create new abstractions.
It would just keep adding what it called "heuristics", which were just if statements that tested for a specific condition that arose during the bug. I could write 10 tests for a specific type of bug, and it would happily fix all of them. When I add another one test with the same kind of bug it obviously fails, because the fix that Codex came up with was a bunch of if statements that matched the first 10 tests.
Also they hedge a lot, will try doing things one way, have a catch / error handler and then try a completely different way - only one of them can right but it just doesn't care. Have to lean hard to get it to check which paths are actually used and delete the others.
I am convinced this behaviour and the one you described are due to optimising for swe benchmarks that reward 1-shotting fixes without regard to quality. Writing code like this makes complete sense in that context.
It's clear that these AIs are approaching human level intelligence. (:
Thank you for giving a perfect example of what I was describing.
The thing is, you actually can make the software work this way, you just have to add enough if-statements to handle all cases--or rather, enough cases that the manager is happy.
There are many ways code can get simpler even with ifs.
If you find yourself sprinkling ifs everywhere, try to lift them up, they’ll congregate at the same place eventually, so all of your variability is implemented and documented at a single place, no need to abstract anything.
It’s very useful to model your inputs and outputs precisely. Postpone figuring out unified data types as long as possible and make your programming language nice to use with that decision.
Hierarchies of classes, patterns etc are a last resort for when you’re actually sure you know what’s going on.
I’d go further and say you don’t need functions or files as long as your programming is easy to manage. The only reason why you’d need separate files is if your vcs is crippled or if you’re very sure that these datetime handlers need to be reused everywhere consistently.
Modern fullstack programming is filled with models, middleware,
Controllers , views , … as if anyone needs all of that separation up front.
These abstractions become a toolset for creating a program that naturally evolves as new goals and constraints are introduced. It also allows other engineers to understand your code at a high level without reading it from top to bottom.
If your code ever has the possibility of changing, your early wins by having no abstraction are quickly paid for, with interest, as you immediately find yourself refactoring to a higher abstraction in order to reason about higher-order concepts.
In this case, the abstraction is the simplicity, for the same reason that when I submit this comment, I don't have to include a dictionary or a definition of every single word I use. There is a reason that experienced programmers reach for abstractions from the beginning, experience has taught them the benefits of doing so.
The mark of an expert is knowing the appropriate level of abstraction for each task, and when to apply specific abstractions. This is also why abstractions can sometimes feel clumsy and indirect to less experienced engineers.
Most business logic is “last mile” software. Built on top of beautiful abstractions that came not from an abstract idea of what’s correct but from painful clashes with reality that eventually provided enough clarity to build a good abstraction.
Sometimes last mile software turns into these abstractions but often not.
I’ve worked with very smart devs that try to build these abstractions too early, and once they encounter reality you just have a more confusing version of if statement soup.
I like to make truth tables for understanding piles of ifs. Like there's 5 ifs with 5 different conditions - so I make 5 columns and 32 rows, and enumerate all the possible combinations of these 5 ifs and write what happens for each. And then what should happen.
Of course, the disadvantage is the exponential growth. 20 ifs means a million cases (usually less because the conditions aren't independent, but still).
Then I have a flat list of all possible cases, and I can reconstruct a minimal if tree if I really want to (or just keep it as a list of cases - much easier to understand that way, even if less efficient).
Often you can check validity one time before everything else. 5 bools might only actually be valid in 7 possible combinations instead of 32. Convert in one place to a 7 element enum and handle with exhaustive switch statements everywhere else can sometimes be a lot cleaner.
Making invalid data unrepresentable simplifies so much code. It's not always possible but it is way underused. You can do some of it with object encapsulation too, but simple enums with exhaustive switch statements enforced by the compiler (so if it changes you have to go handle the new case everywhere) is often the better option.
I'm also into building abstractions, but I always try to leave "escape hatches" in place. I try to build my abstractions out of reusable components, that can also be used independently.
If the abstraction doesn't fit a new problem, it should be easy to reassemble the components in a different way, or use an existing abstraction and replace some components with something that fits this one problem.
The developers shouldn't be forced to use the abstractions, they should voluntarily use them because it makes it easier for them.
One would imagine by now we would have some incredibly readable logical language to use with the SQL on that context...
But instead we have people complaining that SQL is too foreign and insisting we beat it down until it becomes OOP.
To be fair, creating that language is really hard. But then, everybody seems to be focusing on destroying things more, not on constructing a good ecosystem.
But even with OOP... Virtual functions take over the pile of ifs, and so the ifs move to where you instantiate the class that has the virtual functions. (There is some improvement, though - one class can have many virtual functions, so you can replace all the ifs that ask the same question with one if that creates a class with all the right virtual functions. It gets messier if your class has to be virtual against more than one question.)
You usually don't want your code logic to stray from the mental or domain model of business stakeholders. Usually when my code makes assumptions to unify things or make elegant hierarchies, I find myself in a very bad place when stakeholders later make decisions that flip everything and make all my assumptions in the code base structure fall apart.
I think one issue you can run into with clever abstractions is that it can be harder to fix/change them if something is wrong with their fundamental assumptions (or those assumptions change later). Something like this happened at my work a while back, where if I had written the code it would have probably just involved a few really long/ugly functions (but only required changing a few lines in and after the SQL query to fix), but instead the logic was so deeply intertwined with the code structure that there wasn't any simple way to fix it without straight-up rewriting the code (it was written in a functional way with a bunch of functions taking other functions as arguments and giving functions as output, which also made debugging really tough).
It also depends how big the consequences to failure/bugs are. Sometimes bugs just aren't a huge deal, so it's a worthwhile trade-off to make development easier in change for potentially increasing the chance of them appearing.
Abstractions are ok, SomethingFactories are stupid. If your code is more abstractions than actual logic and you need logic to manage the abstractions (eg. FactoryFactories, 2+ inheritance levels), you should rethink your strategy.
I had previously thought SomethingFactory was abstracting away the logic for the "new" keyword, but for people who dislike inversion of control frameworks
I'm firmly in the "DI||GTFO" camp, so I don't meant to advocate for the Factory pattern but saying that only abstractions that you like are ok starts to generate PR email threads
> "A single order ships to a single address, keep it simple, build it, oh actually, a salesman promised a big customer, so now we need to make it so a single order can ship to multiple addresses"--you've heard something like this before, haven't you?
I don't see the problem. Okay, so we need to support multiple addresses for orders. We can add a relationship table between the Orders and ShippingAddresses tables, fix the parts of the API that need it so that it still works for all existing code like before using the updated data model, then publish a v2 of the api with updated endpoints that support creating orders with multiple addresses, adding shipping addresses, whatever you need.
Now whoever is dependent on your system can update their software to use the v2 endpoints when they're ready for it. If you've been foolish enough to let other applications connect to your DB directly then those guys are going to have a bad time, might want to fix that problem first if those apps are critical. Or you could try to coordinate the fix across all of them and deploy them together with the db update.
The problems occur when people don't do things properly, we have solutions for these problems. It's just that people love taking shortcuts and that leads to a terrible system full of workarounds rather than abstractions. Abstractions are malleable, you can change them to suit your needs. Use the abstractions that work for you, change them if they don't work any more. Design the code in such a way that changing them isn't a gargantuan task.
> I don't see the problem. Okay, so we need to support multiple addresses for orders. We can add a relationship table between the Orders and ShippingAddresses tables
Which items ship to each of those locations and in which quantities?
What is the status of each of those sub-orders in the fulfillment process?
Should the orders actually ship to those addresses or should the cartons just be packed and marked for those locations for cross-docking and the shipments should be split across some number of regional DC's based on proximity to the final address?
Many things need to be updated in the DB schema+code. And if you think this isn't a very good example, it's a real life example of orders for large retailers.
You can carefully pick an order of features to build in a way, that every new feature will invalidate an abstraction correctly implementing all the previous features.
There is an alternative: take the parts that _aren't_ in if statements (the actual common code) and make them into shared functions. Then split up the rest into multiple functions that call the shared functions, one for each independent condition, so that they don't have if statements.
These individual functions are easier to reason about since they have specific use cases, you don't have to remember which combinations of conditions happen together while reading the code, they simplify control flow (i.e. you don't have to hack around carrying data from one if block to the next), and it uses no "abstraction" (interfaces) just simple functions.
It's obviously a balance, you'll still have some if statements, but getting rid of mutually exclusive conditions is basically a guaranteed improvement.
"the owners of the business logic do not treat the business logic with enough care."
Certainly, there are such people who simply don't care.
However I would also say that corporations categorically create an environment where you are unable to care - consider how short software engineer tenures are! Any even somewhat stable business will likely have had 3+ generations of owner by the time you get to them. Owner 1 is the guy who wrote 80% of the code in the early days, fast and loose, and got the company to make payroll. Owner 2 was the lead of a team set up to own that service plus 8 others. Owner 3 was a lead of a sub-team that split off from that team and owns that service plus 1 other related service.
Each of these people will have different styles - owner 1 hated polymorphism and everything is component-based, owner 2 wrapped all existing logic into a state machine, owner 3 realized both were leaky abstractions and difficult to work with, so they tried to bring some semblance of a sustainable path forward to the system, but were busy with feature work. And owner 3 did not get any Handoff from person 2 because person 2 ragequit the second enough of their equity vested. And now there's you. You started about 9 months ago and know some of the jargon and where some bodies are buried. You're accountable for some amount of business impact, and generally can't just go rewrite stuff. You also have 6 other people on call for this service with you who have varying levels of familiarity with the current code. You have 2.25 years left. Good luck.
Meanwhile I've seen codebases owned by the same 2 people for over 10 years. It's night and day.
What you say is true, but I meant the product owners are the one who don't fully weigh the cost of their decisions.
I once tried to explain to a product owner that we should be careful to document what assumptions are being made in the code, and make sure the company was okay committing to those assumptions. Things like "a single order ships to a single address" are early assumptions that can get baked into the system and can be really hard to change later, so the company should take care and make sure the assumptions the programmers are baking into the system are assumptions the company is willing to commit to.
Anyway, I tried to explain all this to the product owner, and their response was "don't assume anything". Brillant decisions like that are why they earned the big bucks.
I honestly think that's pretty close to optimal for a lot of places. With business software it's often not desirable to have large sweeping changes. You may need some small change to a rule or condition, but usually you want things to stay exactly the way they are.
The model of having a circle of ancient greybeards in charge of carefully updating the sacred code to align with the business requirements, while it seems bizarre bordering on something out of WH40K, actually works pretty well and has worked pretty well everywhere I've encountered it.
Attempts to refactor or replace these systems with something more modern has universally been an expensive disaster.
Project Manager: "Can we ship an order to multiple addresses?"
Grey Beard: "No. We'd have to change thousands of random if-statements spread throughout the code."
Project Manager: "How long do you think that would take?"
Grey Beard: "2 years or more."
Project Manager: "Okay, we will break you down--err, I mean, we'll need to break the task down. I'll schedule long meetings until you relent and commit to a shorter time estimate."
Grey Beard eventually relents and gives a shorter time estimate for the project, and then leaves the company for another job that pays more half-way through the project.
There are two things I hate more than anything else when coding - if statements and abstraction. If statements are bug magnets. Abstraction is a mental drain. My coding stile is to balance those two things in a way that makes the code as easy to read and extend as possible without relying on either too much and only just enough.
I am not a super experienced coder or anything. But I like thinking about it[1].
The way I've been thinking about it is about organization. Organize code like we should organize our house. If you have a collection of pens, I guess you shouldn't leave them scattered everywhere and in your closet, and with your cutlery, and in the bathroom :) You should set up somewhere to keep your pens, and other utensils in a kind of neat way. You don't need to spend months setting up a super-pen-organizer that has a specially sculpted nook for your $0.50 pen that you might lose or break next week. But you make it neat enough, according to a number of factors like how likely it is to last, how stable is your setup, how frequently it is used, and so on. Organizing has several advantages: it makes it easier to find pens, shows you a breath of options quickly, keeps other places in your house tidier and so less cognitively messy as well. And it has downsides, like you need to devote a lot of time and effort, you might lose flexibility if you're too strict like maybe you've been labeling stuff in the kitchen, or doing sketches in your living room, and you need a few pens there.
I don't like the point of view that messiness (and say cognitive load) is always bad. Messiness has real advantages sometimes! It gives you freedom to be more flexible and dynamic. I think children know this when living in a strict "super-tidy" parent house :) (they'd barely get the chance to play if everything needs to be perfectly organized all the time)
I believe in real life almost every solution and problem is strongly multifactorial. It's dangerous to think a single factor, say 'cognitive load', 'don't repeat yourself', 'lesser lines of code', and so on is going to be the single important factor you should consider. Projects have time constraints, cost, need for performance; expressing programs, the study of algorithms and abstractions itself is a very rich field. But those single factors help us improve a little on one significant facet of your craft if you're mindful about it.
Another factor I think is very important as well (and maybe underestimated) is beauty. Beauty for me has two senses: one in an intuitive sense that things are 'just right' (which capture a lot of things implicitly). A second and important one I think is that working and programming, when possible, should be nice, why not. The experience of coding should be fun, feel good in various ways, etc. when possible (obviously this competes with other demands...). When I make procedural art projects, I try to make the code at least a little artistic as well as the result, I think it contributes to the result as well.
[1] a few small projects, procedural art -- and perhaps a game coming soon :)
> So, is pile-of-if-statements the best we can do for business software?
I'm not sure if that's anywhere in the rating of quality of business software. Things that matter:
1. How fast can I or someone else change it next time to fulfill the next requirements?
2. How often does it fail?
3. How much money does the code save or generate by existing.
Good architecture can affect 1 and 2 in some circumstances but not every time and most likely not forever at the rate people are starting to produce LLM garbage code. At some point we'll just compile English directly into bytecode and so architecture will matter even less. And obviously #3 matters by far the most.
It's obviously a shame for whoever appreciates the actual art / craft of building software, but that isn't really a thing that matters in business software anyway, at least for the people paying our salaries (or to the users of the software).
The ability to create code that imposes low cognitive load on others not only is a rare and difficult skill to cultivate- it takes active effort and persistence to do even for someone who already has the ability and motivation. I think fundamentally the developer is computing a mental compression of the core ideas - distilling them to their essence - and then making sure that the code exposes only the minimum essential complexity of those ideas. not easy and rare to see in practice
And if you do it really well, people think it must have been such an easy problem to solve all along. Since everything always appears so obvious in insight.
While the castle of cards of unfathomable complexity is praised for visibly hard work and celebrated with promotions.
This is also true of interface/UX/interaction design. Most developers are really skilled at maintaining a higher cognitive load than most, and the interfaces that work best for less technical people often frustrate developers, who want everything in front of them, visible, at all times because they intuitively know what’s important. Interfaces created by developers might click with other devs, but often bewilder less technical people. It’s really hard to design a tool that less technical people can use intuitively to solve complex problems without wanting to throw their electronics out the window.
This was maybe a problem years ago, but I don't think its a pro lem these days. I see many more cases of the opposite problem, interfaces that are meant of technical users but are designed using modern mobile centric paradigms, over emphasizing negative space and progressive disclosure.
this is also a problem for tools designed for non-technical users for complex tasks that are performed frequently. your power users needs a powerful interface even if they are less technical.
And ironically, writing code that is maintainable on the long run and imparts a low cognitive load on successive developers is itself a cognition consuming effort.
In tradeoff engineering, maintainability over the long term is one of the many variables to optimize, and finite resources need to be alloted to it.
When I read this article I get the feeling that it's more likely that he is obsessing over maintainability over the long term while his app has a user count of zero. This malady usually comes from the perspective of being a user, one finds that the experience of writing some code is a "bad experience" so they strive to improve it or learn how to build a good "coder experience", the right answer is to understand that one is stepping into the shoes of the plumber, and it will be shitty, just gotta roll up your sleeves.
Don't get me wrong, there's a lot of wisdom here, but to the extent that there is, it's super derivative and well established, it's just the kind of stuff that a developer learns on their first years of software by surfing the web and learning about DRY, KISS and other folklore of software. To some extent this stuff is useful, but there's diminishing returns and at some point you have to throw shit and focus on the product instead of obsessing over the code.
Plus rarely survives requirements/context changing because most abstractions are leaky.
My favourite frameworks are written by people smart enough to know they're not smart enough to build the eternal perfect abstraction layers and include 'escape hatches' (like getting direct references to html elements in a web UI framework etc) in their approach so you're not screwed when it turns out they didn't have perfect future-sight.
I learned a lot this in competitive programming. I used to write code that were 3-5x the size of the intended solution. There are many reason why you could write a worse/bigger solution but sometimes when I compared both solutions one thing I could notice is that I could remove a lot of things from my solution until nothing could be removed anymore and the final solution would really seem like it were the essence of the problem.
But even if the intended solution seemed simpler, it could be much harder to discover it.
I think one issue is that some people just find very different things intuitive. Low cognitive load for one person might be high cognitive load for another.
Because of some quirk of the way my brain works, giant functions with thousands of lines of code doesn't really present a high cognitive load for me, while lots of smaller functions do. My "working memory" is very low (so I have trouble seeing the "big picture" while hopping from function to function), while "looking through a ton of text" comes relatively easily to me.
I have coworkers who tend to use functional programming, and even though it's been years now and I technically understand it, it always presents a ton of friction for me, where I have to stop and spend a while figuring out exactly what the code is saying (and "mentally translating" it into a form that makes more sense to me). I don't think this is necessarily because their code inherently presents a higher cognitive load - I think it's easier for them to mentally process it, while my brain has an easier time with looking at a lot of lines of code, provided the logic within is very simple.
This article reminds me of my early days at Microsoft. I spent 8 years in the Developer Division (DevDiv).
Microsoft had three personas for software engineers that were eventually retired for a much more complex persona framework called people in context (the irony in relation to this article isn’t lost on me).
But those original personas still stick with me and have been incredibly valuable in my career to understand and work effectively with other engineers.
Mort - the pragmatic engineer who cares most about the business outcome. If a “pile of if statements” gets the job done quickly and meets the requirements - Mort became a pejorative term at Microsoft unfortunately. VB developers were often Morts, Access developers were often Morts.
Elvis - the rockstar engineer who cares most about doing something new and exciting. Being the first to use the latest framework or technology. Getting visibility and accolades for innovation. The code might be a little unstable - but move fast and break things right? Elvis also cares a lot about the perceived brilliance of their code - 4 layers of abstraction? That must take a genius to understand and Elvis understands it because they wrote it, now everyone will know they are a genius.
For many engineers at Microsoft (especially early in career) the assumption was (and still is largely) that Elvis gets promoted because Elvis gets visibility and is always innovating.
Einstein - the engineer who cares about the algorithm. Einstein wants to write the most performant, the most elegant, the most technically correct code possible. Einstein cares more if they are writing “pythonic” code than if the output actually solves the business problem. Einstein will refactor 200 lines of code to add a single new conditional to keep the codebase consistent. Einsteins love love love functional languages.
None of these personas represent a real engineer - every engineer is a mix, and a human with complex motivations and perspectives - but I can usually pin one of these 3 as the primary within a few days of PRs and a single design review.
Clearly they were missing Amanda, the engineer who's had to review others' terrible code (and her own) for 20 years, and has learned the hard way to keep it simple. She knows she's writing code mostly for people to read, not computers. Give me a small team of Amandas any day.
Mort, Elvis, Einstein, Amanda does seem to fit well with my experience. While people are a mix, generally I think its fair that there is a primary focus/mode that fits on career goals.
- Mort wants to climb the business ladder.
- Elvis wants earned social status.
- Einstein wants legacy with unique contributions.
- Amanda just wants group cohesion and minimizing future unpredictability.
And as a manager/CTO, the way to do this is to give the devs time to think about what they're doing, and reward implementation clarity (though it's its own reward for Amandas).
If there is no inherent complexity, a Mort will come up with the simplest solution. If it's a complex problem needing trade-offs the Mort will come up with the fastest and most business centric solution.
Or would you see that Amanda refactoring a whole system to keep it simple above all whatever the deadlines and stakes ?
Mort: Someone who lacks sense of life, looks dumbfounded, and has only a limited ability to learn and understand. (urban slang)
Elvis: A famous rock star
Enstein: A famous physicist
Amanda: ???
Mort, Elvis, Enstein are referencing things I've heard of before. What is Amanda referencing? is there some famous person named Amanda? Is it slang I'm unaware of?
They were also missing Steve Jobs. Having had the displeasure to work with Microsoft tools and code for most of my career. Microsoft never in my experience just plain works. I had to fight Microsoft every step of the way to get things to "work". And when it does it invariably breaks in the next major software release.
You clearly missed the entire message of the entire "three kinds of developers" sort of shit if you think that a fourth type that's perfect is what's missing from it.
Mort is the pragmatist, Einstein is the perfectionist, and Elvis is... let's be honest, Elvis is basically cancer to a project. I guess maybe a small dose of Elvis can help motivate?
I see the ideal as a combination of Mort and Einstein that want to keep it simple enough that it can be delivered (less abstraction, distilled requirements) while ensuring the code is sufficiently correct (not necessarily "elegant" mind you) that maintenance and support won't be a total nightmare.
IMO, seek out Morts and give them long term ownership of the project so they get a little Einstein-y when they realize they need to support that "pile of if statements".
As an aside, I'm finding coding agents to be a bit too much Mort at times (YOLO), when I'd prefer they were more Einstein. I'd rather be the Mort myself to keep it on track.
> Elvis is basically cancer to a project. I guess maybe a small dose of Elvis can help motivate?
Sometimes teams are quite stuck in their ways because they don’t have the capacity or desire to explore anything new.
For example, an Elvis would probably introduce containers which would eliminate a class of dependency and runtime environment related issues, alongside allowing CI to become easier and simpler, even though previously using SCP and Jenkins and deploying things into Tomcat mostly worked. Suddenly even the front end components can be containers, as can be testing and development databases, everyone can easily have the correct version locally and so on.
An unchecked Elvis will eventually introduce Kubernetes in the small shop to possibly messy results, though.
Your comment made me think Mort represents efficiency, Einstein represents quality, and Elvis represents risk. The ideal combination is difficult, and it changes over time. If anyone knew what the ideal combination was, companies would never fail. Risk can get something started, and lack of it can eventually kill software. In fact, I would argue the vast majority of software we’ve seen so far dies an eventual death due in part to its inability to take risk and change and adapt - it might be not enough Elvis in the long term. Too much risk can kill something before it takes off and can undermine the ability to ship and to ship quality. Generally speaking my gut instinct was to (perhaps like you) align with and defend Morts; the business objective is the only thing that matters and pays the bills, and there is certainly a class of Morts that doesn’t write spaghetti code, and cares about quality and tries new things, but prioritizes work toward the customer and not code wonkery. Anyway… this is too probably abstract to be very useful and I made it worse and more abstract, but it’s fun to hypothesize!
Unfortunately for a fast growing industry (think AI, LLM), Mort + Elvis will be much more success then any combination with Einstein. The speed to adapt a new technology into a specific domain outweight your ability to scale for long term (think the oracle vs sybase in server)
The best engineers are all three, and can turn up or down these tendencies depending on what's required for the project, business, or personal goals. These should not be fixed in proportion over time, as they are each useful in different circumstances.
I spent time at Microsoft as well, and one of the things I noticed was folks who spent time in different disciplines (e.g. dev, test, pgm) seemed to be especially great at tailoring these qualities to their needs. If you're working on optimizing a compiler, you probably need a bit more Einstein and Mort than Elvis. If you're working on a game engine you may need a different combination.
The quantities of each (or whether these are the correct archetypes) is certainly debatable, but understanding that you need all of them in different proportions over time is important, IMHO.
Personas are a great tool. IMO - By the time you arrived these had transformed into bad shorthand. (I say this having been in Devdiv through those years.)
Elvis is not a persona - it is an inside baseball argument to management. It suffered a form of Goodhart’s law … it is a useful tool so people phrase their arguments in that form to win a biz fight and then the tool degrades.
Alan Cooper, who created VB advocated personas. When used well they are great.
The most important insight is your own PoV may be flawed. The way a scientist provides value via software is different than how a firmware developer provides value.
I think this is somewhat dangerous, it can lead you to categorise people unfairly and permanently. Also, in my experience this has a critical flaw - the managers love morts in my experience, not Elvises. They don’t care about the technical details, so “fastest and fits the business outcome the most” is ideal.
Also the actual solution is proper team leadership/management. If you have morts, make sure that code quality requirements are a PART of the requirements their code must pass, and they’ll instead deliver decent work slightly slower. Got an elvis? Give more boundaries. Got Einsteins? Redefine the subtasks so they can’t refactor everything and give deadlines both in terms of time but also pragmatism.
Either way, I don’t love this approach, as it removes the complexity from the human condition, complexity which is most important to keep in mind.
I agree with you and one of the most important ways is that it bakes in an assumption that people cant grow, learn and change.
Life is all about learning, adapting and changing. Great leaders see the potential growth in people and are up for having hard conversations about how they can improve.
Even if people do have these personality traits as life long attributes, that doesn't define them or prevent them from learning aspects of the others over time.
Is Microsoft so Balkanized that they have a Developer Division, Developer Multiplication, Developer Addition, and Developer Subtraction (where you get transferred to before they fire you)?
Indeed. And besides that, all three are really bad parodies. Mort is the only one where the product actually works, because for him that’s an explicit goal. With the other two, a working product is mere coincidence.
Exactly the expectation value for "analysis of types of developers done by people who really don't care about people"
"so, there's 3 boxes. no more, no less. why? i have a gut feeling. axis? on a case by case basis. am i willing to put my money where my mouth is? heallnaw!"
I'd love one of those old facebook quizzes like "take this quizz to figure out which friends character you are", but for figuring out whether you are a Mort, an Elvis or an Einstein
That’s super interesting. What was the ideal ratio, back then. Is it still the same now? Or I guess maybe it depends on the specific role and could be different in each. What exactly do you find valuable about thinking in these terms?
Something was bugging me after an interview with a potential hire, and now I can articulate that they were too much Einstein and not enough Mort for the role.
The kind of psycho-bullshit that we should stay away from, and wouldn't happen if we respected each other. Coming from Microsoft is not surprising though.
For my frame of reference, do you think the Myers-Briggs Type Indicator are psycho-bullshit, too? Because I had characterized personas as a very similar "of course it's a generalization" and OP even said themselves "every engineer is a mix" but if you're coming from stance that bucketing people is disrespectful, then your perspective on MBTI would help me digest your stance
A lot of comments mention John Ousterhout's book Philosophy of software design and it's definition of complexity of a system being cognitive load (I.e the number of disparate things one has to keep in mind when making a change). However IIRC from the book, complexity of a system = Cognitive load * Frequency of change.
The second component, frequency of change is equally important as when faced with tradeoffs, we can push high cognitive load to components edited less frequently (eg: lower down the stack) in exchange for lower cognitive load in the most frequently edited components.
This is one of the reasons I fear AI will harm the software engineering industry. AI doesn't have any of these limitation so it can write extremely complex and unreadable code that works... until it doesn't. And then no one can fix it.
It's also why I urge junior engineers to not rely on AI so much because even though it makes writing code so much faster, it prevents them from learning the quirks of the codebase and eventually they'll lose the ability to write code on their own.
> It's also why I urge junior engineers to not rely on AI so much because even though it makes writing code so much faster […]
I am afraid, the cat is out the bag, and there is no turning back with GenAI and coding – juniors have got a taste of GenAI assisted coding and will persevere. The best we can do it educate them on how to use it correctly and responsibly.
The approach I have taken involves small group huddles where we talk to each other as equals, and where I emphasise the importance of understanding the problem space, the importance of the depth and breadth of knowledge, i.e. going across the problem domain – as opposed to focusing on a narrow part of it. I do not discourage the junior engineers from using GenAI, but I stress the liability factor and the cost: «if you use GenAI to write code, and the code falls apart in production, you will have a hard time supporting it if you do not understand the generated code, so choose your options wisely». I also highlight the importance of simplicity over complexity of the design and implementation, and that simplicity is hard, although it is something we should strive for as an aspiration and as a delivery target.
I reflect on and adjust the approach based on new observations, feedback loop (commits) and other indirect signs and metrics – this area is still new, and the GenAI assisted coding framework is still fledging.
Thus far, and granted I don't have as much experience as others, I just demand that AI simplify the code until I understand everything that it is doing. If I see it doing something in a convoluted way, I demand that it does it in the obvious way. If it's adding too many dependencies, I tell it to remove the goofy ones and write it the long way with the less capable stdlib function or helped by something that I already have a dependency on.
Yeah, as much as I don’t like to use AI to write large portions of code, I’m using it to help me learn web development and it can feel like a following a tutorial, but tailored to the exact project I want.
My current approach is creating something like a Gem on Gemini with custom instructions and the updated source code of the project as context.
I just discuss what I want, and it gives me the code to do it, then I write by hand, ask for clarifications and suggest changes until I feel like the current approach is actually a good one. So not really “vibe-coding”, though I guess a large number of software developers who care about keeping the project sane must be doing this.
I'm talking about cases where even AI can't fix it. I've heard of a lot of stories where people vibe code their applications to 80% and then get stuck in a loop where AI is unable to solve their problems.
It's been well documented that LLMs collapse after a certain complexity level.
Or maybe it will actually increase the quality of software engineering because it will free up the cognitive load from thinking of the low level design to higher level architecture.
That's my fear, it will become a sort of a compiler. Prompts will be the code and code will be assembly, and nobody will even try to understand the details of the generated code unless there is something to debug. This will cause the codebases to be less refined with less abstraction and more duplication and bloat, but we will accept it as progress.
Cognitive load is an important concept in aviation. It is linked to the number of tasks to run and the number of parameters to monitor, but it can be greatly reduced by training. Things you know inside and out don't seem to consume as much working memory.
So in software development there may be an argument to always structure projects the same way. Standards are good — even when they're bad! because one of their main benefit is familiarity.
I would say that's very important rule. We have lots of projects using framework dependent magic, lots of useless interfaces and factories that give only theoretical value, magic that patch other classes methods etcz but this is standard practice in this framework and all experienced developers know that.
by doing something better here would actually not bring any value, because it would mean that developers would have to remember that this one thing is done differently.
that's trap where I would say many mid Devs fall in, they learned how do things better, but increase congnitive load for the rest of developers just by doing things differently.
An important difference is that the aviator would be the user of the airplane system. OP is talking about the cognitive load of the plane engineer.
It's an important distinction in terms of priorities. I personally think the experience of the user is orders of magnitude more important than engineer cognitive load.
For instance, the article itself suggests to use early/premature returns, while they are sometimes compared to "goto", making the control flow less obvious/predictable (as paxcoder mentioned here). Intermediate variables, just as small functions, can easily complicate reading of the code (in the example from the article, one would have to look up what "isSecure" means, while "(condition4 && !condition5)" would have shown it at once, and an "is secure" comment could be used to assist skimming). As for HTTP codes, those are standardized and not dependent on the content, unlike custom JSON codes: most developers working with HTTP would recognize those without additional documentation. And it goes on and on: people view different things as good practices and being simpler, depending (at least in part) on their backgrounds. If one considers simplicity, perhaps it is best to also consider it as subjective, taking into account to whom it is supposed to look simple. I think sometimes we try to view "simple" as something more objective than "easy", but unless it is actually measured with something like Kolmogorov complexity, the objectivity does not seem to be there.
You would feel the need to look up a variable called isSecure, but would not need to look up condition4 or condition5? I think the point TFA was making is that one could read isSecure and assume what kind of implementation to expect, whereas with condition4 I wouldn't even know what to look for, or I'd even struggle to hold any assumption.
> and an "is secure" comment could be used to assist skimmingThose are exactly the kind of comments I'd rather see written out as intermediate variables. Such comments are not explaining to you any of the Why?s anyway, and I also tend to trust the executing code much more than any non-type-related annotating code, as comments are rarely helpful and sometimes even describe wishful thinking by straight-up lying.
Intermediate variables assist in skimming too.
I assume that those "conditions" are placeholders, not to be read literally in the example (since the example is not about poorly named variables, but about complex conditions), so I did not mean them literally, either. Supposedly those would be more informative names, such as "channel_encrypted", "checksum_verified".
> [...] describe wishful thinking by straight-up lying
This was what I had in mind upon seeing that "isSecure" bit, too: could easily be a lie (or understood differently by different people). But taking a little more effort to check then, and/or having to remember what those variables actually mean. It is a commonly debatable topic though, where the good balance is, similarly to splitting code into small functions: people tend to balance between spaghetti code and extreme splitting.
My point though is not to argue with those particular points here, but that we have no such practices/rules universally considered simple and formally stated/verifiable.
I would rather name intermediate variables to match the statement rather than some possible intent, it's basically a form of semantic compression. For example isAdminUser = user.role == 'admin' - here we are hiding away the use of roles which is not relevant for the conditional, but isSecure can mean anything, we don't want to hide the concept of being an admin user, just the details of using roles to determine that. At least that's my take.
if (user.id < 4 and user.session): ...
# you'd do
_user_has_access = (user.id < 4 and user.session) if _user_has_access: ...
# or even walrus
if _user_has_access := (user.id < 4 and user.session): ...
One of my coworkers really liked this pattern and his code was always so easy to read and to this day I'm carrying this torch!
> The problem is that familiarity is not the same as simplicity. They feel the same — that same ease of moving through a space without much mental effort — but for very different reasons. Every “clever” (read: “self-indulgent”) and non-idiomatic trick you use incurs a learning penalty for everyone else. Once they have done that learning, then they will find working with the code less difficult. So it is hard to recognise how to simplify code that you are already familiar with. This is why I try to get “the new kid” to critique the code before they get too institutionalised!
This was explored in the latter part of the article.
Low ego wins.
1. Given: The quality of a codebase as a whole is greatly affected by its level of consistency + cohesiveness
2. Therefore: The best codebases are created by groups that either (1) internally have similar taste or (2) are comprised of low ego people willing to bend their will to the established conventions of the codebase.
Obviously, this comes with caveats. (Objectively bad patterns do exist.) But in general:
Low-ego → Following existing conventions → They become familiar → They seem simpler
The best I can come up with is "code that causes the least amount of scrolling" which is hard to measure and may be mitigated with IDE help.
For one-off things like value = condition ? a : b I don't mind much, but I will make an issue as soon as it spans more than one line or if it's nested.
I like premature returns and think they reduce complexity, but as exclipy writes (I think quoting Ousterhout) 'complexity is defined as "how difficult is it to make changes to it"'.
If premature returns are the only premature exit your language has then they add complexity in that you can't then add code (in just one place) that is always executed before returning.
A good language will also have "break" from any block of code, such that the break can also carry a return value, AND the break can be from any number of nested blocks, which would generally mean that blocks can be labelled / named. And also of course that any block can have a return value.
So you don't actually need a distinguished "return" but only a "break" that can be from the main block of the function.
A nice way to do this is the "exit function", especially if the exit function is a first class value and can also exit from called functions. (of course they need to be in a nested scope or have the exit function passed to them somehow).
It is also nice to allow each block to have a "cleanup" section, so that adding an action to happen on every exit doesn't require wrapping the block in another block, but this is just a convenience, not a necessity.
Note that this is quite different to exception handling try / catch / finally (Java terms) though it can be used to implement exception handling.
Even in a language that is not "good" by your definition... you have basically just described a function. A wrapper function around a sub-function that has early returns does everything you want. I use this pattern in C all of the time.
As a C++ guy I'm on the early-return side of things, because it communicates quickly which fallible operations (don't) have fallbacks.
You see "return" you know there is no "else".
Also, as a code-formatting bonus, you can chain a bunch of fallible operations without indenting the code halfway across your monitor.
Instead I think you should generally only use early returns for errors or a null result, then they're fine. Ditto if you're doing a result pattern, and return a result object, as long as the early return is not the success result (return error or validation errors or whatever with the result object).
So I feel code like this is confusing:
The early return is easy to miss when scanning the code. This is much less confusing: It makes the control flow much more obvious to me. Also, in the 2nd pattern, you can always have your cleanup code after the control block.Basically, you should aim to minimise complexity in software design, but importantly, complexity is defined as "how difficult is it to make changes to it". "How difficult" is largely determined by the amount of cognitive load necessary to understand it.
You can't win architecture arguments.
I like the article but the people who need it won't understand it and the people who don't need it already know this. As we say, it's not a technical problem, it's always a people and culture problem. Architecture just follows people and culture. If you have Rob Pike and Google you'll get Go. You can't read some book and make Go. (whether you like it or not is a different question).
* Our coding standards require that functions have a fairly low cyclomatic complexity. The goal is to ensure that we never have a a function which is really hard to understand.
* We also require a properly descriptive header comment for each function and one of the main emphases in our code reviews is to evaluate the legibility and sensibility of each function signature very carefully. My thinking is the comment sort of describes "developer's intent" whereas the naming of everything in the signature should give you a strong indication of what the function really does.
Now is this going to buy you good architecture for free, of course not.
But what it does seem to do is keep the cognitive load manageable, pretty much all of the time these rules are followed. Understanding a particular bit of the codebase means reading one simple function, and perhaps 1-2 that are related to it.
Granted we are building websites and web applications which are at most medium fancy, not solving NASA problems, but I can say from working with certain parts of the codebase before and after these standards, it's like night and day.
One "sin" this set of rules encourages is that when the logic is unavoidably complex, people are forced to write a function which calls several other functions that are not used anywhere else; it's basically do_thing_a(); do_thing_b(); do_thing_c();. I actually find this to be great because it's easy to notice and tells us what parts of the code are sufficiently complex or awkward as to merit more careful review. Plus, I don't really care that people will say "that's not the right purpose for functions," the reality is that with proper signatures it reads like an easy "cliffs notes" in fairly plain English of exactly what's about to happen, making the code even easier to understand.
That's not true. There's plenty of beginner programmers who will benefit from this.
That's true. One doesn't change his mindset just after reading. Even after some mentorship the results are far from satisfying. Engineers can completely agree with you on the topic, only to go and do just the opposite.
It seems like the hardest thing to do is to build a feedback loop - "what decisions I made in past -> what it led to". Usually that loop takes a few years to complete, and most people forget that their architecture decisions led to a disaster. Or they just disassociate themselves.
Unless it's a rule prohibiting complexity by removing technologies. Here's a set of rules I have in my head.
1. No multithreading. (See Mozilla's "You must be this high" sign)
2. No visitor pattern. (See grug oriented development)
3. No observer pattern. (See django when signals need to run in a particular order)
4. No custom DSL's. (I need to add a new operator, damnit, and I can't parse your badly written LALR(1) schema).
5. No XML. (Fight me, I have battle scars.)
I feel this in my soul. But I'm starting to understand this and accept it. Acceptance seem to lessen my frustration on discussing with architects that seemingly always take the opposite stance to me. There is no right or wrong, just always different trade offs depending on what rule or constraint you are prioritizing in your mind.
This is true. However, very few people can clearly explain all the rules.
If they can, they have understood the system and are qualified.
I think it's a pretty good compromise. I have tried in the past not to duplicate code at all, and it often ends up more pain than gain. Allow copy/paste if code is needed in two different places, but refactor if needed in three or more, is a pretty good rule of thumb.
Example: you have a config defined as Java/Go classes/structures. You want to check that the config file has the correct syntax. Non-DRY strategy is to describe its structure in an XSD schema (ok, ok JSON schema) and then validate the config. So you end up with two sources of truth: the schema and Java/Go classes, they can drift apart and cause problems.
The DRY way is to generate the classes/structures that define the config from that schema.
When you’re dealing with perennial plants, there’s only so much control you actually have, and there’s a list of things you know you have to do with them but you cannot do them all at once. There is what you need to do now, what you need to do next year, and a theory of what you’ll do over the next five years. And two years into any five year plan, the five year plan has completely changed. You’re hedging your bets.
Traditional Formal English and French gardens try to “master” the plants. Force them to behave to an exacting standard. It’s only possible with both a high degree of skill and a vast pool of labor. They aren’t really about nature, or food. They’re displays of opulence. They are conspicuous consumption. They are keeping up with the Joneses. Some people love that about them. More practical people see it as pretentious bullshit.
I think we all know a few companies that make a bad idea work by sheer force of will and overwhelming resources.
Deliberately going earlier makes sense if experience teaches you there will be 3+ of this eventually, but the point where I'm going to pick "Decline" and write that you need to fix this first is when I see you've repeated something 4-5 times, that's too many, we have machines to do repetition for us, have the machine do it.
An EnableEditor function? OK, meaningful name. EnablePublisher? Hmm, yes I understand the name scheme but I get a bad feeling. EnableCoAuthor? Approved with a stern note to reconsider, are we really never adding more of these, is there really some reason you can't factor this out? EnableAuditor. No. Stop, this function is named Enable and it takes a Role, do not copy-paste and change the names.
I even tried calling the bookstore on his campus and they said try back at the beginning of a semester, they didn’t have any copies.
My local book store could not source me a copy, and neither IIRC could Powell’s.
Ultimately, context, industries and teams vary so greatly that it doesn't make sense to quantify it.
What I've settled on instead is aiming for a balance between "mess" and "beauty" in my design. The hardest thing for me personally to grasp was that businesses are indeterministic whereas software is not, thus requirements always shifts and fitting this into the rigidity of computer systems is _difficult_.
These days, I only attempt to refactor when I start to feel the pain when I'm about to change the code.. and even then, I perform the bare minimum to clean up the code. Eventually multiple refactoring shapes a new pattern which can be pulled into an abstraction.
Also, people confuse familiar with simple, they tend to find things simple if they are familiar, even if they are complex (interwine a lot of different things).
But I'm not experienced enough to tell, whether it is my inexperience that causes the difficulty, or it is indeed that the unnecessary complexity tha causes it.
Watch ‘Simple made Easy’ by Rich Hickey; a classic from our industry. The battle against complexity is ever ongoing. https://youtu.be/SxdOUGdseq4?feature=shared
With that said , get good at jumping into definitions, finding all implementations, then jumping back where you were. With a good IDE you can do that at the speed of thought (in IntelliJ that’s Cmb+b, Cmd+Alt+b, Cmd+[ on Mac). I only open more than one file at the same time when comparing them. Otherwise it’s much easier to jump around back and forth (you can even open another function inline if you just want to take a Quick Look, it’s Alt+Space). Don’t use the mouse to do that, things you do all the time can be made an order of magnitude faster via shortcuts. Too many developers I see struggle with that and are embarrassingly slow moving around the code base!
I remember using TCL in the 90s for my own projects as an embeddable command language. The main selling point was that it was a relatively widely understood scripting language with an easily embeddable off-the-shelf open source code base, perhaps one of the first of its kind (ignoring lisps.) Of course the limitations soon became clear. Only a few years later I had high hopes that Python would become a successor, but it went in a different direction and became significantly more difficult to embed in other applications than was TCL -- it just wasn't a primary use case for the core Python project. The modern TCL-equivalent is Lua, definitely a step up from TCL, but I think if EDA tools used Lua there would be plenty of hand-wringing too.
Just guessing, but I imagine that at the time TCL was adopted within EDA tools there were few alternatives. And once TCL was established it was going to be very hard to replace. Even if you ignore inertia at the EDA vendors, I can't imagine hardware engineers (or anyone with a job to do) wanting to switch languages every two to five years like some developers seem happy to do. It's a hard sell all around.
I reckon the best you can do is blame the vendors for (a) not choosing a more fit-for purpose language at the outset, which probably means Scheme, or inventing their own, (b) or not ripping the bandaid off at some point and switching to a more fit-for-purpose language. Blaming (b) is tough though, even today selecting an embedded application language is vexed: you want something that has good affordances as a language, is widely used and documented, easily embedded, and long-term stable. Almost everything I can think of fails the long term stability test (Python, JavaScript, even Lua which does not maintain backward compatibility between releases).
Dead Comment
I'm both bothered and intrigued by the industry returning to, what I call, "pile-of-if-statements architecture". It's really easy to think it's simple, and it's really easy to think you understand, and it's really easy to close your assigned Jira tickets; so I understand why people like it.
People get assigned a task, they look around and find a few places they think are related, then add some if-statements to the pile. Then they test; if the tests fail they add a few more if-statements. Eventually they send it to QA; if QA finds a problem, another quick if-statement will solve the problem. It's released to production, and it works for a high enough percentage of cases that the failure cases don't come to your attention. There's approximately 0% chance the code is actually correct. You just add if-statements until you asymptotically approach correctness. If you accidentally leak the personal data of millions of people, you wont be held responsible, and the cognitive load is always low.
But the thing is... I'm not sure there's a better alternative.
You can create a fancy abstraction and use a fancy architecture, but I'm not sure this actually increases the odds of the code being correct.
Especially in corporate environments--you cannot build a beautiful abstraction in most corporate environments because the owners of the business logic do not treat the business logic with enough care.
"A single order ships to a single address, keep it simple, build it, oh actually, a salesman promised a big customer, so now we need to make it so a single order can ship to multiple addresses"--you've heard something like this before, haven't you?
You can't build careful bug-free abstractions in corporate environments.
So, is pile-of-if-statements the best we can do for business software?
You’ll enjoy the Big Ball of Mud paper[1].
Real world systems are prone to decay. You first of all start with a big ball of mud because you’re building a system before you know what you want. Then as parts of the system grow up, you improve the design. Then things change again and the beautiful abstraction breaks down.
Production software is always changing. That’s the beauty of it. Your job is to support this with a mix of domain modeling, good enough abstraction, and constructive destruction. Like a city that grows from a village.
[1] https://laputan.org/mud/mud.html
[2] my recap (but the paper is very approachable, if long) https://swizec.com/blog/big-ball-of-mud-the-worlds-most-popu...
I think anyone that thinks mudball is OK because business is messy has never seen true mudball code.
I've had to walk out of potential work because after looking at what they had I simply had to tell them I cannot help you, you need a team and probably at minimum a year to make any meaningful progress. That is what mudballs leads to. What this paper describes is competent work that is pushed too quickly for cleaning rough edges but has some sort of structure.
I've seen mudballs that required 6-12 months just to do discovery of all the pieces and parts. Hundreds of different version of things no central source control, different deployment techniques depending on the person that coded it even within the same project.
https://s3.amazonaws.com/systemsandpapers/papers/bigballofmu...
You're right that the business logic is gonna be messy, and that's because nobody really cares, and they can offload the responsibility to developers, or anyone punching it in.
On the other hand, separating "good code" and "bad code" can have horrible outcomes too.
One "solution" I saw in a fintech I worked at, was putting the logic in the hands of business people itself, in the form of a decision engine.
Basically it forced the business itself to maintain its own ball of mud. It was impossible to test, impossible to understand and even impossible simulate. Eventually software operators were hired, basically junior-level developers using a graphical interface for writing the code.
It was rewritten a couple times, always with the same outcome of everything getting messy after two or three years.
Instead, at least one implementer needs to get hands dirty on what the application space really is. Very dirty. So dirty that they actually start to really know and care about what the users actually experience every day.
Or, more realistically for most companies, we insist on separate silos, "business logic" comes to mean "stupid stuff we don't really care about", and we screw around with if statements. (Or, whatever, we get hip to monads and screw around with those. That's way cooler.)
When the problem itself is technical or can be generalised then abstractions can eliminate the need for 1000s of if-statement developers but if the domain itself is messy and poorly specified then the only ways abstractions (and tooling) can help is to bake in flexibility, because contradiction might be a feature not a bug...
IMO a lot of (software) engineering wisdom and best practices fails in the face of business requirements and logic. In hard engineering you can push back a lot harder because it's more permanent and lives are more often on the line, but with software, it's harder to do so.
I truly believe the constraints of fast moving business and inane, non sensical requests for short term gains (to keep your product going) make it nearly impossible to do proper software engineering, and actually require these if-else nests to work properly. So much so that I think we should distinguish between software engineering and product engineering.
They fail on reality. A lot of those "best" practices assume, that someone understands the problem and knows what needs to be built. But that's never true. Building software is always an evolutionary process, it needs to change until it's right.
Try to build an side project, that doesn't accept any external requirements, just your ideas. You will see that even your own ideas and requirements shift over time, a year (or two) later your original assumptions won't be correct anymore.
Add to the fact that they're the professor of many software engineering courses and you start to see why so many new grads follow SOLID so dogmatically, which leads to codebases quickly decaying.
It would just keep adding what it called "heuristics", which were just if statements that tested for a specific condition that arose during the bug. I could write 10 tests for a specific type of bug, and it would happily fix all of them. When I add another one test with the same kind of bug it obviously fails, because the fix that Codex came up with was a bunch of if statements that matched the first 10 tests.
I am convinced this behaviour and the one you described are due to optimising for swe benchmarks that reward 1-shotting fixes without regard to quality. Writing code like this makes complete sense in that context.
Thank you for giving a perfect example of what I was describing.
The thing is, you actually can make the software work this way, you just have to add enough if-statements to handle all cases--or rather, enough cases that the manager is happy.
If you find yourself sprinkling ifs everywhere, try to lift them up, they’ll congregate at the same place eventually, so all of your variability is implemented and documented at a single place, no need to abstract anything.
It’s very useful to model your inputs and outputs precisely. Postpone figuring out unified data types as long as possible and make your programming language nice to use with that decision.
Hierarchies of classes, patterns etc are a last resort for when you’re actually sure you know what’s going on.
I’d go further and say you don’t need functions or files as long as your programming is easy to manage. The only reason why you’d need separate files is if your vcs is crippled or if you’re very sure that these datetime handlers need to be reused everywhere consistently.
Modern fullstack programming is filled with models, middleware, Controllers , views , … as if anyone needs all of that separation up front.
If your code ever has the possibility of changing, your early wins by having no abstraction are quickly paid for, with interest, as you immediately find yourself refactoring to a higher abstraction in order to reason about higher-order concepts.
In this case, the abstraction is the simplicity, for the same reason that when I submit this comment, I don't have to include a dictionary or a definition of every single word I use. There is a reason that experienced programmers reach for abstractions from the beginning, experience has taught them the benefits of doing so.
The mark of an expert is knowing the appropriate level of abstraction for each task, and when to apply specific abstractions. This is also why abstractions can sometimes feel clumsy and indirect to less experienced engineers.
Sometimes last mile software turns into these abstractions but often not.
I’ve worked with very smart devs that try to build these abstractions too early, and once they encounter reality you just have a more confusing version of if statement soup.
Of course, the disadvantage is the exponential growth. 20 ifs means a million cases (usually less because the conditions aren't independent, but still).
Then I have a flat list of all possible cases, and I can reconstruct a minimal if tree if I really want to (or just keep it as a list of cases - much easier to understand that way, even if less efficient).
Making invalid data unrepresentable simplifies so much code. It's not always possible but it is way underused. You can do some of it with object encapsulation too, but simple enums with exhaustive switch statements enforced by the compiler (so if it changes you have to go handle the new case everywhere) is often the better option.
If the abstraction doesn't fit a new problem, it should be easy to reassemble the components in a different way, or use an existing abstraction and replace some components with something that fits this one problem.
The developers shouldn't be forced to use the abstractions, they should voluntarily use them because it makes it easier for them.
An underappreciated value. I call this composability and it is one of my primary software development goals.
Sales contracts with weird conditions and odd packaging and contingencies? Pile of if statements.
The other great model for business logic is a spreadsheet, which is well modeled by SQL which is a superset of spreadsheet functionality.
So piles of if’s and SQL. Yeah.
Elegant functional or OOP models are usually too rigid unless they are scaffolding to make piles of conditions and relational queries easier to run.
One would imagine by now we would have some incredibly readable logical language to use with the SQL on that context...
But instead we have people complaining that SQL is too foreign and insisting we beat it down until it becomes OOP.
To be fair, creating that language is really hard. But then, everybody seems to be focusing on destroying things more, not on constructing a good ecosystem.
It also depends how big the consequences to failure/bugs are. Sometimes bugs just aren't a huge deal, so it's a worthwhile trade-off to make development easier in change for potentially increasing the chance of them appearing.
I'm firmly in the "DI||GTFO" camp, so I don't meant to advocate for the Factory pattern but saying that only abstractions that you like are ok starts to generate PR email threads
I don't see the problem. Okay, so we need to support multiple addresses for orders. We can add a relationship table between the Orders and ShippingAddresses tables, fix the parts of the API that need it so that it still works for all existing code like before using the updated data model, then publish a v2 of the api with updated endpoints that support creating orders with multiple addresses, adding shipping addresses, whatever you need.
Now whoever is dependent on your system can update their software to use the v2 endpoints when they're ready for it. If you've been foolish enough to let other applications connect to your DB directly then those guys are going to have a bad time, might want to fix that problem first if those apps are critical. Or you could try to coordinate the fix across all of them and deploy them together with the db update.
The problems occur when people don't do things properly, we have solutions for these problems. It's just that people love taking shortcuts and that leads to a terrible system full of workarounds rather than abstractions. Abstractions are malleable, you can change them to suit your needs. Use the abstractions that work for you, change them if they don't work any more. Design the code in such a way that changing them isn't a gargantuan task.
Which items ship to each of those locations and in which quantities?
What is the status of each of those sub-orders in the fulfillment process?
Should the orders actually ship to those addresses or should the cartons just be packed and marked for those locations for cross-docking and the shipments should be split across some number of regional DC's based on proximity to the final address?
Many things need to be updated in the DB schema+code. And if you think this isn't a very good example, it's a real life example of orders for large retailers.
I don't think it is malice or incompetence, but this happens too often to feel good.
These individual functions are easier to reason about since they have specific use cases, you don't have to remember which combinations of conditions happen together while reading the code, they simplify control flow (i.e. you don't have to hack around carrying data from one if block to the next), and it uses no "abstraction" (interfaces) just simple functions.
It's obviously a balance, you'll still have some if statements, but getting rid of mutually exclusive conditions is basically a guaranteed improvement.
Certainly, there are such people who simply don't care.
However I would also say that corporations categorically create an environment where you are unable to care - consider how short software engineer tenures are! Any even somewhat stable business will likely have had 3+ generations of owner by the time you get to them. Owner 1 is the guy who wrote 80% of the code in the early days, fast and loose, and got the company to make payroll. Owner 2 was the lead of a team set up to own that service plus 8 others. Owner 3 was a lead of a sub-team that split off from that team and owns that service plus 1 other related service.
Each of these people will have different styles - owner 1 hated polymorphism and everything is component-based, owner 2 wrapped all existing logic into a state machine, owner 3 realized both were leaky abstractions and difficult to work with, so they tried to bring some semblance of a sustainable path forward to the system, but were busy with feature work. And owner 3 did not get any Handoff from person 2 because person 2 ragequit the second enough of their equity vested. And now there's you. You started about 9 months ago and know some of the jargon and where some bodies are buried. You're accountable for some amount of business impact, and generally can't just go rewrite stuff. You also have 6 other people on call for this service with you who have varying levels of familiarity with the current code. You have 2.25 years left. Good luck.
Meanwhile I've seen codebases owned by the same 2 people for over 10 years. It's night and day.
I once tried to explain to a product owner that we should be careful to document what assumptions are being made in the code, and make sure the company was okay committing to those assumptions. Things like "a single order ships to a single address" are early assumptions that can get baked into the system and can be really hard to change later, so the company should take care and make sure the assumptions the programmers are baking into the system are assumptions the company is willing to commit to.
Anyway, I tried to explain all this to the product owner, and their response was "don't assume anything". Brillant decisions like that are why they earned the big bucks.
Employees aren’t fired. They leave for a 10% increase. Employees are the ones who seek always more in a short-termist way.
The model of having a circle of ancient greybeards in charge of carefully updating the sacred code to align with the business requirements, while it seems bizarre bordering on something out of WH40K, actually works pretty well and has worked pretty well everywhere I've encountered it.
Attempts to refactor or replace these systems with something more modern has universally been an expensive disaster.
Project Manager: "Can we ship an order to multiple addresses?"
Grey Beard: "No. We'd have to change thousands of random if-statements spread throughout the code."
Project Manager: "How long do you think that would take?"
Grey Beard: "2 years or more."
Project Manager: "Okay, we will break you down--err, I mean, we'll need to break the task down. I'll schedule long meetings until you relent and commit to a shorter time estimate."
Grey Beard eventually relents and gives a shorter time estimate for the project, and then leaves the company for another job that pays more half-way through the project.
The way I've been thinking about it is about organization. Organize code like we should organize our house. If you have a collection of pens, I guess you shouldn't leave them scattered everywhere and in your closet, and with your cutlery, and in the bathroom :) You should set up somewhere to keep your pens, and other utensils in a kind of neat way. You don't need to spend months setting up a super-pen-organizer that has a specially sculpted nook for your $0.50 pen that you might lose or break next week. But you make it neat enough, according to a number of factors like how likely it is to last, how stable is your setup, how frequently it is used, and so on. Organizing has several advantages: it makes it easier to find pens, shows you a breath of options quickly, keeps other places in your house tidier and so less cognitively messy as well. And it has downsides, like you need to devote a lot of time and effort, you might lose flexibility if you're too strict like maybe you've been labeling stuff in the kitchen, or doing sketches in your living room, and you need a few pens there.
I don't like the point of view that messiness (and say cognitive load) is always bad. Messiness has real advantages sometimes! It gives you freedom to be more flexible and dynamic. I think children know this when living in a strict "super-tidy" parent house :) (they'd barely get the chance to play if everything needs to be perfectly organized all the time)
I believe in real life almost every solution and problem is strongly multifactorial. It's dangerous to think a single factor, say 'cognitive load', 'don't repeat yourself', 'lesser lines of code', and so on is going to be the single important factor you should consider. Projects have time constraints, cost, need for performance; expressing programs, the study of algorithms and abstractions itself is a very rich field. But those single factors help us improve a little on one significant facet of your craft if you're mindful about it.
Another factor I think is very important as well (and maybe underestimated) is beauty. Beauty for me has two senses: one in an intuitive sense that things are 'just right' (which capture a lot of things implicitly). A second and important one I think is that working and programming, when possible, should be nice, why not. The experience of coding should be fun, feel good in various ways, etc. when possible (obviously this competes with other demands...). When I make procedural art projects, I try to make the code at least a little artistic as well as the result, I think it contributes to the result as well.
[1] a few small projects, procedural art -- and perhaps a game coming soon :)
I'm not sure if that's anywhere in the rating of quality of business software. Things that matter:
1. How fast can I or someone else change it next time to fulfill the next requirements?
2. How often does it fail?
3. How much money does the code save or generate by existing.
Good architecture can affect 1 and 2 in some circumstances but not every time and most likely not forever at the rate people are starting to produce LLM garbage code. At some point we'll just compile English directly into bytecode and so architecture will matter even less. And obviously #3 matters by far the most.
It's obviously a shame for whoever appreciates the actual art / craft of building software, but that isn't really a thing that matters in business software anyway, at least for the people paying our salaries (or to the users of the software).
While the castle of cards of unfathomable complexity is praised for visibly hard work and celebrated with promotions.
this is also a problem for tools designed for non-technical users for complex tasks that are performed frequently. your power users needs a powerful interface even if they are less technical.
In tradeoff engineering, maintainability over the long term is one of the many variables to optimize, and finite resources need to be alloted to it.
When I read this article I get the feeling that it's more likely that he is obsessing over maintainability over the long term while his app has a user count of zero. This malady usually comes from the perspective of being a user, one finds that the experience of writing some code is a "bad experience" so they strive to improve it or learn how to build a good "coder experience", the right answer is to understand that one is stepping into the shoes of the plumber, and it will be shitty, just gotta roll up your sleeves.
Don't get me wrong, there's a lot of wisdom here, but to the extent that there is, it's super derivative and well established, it's just the kind of stuff that a developer learns on their first years of software by surfing the web and learning about DRY, KISS and other folklore of software. To some extent this stuff is useful, but there's diminishing returns and at some point you have to throw shit and focus on the product instead of obsessing over the code.
My favourite frameworks are written by people smart enough to know they're not smart enough to build the eternal perfect abstraction layers and include 'escape hatches' (like getting direct references to html elements in a web UI framework etc) in their approach so you're not screwed when it turns out they didn't have perfect future-sight.
But even if the intended solution seemed simpler, it could be much harder to discover it.
Because of some quirk of the way my brain works, giant functions with thousands of lines of code doesn't really present a high cognitive load for me, while lots of smaller functions do. My "working memory" is very low (so I have trouble seeing the "big picture" while hopping from function to function), while "looking through a ton of text" comes relatively easily to me.
I have coworkers who tend to use functional programming, and even though it's been years now and I technically understand it, it always presents a ton of friction for me, where I have to stop and spend a while figuring out exactly what the code is saying (and "mentally translating" it into a form that makes more sense to me). I don't think this is necessarily because their code inherently presents a higher cognitive load - I think it's easier for them to mentally process it, while my brain has an easier time with looking at a lot of lines of code, provided the logic within is very simple.
Microsoft had three personas for software engineers that were eventually retired for a much more complex persona framework called people in context (the irony in relation to this article isn’t lost on me).
But those original personas still stick with me and have been incredibly valuable in my career to understand and work effectively with other engineers.
Mort - the pragmatic engineer who cares most about the business outcome. If a “pile of if statements” gets the job done quickly and meets the requirements - Mort became a pejorative term at Microsoft unfortunately. VB developers were often Morts, Access developers were often Morts.
Elvis - the rockstar engineer who cares most about doing something new and exciting. Being the first to use the latest framework or technology. Getting visibility and accolades for innovation. The code might be a little unstable - but move fast and break things right? Elvis also cares a lot about the perceived brilliance of their code - 4 layers of abstraction? That must take a genius to understand and Elvis understands it because they wrote it, now everyone will know they are a genius. For many engineers at Microsoft (especially early in career) the assumption was (and still is largely) that Elvis gets promoted because Elvis gets visibility and is always innovating.
Einstein - the engineer who cares about the algorithm. Einstein wants to write the most performant, the most elegant, the most technically correct code possible. Einstein cares more if they are writing “pythonic” code than if the output actually solves the business problem. Einstein will refactor 200 lines of code to add a single new conditional to keep the codebase consistent. Einsteins love love love functional languages.
None of these personas represent a real engineer - every engineer is a mix, and a human with complex motivations and perspectives - but I can usually pin one of these 3 as the primary within a few days of PRs and a single design review.
- Mort wants to climb the business ladder.
- Elvis wants earned social status.
- Einstein wants legacy with unique contributions.
- Amanda just wants group cohesion and minimizing future unpredictability.
If there is no inherent complexity, a Mort will come up with the simplest solution. If it's a complex problem needing trade-offs the Mort will come up with the fastest and most business centric solution.
Or would you see that Amanda refactoring a whole system to keep it simple above all whatever the deadlines and stakes ?
Elvis: A famous rock star
Enstein: A famous physicist
Amanda: ???
Mort, Elvis, Enstein are referencing things I've heard of before. What is Amanda referencing? is there some famous person named Amanda? Is it slang I'm unaware of?
Am I missing a reference? If not, may I suggest “Ada”?
https://en.wikipedia.org/wiki/Ada_Lovelace
Or even better, “Grace”. Seems to fit your description better.
https://en.wikipedia.org/wiki/Grace_Hopper
https://www.youtube.com/watch?v=gYqF6-h9Cvg
I see the ideal as a combination of Mort and Einstein that want to keep it simple enough that it can be delivered (less abstraction, distilled requirements) while ensuring the code is sufficiently correct (not necessarily "elegant" mind you) that maintenance and support won't be a total nightmare.
IMO, seek out Morts and give them long term ownership of the project so they get a little Einstein-y when they realize they need to support that "pile of if statements".
As an aside, I'm finding coding agents to be a bit too much Mort at times (YOLO), when I'd prefer they were more Einstein. I'd rather be the Mort myself to keep it on track.
Sometimes teams are quite stuck in their ways because they don’t have the capacity or desire to explore anything new.
For example, an Elvis would probably introduce containers which would eliminate a class of dependency and runtime environment related issues, alongside allowing CI to become easier and simpler, even though previously using SCP and Jenkins and deploying things into Tomcat mostly worked. Suddenly even the front end components can be containers, as can be testing and development databases, everyone can easily have the correct version locally and so on.
An unchecked Elvis will eventually introduce Kubernetes in the small shop to possibly messy results, though.
I spent time at Microsoft as well, and one of the things I noticed was folks who spent time in different disciplines (e.g. dev, test, pgm) seemed to be especially great at tailoring these qualities to their needs. If you're working on optimizing a compiler, you probably need a bit more Einstein and Mort than Elvis. If you're working on a game engine you may need a different combination.
The quantities of each (or whether these are the correct archetypes) is certainly debatable, but understanding that you need all of them in different proportions over time is important, IMHO.
Elvis is not a persona - it is an inside baseball argument to management. It suffered a form of Goodhart’s law … it is a useful tool so people phrase their arguments in that form to win a biz fight and then the tool degrades.
Alan Cooper, who created VB advocated personas. When used well they are great.
The most important insight is your own PoV may be flawed. The way a scientist provides value via software is different than how a firmware developer provides value.
https://www.amazon.com/Inmates-Are-Running-Asylum/dp/0672316...
Also the actual solution is proper team leadership/management. If you have morts, make sure that code quality requirements are a PART of the requirements their code must pass, and they’ll instead deliver decent work slightly slower. Got an elvis? Give more boundaries. Got Einsteins? Redefine the subtasks so they can’t refactor everything and give deadlines both in terms of time but also pragmatism.
Either way, I don’t love this approach, as it removes the complexity from the human condition, complexity which is most important to keep in mind.
Life is all about learning, adapting and changing. Great leaders see the potential growth in people and are up for having hard conversations about how they can improve.
Even if people do have these personality traits as life long attributes, that doesn't define them or prevent them from learning aspects of the others over time.
https://mitpress.mit.edu/9780262543798/the-plenitude/
"so, there's 3 boxes. no more, no less. why? i have a gut feeling. axis? on a case by case basis. am i willing to put my money where my mouth is? heallnaw!"
Mort == maker Elvis ==? hacker Einstein == poet
Something was bugging me after an interview with a potential hire, and now I can articulate that they were too much Einstein and not enough Mort for the role.
Dead Comment
elvis = thief
einstein = mage
The kind of psycho-bullshit that we should stay away from, and wouldn't happen if we respected each other. Coming from Microsoft is not surprising though.
The second component, frequency of change is equally important as when faced with tradeoffs, we can push high cognitive load to components edited less frequently (eg: lower down the stack) in exchange for lower cognitive load in the most frequently edited components.
It's also why I urge junior engineers to not rely on AI so much because even though it makes writing code so much faster, it prevents them from learning the quirks of the codebase and eventually they'll lose the ability to write code on their own.
I am afraid, the cat is out the bag, and there is no turning back with GenAI and coding – juniors have got a taste of GenAI assisted coding and will persevere. The best we can do it educate them on how to use it correctly and responsibly.
The approach I have taken involves small group huddles where we talk to each other as equals, and where I emphasise the importance of understanding the problem space, the importance of the depth and breadth of knowledge, i.e. going across the problem domain – as opposed to focusing on a narrow part of it. I do not discourage the junior engineers from using GenAI, but I stress the liability factor and the cost: «if you use GenAI to write code, and the code falls apart in production, you will have a hard time supporting it if you do not understand the generated code, so choose your options wisely». I also highlight the importance of simplicity over complexity of the design and implementation, and that simplicity is hard, although it is something we should strive for as an aspiration and as a delivery target.
I reflect on and adjust the approach based on new observations, feedback loop (commits) and other indirect signs and metrics – this area is still new, and the GenAI assisted coding framework is still fledging.
It's writing something for me, not for itself.
My current approach is creating something like a Gem on Gemini with custom instructions and the updated source code of the project as context.
I just discuss what I want, and it gives me the code to do it, then I write by hand, ask for clarifications and suggest changes until I feel like the current approach is actually a good one. So not really “vibe-coding”, though I guess a large number of software developers who care about keeping the project sane must be doing this.
AI can fix it
I'm not defending or encouraging AI, just saying that argument doesn't work
It's been well documented that LLMs collapse after a certain complexity level.
Dead Comment
So in software development there may be an argument to always structure projects the same way. Standards are good — even when they're bad! because one of their main benefit is familiarity.
by doing something better here would actually not bring any value, because it would mean that developers would have to remember that this one thing is done differently.
that's trap where I would say many mid Devs fall in, they learned how do things better, but increase congnitive load for the rest of developers just by doing things differently.
It's an important distinction in terms of priorities. I personally think the experience of the user is orders of magnitude more important than engineer cognitive load.