Cognitive load is what matters

2024-12-2222:181769721minds.md

There are so many buzzwords and best practices out there, but let's focus on something more fundamental. What matters is the amount of confusion developers feel when going through the code.

The logo image was taken from Reddit.

It is a living document, last update: November 2024. Your contributions are welcome!

Introduction

There are so many buzzwords and best practices out there, but let's focus on something more fundamental. What matters is the amount of confusion developers feel when going through the code.

Confusion costs time and money. Confusion is caused by high cognitive load. It's not some fancy abstract concept, but rather a fundamental human constraint.

Since we spend far more time reading and understanding code than writing it, we should constantly ask ourselves whether we are embedding excessive cognitive load into our code.

Cognitive load

Cognitive load is how much a developer needs to think in order to complete a task.

When reading code, you put things like values of variables, control flow logic and call sequences into your head. The average person can hold roughly four such chunks in working memory. Once the cognitive load reaches this threshold, it becomes much harder to understand things.

Let's say we have been asked to make some fixes to a completely unfamiliar project. We were told that a really smart developer had contributed to it. Lots of cool architectures, fancy libraries and trendy technologies were used. In other words, the author had created a high cognitive load for us.

Cognitive Load

We should reduce the cognitive load in our projects as much as possible.

Types of cognitive load

Intrinsic - caused by the inherent difficulty of a task. It can't be reduced, it's at the very heart of software development.

Extraneous - created by the way the information is presented. Caused by factors not directly relevant to the task, such as smart author's quirks. Can be greatly reduced. We will focus on this type of cognitive load.

Intrinsic vs Extraneous

Let's jump straight to the concrete practical examples of extraneous cognitive load.

We will refer to the level cognitive load as follows:
🧠: fresh working memory, zero cognitive load
🧠++: two facts in our working memory, cognitive load increased
🤯: cognitive overload, more than 4 facts

Our brain is much more complex and unexplored, but we can go with this simplistic model.

Complex conditionals

if

val

>

someConstant && (condition2

||

condition3) && (condition4

&&

!

condition5) { ... }

Introduce intermediate variables with meaningful names:

isValid

= val

>

someConstant isAllowed

=

condition2

||

condition3 isSecure

=

condition4

&&

!

condition5

if

isValid

&&

isAllowed

&&

isSecure { ... }

Nested ifs

if

isValid {

if

isSecure { stuff // 🧠+++ } }

Compare it with the early returns:

if

!

isValid

return

if

!

isSecure

return

stuff

We can focus on the happy path only, thus freeing our working memory from all sorts of preconditions.

Inheritance nightmare

We are asked to change a few things for our admin users: 🧠

AdminController 

extends

UserController

extends

GuestController

extends

BaseController

Ohh, part of the functionality is in BaseController, let's have a look: 🧠+
Basic role mechanics got introduced in GuestController: 🧠++
Things got partially altered in UserController: 🧠+++
Finally we are here, AdminController, let's code stuff! 🧠++++

Oh, wait, there's SuperuserController which extends AdminController. By modifying AdminController we can break things in the inherited class, so let's dive in SuperuserController first: 🤯

Prefer composition over inheritance. We won't go into detail - there's plenty of material out there.

Too many small methods, classes or modules

Method, class and module are interchangeable in this context

Mantras like "methods should be shorter than 15 lines of code" or "classes should be small" turned out to be somewhat wrong.

Deep module - simple interface, complex functionality
Shallow module - interface is relatively complex to the small functionality it provides

Deep module

Having too many shallow modules can make it difficult to understand the project. Not only do we have to keep in mind each module responsibilities, but also all their interactions. To understand the purpose of a shallow module, we first need to look at the functionality of all the related modules. 🤯

Information hiding is paramount, and we don't hide as much complexity in shallow modules.

I have two pet projects, both of them are somewhat 5K lines of code. The first one has 80 shallow classes, whereas the second one has only 7 deep classes. I haven't been maintaining any of these projects for one year and a half.

Once I came back, I realised that it was extremely difficult to untangle all the interactions between those 80 classes in the first project. I would have to rebuild an enormous amount of cognitive load before I could start coding. On the other hand, I was able to grasp the second project quickly, because it had only a few deep classes with a simple interface.

The best components are those that provide powerful functionality yet have simple interface.
John K. Ousterhout

The interface of the UNIX I/O is very simple. It has only five basic calls:

open

(path, flags, permissions)

read

(fd, buffer, count)

write

(fd, buffer, count)

lseek

(fd, offset, referencePosition)

close

(fd)

A modern implementation of this interface has hundreds of thousands of lines of code. Lots of complexity is hidden under the hood. Yet it is easy to use due to its simple interface.

This deep module example is taken from the book A Philosophy of Software Design by John K. Ousterhout. Not only does this book cover the very essence of complexity in software development, but it also has the greatest interpretation of Parnas' influential paper On the Criteria To Be Used in Decomposing Systems into Modules. Both are essential reads. Other related readings: It's probably time to stop recommending Clean Code, Small Functions considered Harmful.

P.S. If you think we are rooting for bloated God objects with too many responsibilities, you got it wrong.

Shallow modules and SRP

All too often, we end up creating lots of shallow modules, following some vague "a module should be responsible for one, and only one, thing" principle. What is this blurry one thing? Instantiating an object is one thing, right? So MetricsProviderFactoryFactory seems to be just fine. The names and interfaces of such classes tend to be more mentally taxing than their entire implementations, what kind of abstraction is that? Something went wrong.

Jumping between such shallow components is mentally exhausting, linear thinking is more natural to us humans.

We make changes to our systems to satisfy our users and stakeholders. We are responsible to them.

A module should be responsible to one, and only one, user or stakeholder.

This is what this Single Responsibility Principle is all about. Simply put, if we introduce a bug in one place, and then two different business people come to complain, we've violated the principle. It has nothing to do with the number of things we do in our module.

But even now, this interpretation can do more harm than good. This rule can be understood in as many different ways as there are individuals. A better approach would be to look at how much cognitive load it all creates. It's mentally demanding to remember that change in one module can trigger a chain of reactions across different business streams. And that's about it.

Too many shallow microservices

This shallow-deep module principle is scale-agnostic, and we can apply it to microservices architecture. Too many shallow microservices won't do any good - the industry is heading towards somewhat "macroservices", i.e., services that are not so shallow (=deep). One of the worst and hardest to fix phenomena is so-called distributed monolith, which is often the result of this overly granular shallow separation.

I once consulted a startup where a team of five developers introduced 17(!) microservices. They were 10 months behind schedule and appeared nowhere close to the public release. Every new requirement led to changes in 4+ microservices. Diagnostic difficulty in integration space skyrocketed. Both time to market and cognitive load were unacceptably high. 🤯

Is this the right way to approach the uncertainty of a new system? It's enormously difficult to elicit the right logical boundaries in the beginning. The key is to make decisions as late as you can responsibly wait, because that is when you have the most information on which to base the decision. By introducing a network layer up front, we make our design decisions hard to revert right from the start. The team's only justification was: "The FAANG companies proved microservices architecture to be effective". Hello, you got to stop dreaming big.

The Tanenbaum-Torvalds debate argued that Linux's monolithic design was flawed and obsolete, and that a microkernel architecture should be used instead. Indeed, the microkernel design seemed to be superior "from a theoretical and aesthetical" point of view. On the practical side of things - three decades on, microkernel-based GNU Hurd is still in development, and monolithic Linux is everywhere. This page is powered by Linux, your smart teapot is powered by Linux. By monolithic Linux.

A well-crafted monolith with truly isolated modules is often much more flexible than a bunch of microservices. It also requires far less cognitive effort to maintain. It's only when the need for separate deployments becomes crucial, such as scaling the development team, that you should consider adding a network layer between the modules, future microservices.

Feature-rich languages

We feel excited when new features got released in our favourite language. We spend some time learning these features, we build code upon them.

If there are lots of features, we may spend half an hour playing with a few lines of code, to use one or another feature. And it's kind of a waste of time. But what's worse, when you come back later, you would have to recreate that thought process!

You not only have to understand this complicated program, you have to understand why a programmer decided this was the way to approach a problem from the features that are available. 🤯

These statements are made by none other than Rob Pike.

Reduce cognitive load by limiting the number of choices.

Language features are OK, as long as they are orthogonal to each other.

Thoughts from an engineer with 20 years of C++ experience ā­ļø

I was looking at my RSS reader the other day and noticed that I have somewhat three hundred unread articles under the "C++" tag. I haven't read a single article about the language since last summer, and I feel great!

I've been using C++ for 20 years for now, that's almost two-thirds of my life. Most of my experience lies in dealing with the darkest corners of the language (such as undefined behaviours of all sorts). It's not a reusable experience, and it's kind of creepy to throw it all away now.

Like, can you imagine, the token || has a different meaning in requires ((!P<T> || !Q<T>)) and in requires (!(P<T> || Q<T>)). The first is the constraint disjunction, the second is the good-old logical or operator, and they behave differently.

You can't allocate space for a trivial type and just memcpy a set of bytes there without extra effort - that won't start the lifetime of an object. This was the case before C++20. It was fixed in C++20, but the cognitive load of the language has only increased.

Cognitive load is constantly growing, even though things got fixed. I should know what was fixed, when it was fixed, and what it was like before. I am a professional after all. Sure, C++ is good at legacy support, which also means that you will face that legacy. For example, last month a colleague of mine asked me about some behaviour in C++03. 🤯

There were 20 ways of initialization. Uniform initialization syntax has been added. Now we have 21 ways of initialization. By the way, does anyone remember the rules for selecting constructors from the initializer list? Something about implicit conversion with the least loss of information, but if the value is known statically, then... 🤯

This increased cognitive load is not caused by a business task at hand. It is not an intrinsic complexity of the domain. It is just there due to historical reasons (extraneous cognitive load).

I had to come up with some rules. Like, if that line of code is not as obvious and I have to remember the standard, I better not write it that way. The standard is somewhat 1500 pages long, by the way.

By no means I am trying to blame C++. I love the language. It's just that I am tired now.

Business logic and HTTP status codes

On the backend we return:

  • 401 for expired jwt token
  • 403 for not enough access
  • 418 for banned users

The guys on the frontend use backend API to implement login functionality. They would have to temporarily create the following cognitive load in their brains:

  • 401 is for expired jwt token // 🧠+, ok just temporary remember it
  • 403 is for not enough access // 🧠++
  • 418 is for banned users // 🧠+++

Frontend developers would (hopefully) introduce some kind numeric status -> meaning dictionary on their side, so that subsequent generations of contributors wouldn't have to recreate this mapping in their brains.

Then QA people come into play: "Hey, I got 403 status, is that expired token or not enough access?" QA people can't jump straight to testing, because first they have to recreate the cognitive load that the guys on the backend once created.

Why hold this custom mapping in our working memory? It's better to abstract away your business details from the HTTP transfer protocol, and return self-descriptive codes directly in the response body:

{ 

"code"

:

"jwt_has_expired"

}

Cognitive load on the frontend side: 🧠 (fresh, no facts are held in mind)
Cognitive load on the QA side: 🧠

The same rule applies to all sorts of numeric statuses (in the database or wherever) - prefer self-describing strings. We are not in the era of 640K computers to optimise for memory.

People spend time arguing between 401 and 403, making decisions based on their own mental models. New developers are coming in, and they need to recreate that thought process. You may have documented the "whys" (ADRs) for your code, helping newcomers to understand the decisions made. But in the end it just doesn't make any sense. We can separate errors into either user-related or server-related, but apart from that, things are kind of blurry.

P.S. It's often mentally taxing to distinguish between "authentication" and "authorization". We can use simpler terms like "login" and "permissions" to reduce the cognitive load.

Abusing DRY principle

Do not repeat yourself - that is one of the first principles you are taught as a software engineer. It is so deeply embedded in ourselves that we can not stand the fact of a few extra lines of code. Although in general a good and fundamental rule, when overused it leads to the cognitive load we can not handle.

Nowadays, everyone builds software based on logically separated components. Often those are distributed among multiple codebases representing separate services. When you strive to eliminate any repetition, you might end up creating tight coupling between unrelated components. As a result changes in one part may have unintended consequences in other seemingly unrelated areas. It can also hinder the ability to replace or modify individual components without impacting the entire system. 🤯

In fact, the same problem arises even within a single module. You might extract common functionality too early, based on perceived similarities that might not actually exist in the long run. This can result in unnecessary abstractions that are difficult to modify or extend.

Rob Pike once said:

A little copying is better than a little dependency.

We are tempted to not reinvent the wheel so strong that we are ready to import large, heavy libraries to use a small function that we could easily write by ourselves.

All your dependencies are your code. Going through 10+ levels of stack trace of some imported library and figuring out what went wrong (because things go wrong) is painful.

Tight coupling with a framework

There's a lot of "magic" in frameworks. By relying too heavily on a framework, we force all upcoming developers to learn that "magic" first. It can take months. Even though frameworks enable us to launch MVPs in a matter of days, in the long run they tend to add unnecessary complexity and cognitive load.

Worse yet, at some point frameworks can become a significant constraint when faced with a new requirement that just doesn't fit the architecture. From here onwards people end up forking a framework and maintaining their own custom version. Imagine the amount of cognitive load a newcomer would have to build (i.e. learn this custom framework) in order to deliver any value. 🤯

By no means do we advocate to invent everything from scratch!

We can write code in a somewhat framework-agnostic way. The business logic should not reside within a framework; rather, it should use the framework's components. Put a framework outside of your core logic. Use the framework in a library-like fashion. This would allow new contributors to add value from day one, without the need of going through debris of framework-related complexity first.

Why I Hate Frameworks

Layered architecture

There is a certain engineering excitement about all this stuff.

I myself was a passionate advocate of Hexagonal/Onion Architecture for years. I used it here and there and encouraged other teams to do so. The complexity of our projects went up, the sheer number of files alone had doubled. It felt like we were writing a lot of glue code. On ever changing requirements we had to make changes across multiple layers of abstractions, it all became tedious. 🤯

Abstraction is supposed to hide complexity, here it just adds indirection. Jumping from call to call to read along and figure out what goes wrong and what is missing is a vital requirement to quickly solve a problem. With this architecture’s layer uncoupling it requires an exponential factor of extra, often disjointed, traces to get to the point where the failure occurs. Every such trace takes space in our limited working memory. 🤯

This architecture was something that made intuitive sense at first, but every time we tried applying it to projects it made a lot more harm than good. In the end, we gave it all up in favour of the good old dependency inversion principle. No port/adapter terms to learn, no unnecessary layers of horizontal abstractions, no extraneous cognitive load.

If you think that such layering will allow you to quickly replace a database or other dependencies, you're mistaken. Changing the storage causes lots of problems, and believe us, having some abstractions for the data access layer is the least of your worries. At best, abstractions can save somewhat 10% of your migration time (if any), the real pain is in data model incompatibilities, communication protocols, distributed systems challenges, and implicit interfaces.

With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody.

The law of implicit interfaces

We did a storage migration, and that took us about 10 months. The old system was single-threaded, so the exposed events were sequential. All our systems depended on that observed behaviour. This behavior was not part of the API contract, it was not reflected in the code. A new distributed storage didn't have that guarantee - the events came out-of-order. We spent only a few hours coding a new storage adapter. We spent the next 10 months on dealing with out-of-order events and other challenges. It's now funny to say that layering helps us replace components quickly.

So, why pay the price of high cognitive load for such a layered architecture, if it doesn't pay off in the future? Plus, in most cases, that future of replacing some core component never happens.

These architectures are not fundamental, they are just subjective, biased consequences of more fundamental principles. Why rely on those subjective interpretations? Follow the fundamental rules instead: dependency inversion principle, cognitive load and information hiding. Discuss.

Do not add layers of abstractions for the sake of an architecture. Add them whenever you need an extension point that is justified for practical reasons. Layers of abstraction aren't free of charge, they are to be held in our working memory.

DDD

Domain-driven design has some great points, although it is often misinterpreted. People say "We write code in DDD", which is a bit strange, because DDD is about problem space, not about solution space.

Ubiquitous language, domain, bounded context, aggregate, event storming are all about problem space. They are meant to help us learn the insights about the domain and extract the boundaries. DDD enables developers, domain experts and business people to communicate effectively using a single, unified language. Rather than focusing on these problem space aspects of DDD, we tend to emphasise particular folder structures, services, repositories, and other solution space techniques.

Chances are that the way we interpret DDD is likely to be unique and subjective. And if we build code upon this understanding, i.e., if we create a lot of extraneous cognitive load - future developers are doomed. 🤯

Examples

These architectures are quite boring and easy to understand. Anyone can grasp them without much mental effort.

Involve junior developers in architecture reviews. They will help you to identify the mentally demanding areas.

Cognitive load in familiar projects

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!

It is likely that the previous author(s) created this huge mess one tiny increment at a time, not all at once. So you are the first person who has ever had to try to make sense of it all at once.

In my class I describe a sprawling SQL stored procedure we were looking at one day, with hundreds of lines of conditionals in a huge WHERE clause. Someone asked how anyone could have let it get this bad. I told them: ā€œWhen there are only 2 or 3 conditionals, adding another one doesn’t make any difference. By the time there are 20 or 30 conditionals, adding another one doesn’t make any difference!ā€

There is no ā€œsimplifying forceā€ acting on the code base other than deliberate choices that you make. Simplifying takes effort, and people are too often in a hurry.

Thanks to Dan North for his comment.

If you've internalized the mental models of the project into your long-term memory, you won't experience a high cognitive load.

Mental Models

The more mental models there are to learn, the longer it takes for a new developer to deliver value.

Once you onboard new people on your project, try to measure the amount of confusion they have (pair programming may help). If they're confused for more than ~40 minutes in a row - you've got things to improve in your code.

If you keep the cognitive load low, people can contribute to your codebase within the first few hours of joining your company.

Conclusion

Imagine for a moment that what we inferred in the second chapter isn’t actually true. If that’s the case, then the conclusion we just negated, along with the conclusions in the previous chapter that we had accepted as valid, might not be correct either. 🤯

Do you feel it? Not only do you have to jump all over the article to get the meaning (shallow modules!), but the paragraph in general is difficult to understand. We have just created an unnecessary cognitive load in your head. Do not do this to your colleagues.

Smart Author

We should reduce any cognitive load above and beyond what is intrinsic to the work we do.


Read the original article

Comments

  • By K0nserv 2024-12-2521:2626 reply

    I've been thinking about the notion of "reasoning locally" recently. Enabling local reasoning is the only way to scale software development past some number of lines or complexity. When reasoning locally, one only needs to understand a small subset, hundreds of lines, to safely make changes in programs comprising millions.

    I find types helps massively with this. A function with well-constrained inputs and outputs is easy to reason about. One does not have to look at other code to do it. However, programs that leverage types effectively are sometimes construed as having high cognitive load, when it in fact they have low load. For example a type like `Option<HashSet<UserId>>` carries a lot of information(has low load): we might not have a set of user ids, but if we do they are unique.

    The discourse around small functions and the clean code guidelines is fascinating. The complaint is usually, as in this post, that having to go read all the small functions adds cognitive load and makes reading the code harder. Proponents of small functions argue that you don't have to read more than the signature and name of a function to understand what it does; it's obvious what a function called last that takes a list and returns an optional value does. If someone feels compelled to read every function either the functions are poor abstractions or the reader has trust issues, which may be warranted. Of course, all abstractions are leaky, but perhaps some initial trust in `last` is warranted.

    • By 0xFACEFEED 2024-12-2522:1114 reply

      > A function with well-constrained inputs and outputs is easy to reason about.

      It's quite easy to imagine a well factored codebase where all things are neatly separated. If you've written something a thousand times, like user authentication, then you can plan out exactly how you want to separate everything. But user authentication isn't where things get messy.

      The messy stuff is where the real world concepts need to be transformed into code. Where just the concepts need to be whiteboarded and explained because they're unintuitive and confusing. Then these unintuitive and confusing concepts need to somehow described to the computer.

      Oh, and it needs to be fast. So not only do you need to model an unintuitive and confusing concept - you also need to write it in a convoluted way because, for various annoying reasons, that's what performs best on the computer.

      Oh, and in 6 months the unintuitive and confusing concept needs to be completely changed into - surprise, surprise - a completely different but equally unintuitive and confusing concept.

      Oh, and you can't rewrite everything because there isn't enough time or budget to do that. You have to minimally change the current uintuitive and confusing thing so that it works like the new unintuitive and confusing thing is supposed to work.

      Oh, and the original author doesn't work here anymore so no one's here to explain the original code's intent.

      • By justinram11 2024-12-261:282 reply

        > Oh, and the original author doesn't work here anymore so no one's here to explain the original code's intent.

        To be fair, even if I still work there I don't know that I'm going to be of much help 6 months later other than a "oh yeah, I remember that had some weird business requirements"

        • By stouset 2024-12-262:185 reply

          Might I recommend writing those weird business requirements down as comments instead of just hoping someone will guess them six months down the line?

          • By mnsc 2024-12-267:224 reply

            So even if comments are flawlessly updated they are not a silver bullet. Not everyone are good at explaining confusing concepts in plain English so worst case you have confusing code and a comment that is 90% accurate but describe one detail in a way that doesn't really match what the code says. This will make you question if you have understood what the code does and it will take time and effort to convince yourself that code is in fact deterministic and unsurprising.

            (but most often the comment is is just not updated or updated along with the code but without full understanding, which is what caused the bug that is the reason you are looking at the code in question)

            • By michaelcampbell 2024-12-2612:583 reply

              > So even if comments are flawlessly updated they are not a silver bullet.

              This "has to be perfect in perpetuity or it is of no value" mentality I don't find helpful.

              Be kind to FutureDev. Comment the weird "why"s. If you need to change it later, adjust the comment.

              • By mnsc 2024-12-2614:561 reply

                I don't think comments need to be perfect to have value. My point was that if a certain piece of code is solving a particularly confusing problem in the domain, explaining it in a comment doesn't _necessarily_ mean the code will be less confusing to future dev if the current developer is not able to capture the issue in plain English. Future dev would be happier I think with putting more effort into refactoring and making the code more readable and clear. When that fails, a "here be dragons" comment is valuable.

                • By MichaelZuo 2024-12-2615:293 reply

                  They can write a very long comment explaining why it is confusing them in X, Y, Z vague ways. Or even multilingual comments if they have better writing skills in another lanaguage.

                  And even if they don’t know themselves why they are confused, they can still describe how they are confused.

                  • By mnsc 2024-12-2618:491 reply

                    And that time spent writing a small paper in one's native language would be better spent trying to make the code speak for itself. Maybe get some help, pair up and tackle the complexity. And when both/all involved is like, we can't make this any clearer and it's still confusing af. _Then_ it's time to write that lengthy comment for future poor maintainers.

                    • By Ma8ee 2024-12-2712:391 reply

                      You can only do the ā€œwhatā€ with clearer code. The ā€œwhyā€ needs some documentation. Even if it is obvious what the strange conditionals do, someone needs to have written down that this particular code is there because the special exemption from important tariffs of cigarettes due to the trade agreement between Serbia and Tunis that was valid between the years years 1992 and 2007.

                      • By mnsc 2024-12-2721:49

                        This is where a good comment really can help! And in these types of domains I would guess/hope that there exists some project master list to crossref that will send both developers and domain experts to the same source for "tariff-EU-92-0578" specifically the section 'exemptions'. So the comment is not not just a whole paragraph copied in between a couple of /*/

                  • By stouset 2024-12-2617:23

                    And any attempt whatsoever is some improvement over doing nothing and wishing luck to the next guy.

              • By chairmansteve 2024-12-2619:334 reply

                Thing is, good documentation has to be part of the company's process. eg, a QA engineer would have to be responsible for checking the documentation and certifying it. Costs money and time.

                You can't expect developers, already working 60 hour weeks to meet impossible deadlines, to spend another 15 hours altruistically documenting their code.

                • By ropable 2024-12-271:081 reply

                  Any documentation at all > no documentation, 99 times out of 100. And requiring your people to work 60 hours/week is symptomatic of larger problems.

                  • By switchbak 2024-12-275:28

                    How about old, out of date documentation that is actively misleading? Because that’s mostly what I run into, and it’s decidedly worse that no documentation.

                    Give me readable code over crappy documentation any day. In an ideal world the docs would be correct all of the time, apparently I don’t live in that world, and I’ve grown tired of listening to those who claim we just need to try harder.

                • By yearolinuxdsktp 2024-12-271:031 reply

                  Every line of documentation is a line of code and is a liability as it will rot if not maintained. That’s why you should be writing self documenting code as much as possible that’s obviates the need for documentation. But unlike code, stale/wrong doc will not break tests.

                  Spending 15 hours documenting the code is something no leader should be asking of engineering to do. You should not need to do it. Go back and write better code, one That’s more clear at a glance, easily readable, uses small functions written at a comparable level of abstraction, uses clear, semantically meaningful names.

                  Before you write a line of documentation, you should ask yourself whether the weird thing you were about to document can be expressed directly in the name of the method of the variable instead. Only once you have exhausted all the options for expressing the concept in code, then, only then, are you allowed to add the line of the documentation regarding it.

                  • By RaftPeople 2024-12-2717:26

                    > Only once you have exhausted all the options for expressing the concept in code, then, only then, are you allowed to add the line of the documentation regarding it.

                    But that's what people are talking about when talking about comments. The assumption is that the code is organized and named well already.

                    The real world of complexity is way beyond the expressiveness of code, unless you want function names like:

                    prorateTheCurrentDaysPartialFactoryReceiptsToYesterdaysStateOfSalesOrderAllocationsInTheSamePrioritySequenceThatDrivesFinancialReportingOfOwnedInventoryByBusinessUnit()

                    The code that performs this function is relatively simple, but the multiple concepts involved in the WHY and HOW are much less obvious.

                • By actionfromafar 2024-12-2620:49

                  Or you know, work the devs 40 hour weeks and make sure documentation is valued. Everything costs one way or another, it's all trade-off turtles all the way down.

                • By sanderjd 2024-12-270:21

                  Don't let perfect be the enemy of good.

                  "We don't write any documentation because we can't afford a dedicated QA process to certify it" <- that's dumb.

              • By bccdee 2024-12-2614:34

                Yeah: "what if this code becomes tech debt later" applies to everything, not just comments. It's a tradeoff.

                The best thing you can do to avoid creating debt for later maintainers is to write code that's easy to delete, and adding comments helps with that.

            • By rtpg 2024-12-267:484 reply

              An outdated comment is still a datapoint! Including if the comment was wrong when it was first written!

              We live in a world with version history, repositories with change requests, communications… code comments are a part of that ecosystem.

              A comment that is outright incorrect at inception is still valuable even if it is at least an attempt by the writer to describe their internal understanding of things.

              • By more-coffee 2024-12-2611:041 reply

                This. I have argued with plenty of developers on why comments are useful, and the counter arguments are always the same.

                I believe it boils down to a lack of foresight. At some point in time, someone is going to revisit your code, and even just a small `// Sorry this is awful, we have to X but this was difficult because of Y` will go a long way.

                While I (try to) have very fluid opinions in all aspects of programming, the usefulness of comments is not something I (think!) I'll ever budge on. :)

                • By temporallobe 2024-12-2614:121 reply

                  > // Sorry this is awful, we have to X but this was difficult because of Y

                  You don’t know how many times I’ve seen this with a cute little GitLens inline message of ā€œBrian Smith, 10 years agoā€. If Brian couldn’t figure it out 10 years ago, I’m not likely going to attempt it either, especially if it has been working for 10 years.

                  • By larsrc 2024-12-2614:32

                    But knowing what Brian was considering at the time is useful, both due avoiding redoing that and for realising that some constraints may have been lifted.

              • By xnx 2024-12-2612:07

                We should call them code clues

              • By buttercraft 2024-12-2616:152 reply

                What if you don't know that the comment is wrong?

                • By lexicality 2024-12-2618:20

                  IMO the only thing you can assume is that the person who wrote the comment wasn't actively trying to deceive you. You should treat all documentation, comments, function names, commit messages etc with a healthy dose of scepticism because no one truly has a strong grip on reality.

                • By rtpg 2024-12-2622:38

                  Right, unlike code (which does what it does, even if that isn't what the writer meant) there's no real feedback loop for comments. Still worth internalizing the info based on that IMO.

                  "This does X" as a comment when it in fact does Y in condition Z means that the probability you are looking at a bug goes up a bit! Without the comment you might not be able to identify that Y is not intentional.

                  Maybe Y is intentional! In which case the comment that "this is intentional" is helpful. Perhaps the intentionality is also incorrect, and that's yet another data point!

                  Fairly rare for there to be negative value in comments.

              • By temporallobe 2024-12-2614:041 reply

                It just occurred to me that perhaps this is where AI might prove useful. Functions could have some kind of annotation that triggers AI to analyze the function and explain it plain language when you do something like hover over the function name in the IDE, or, you can have a prompt where you can interact with that piece of code and ask it questions. Obviously this would mean developer-written comments would be less likely to make it into the commit history, but it might be better than nothing, especially in older codebases where the original developer(s) are long gone. Maybe this already exists, but I’m too lazy to research that right now.

                • By bccdee 2024-12-2614:28

                  But then could you trust it not to hallucinate functionality that doesn't exist? Seems as risky as out-of-date comments, if not more

                  What I'd really like is an AI linter than noticed if you've changed some functionality referenced in a comment without updating that comment. Then, the worst-case scenario is that it doesn't notice, and we're back where we started.

            • By Zondartul 2024-12-2614:44

              Comments that explain the intent, rather than implementation, are the more useful kind. And when intent doesn't match the actual code, that's a good hint - it might be why the code doesn't work.

            • By hedora 2024-12-2616:542 reply

              If a developer can’t write intelligible comments or straightforward code, then I’d argue they should find another job.

              • By pixl97 2024-12-2617:012 reply

                I mean it's easy to say silly things like this, but in reality most developers suck in one way or another.

                In addition companies don't seem to give a shit about straightforward code, they want LOC per day and the cheapest price possible which leads to tons of crap code.

                • By hallway_monitor 2024-12-2617:351 reply

                  Each person has their own strengths, but a worthwhile team member should be able to meet minimum requirements of readability and comments. This can be enforced through team agreements and peer review.

                  Your second point is really the crux of business in a lot of ways. The balance of quality versus quantity. Cost versus value. Long-term versus short term gains. I’m sure there are situations where ruthlessly prioritizing short term profit through low cost code is indeed the optimal solution. For those of us who love to craft high-quality code, the trick is finding the companies where it is understood and agreed that long-term value from high-quality code is worth the upfront investment and, more importantly, where they have the cash to make that investment.

                  • By pixl97 2024-12-2618:47

                    >I’m sure there are situations where ruthlessly prioritizing short term profit through low cost code is indeed the optimal solution

                    This is mostly how large publicly traded corps work, unless they are ran by programmers that want great applications or are required by law, they tend to write a lot of crap.

                • By mlloyd 2024-12-2618:36

                  >In addition companies don't seem to give a shit about straightforward code, they want LOC per day and the cheapest price possible which leads to tons of crap code.

                  Companies don't care about LOC, they care about solving problems. 30 LOC or 30k LOC doesn't matter much MOST of the time. They're just after a solution that puts the problem to rest.

              • By michaelt 2024-12-2617:051 reply

                If a delivery company has four different definitions of a customer’s first order, and the resulting code has contents that are hard to parse - does the Blake lie with the developer, or the requirements?

                • By TrololoTroll 2024-12-2617:43

                  If the developer had time to do it, with him. Otherwise with the company

                  I'm sure there's some abysmal shit that's extremely hard to properly abstract. Usually the dev just sucks or they didn't have time to make the code not suck

          • By larodi 2024-12-267:57

            Business requirements deviate from code almost immediately. Serving several clients with customisation adds even more strain on the process. Eventually you want to map paragraphs of business req to code which is not a 1:1 mapping.

            Aging codebase and the ongoing operations make it even harder to maintain consistently. eventually people surrender.

          • By ozim 2024-12-267:221 reply

            Then in 3 months someone in between came changing the code slightly that makes comment obsolete but doesn’t update the comment. Making all worse not better.

            Issue trackers are much better because then in git you can find tickets attached to the change.

            No ticket explaining why - no code change.

            Why not in repo? because business people write tickets not devs. Then tickets are passed to QA who also does read the code but also need that information.

            • By jounker 2024-12-2617:211 reply

              Why did the reviewer approve the change if the developer didn’t update the comment?

              It sounds like people are failing at their jobs.

              • By ozim 2024-12-2619:58

                Oh that is one of my pet peeves.

                "If only people would do their jobs properly".

                So we just fire all the employees and hire better ones only because someone did not pay attention to the comment.

                Of course it is an exaggeration - but also in the same line people who think "others are failing at their jobs" - should pick up and do all the work there is to be done and see how long they go until they miss something or make a mistake.

                Solution should be systematic to prevent people from failing and not expecting "someone doing their job properly".

                Not having comments as something that needs a review reduces workload on everyone involved.

                Besides, interfaces for PRs they clearly mark what changed - they don't point what hasn't been changed. So naturally people review what has changed. You still get the context of course and can see couple lines above and below... But still I blame the tool not people.

          • By ajuc 2024-12-2613:082 reply

            Requirements should be in the JIRA. JIRA number should be in the commit message.

            You do git blame and you see why each line is what it is.

            Comments are nice too, but they tend to lie the older they are. Git blame never lies.

            • By SleepyMyroslav 2024-12-2613:361 reply

              A code tends to be reused. When it happens jira is not likely to travel alongside the code. All 'older' jira tickets are useless broken links. All you have in practice is jira name. It usually happen with 'internal documentation' links as well.

              Git blame often lies when big merge was squashed. I mostly had these in Perforce so I might be wrong. Also when code travels between source version control servers and different source version control software it also loses information.

              I would say in my gamedev practical experience the best comments I saw are TODO implement me and (unit) test code that still runs. First clearly states that you have reached outside of what was planned before and 2nd allows you to inspect what code meant to do.

              • By jounker 2024-12-2617:261 reply

                One of my favorite conventions is ā€˜TODO(username): some comment’. This lets attribution survive merges and commits and lets you search for all of someone’s comments using a grep.

                • By ben_w 2024-12-2719:39

                  I tend to do:

                    // TODO: <the name of some ticket>: <what needs to happen here>
                  
                  e.g.

                    // TODO: IOS-42: Vogon construction fleet will need names to be added to this poetry reading room struct
                  
                  I've not felt my name is all that important for a TODO, as the ticket itself may be taken up by someone else… AFAICT they never have been, but they could have been.

            • By TeMPOraL 2024-12-2615:39

              Jira entries get wiped arbitrarily. Git blame may not lie, but it doesn't survive larger organizational "refactoring" around team or company mergers. Or refactoring code out into separate project/library. Hell, often enough it doesn't survive commits that rename bunch of files and move other stuff around.

          • By K0nserv 2024-12-2610:261 reply

            Comments are decent but flawed. Being a type proponent I think the best strategy is lifting business requirements into the type system, encoding the invariants in a way that the compiler can check.

            • By larsrc 2024-12-2614:34

              Comments should describe what the type system can't. Connect, pitfalls, workarounds for bugs in other code, etc.

        • By throwup238 2024-12-262:183 reply

          Thank god we’re held to such low standards. Every time I’ve worked in a field like pharmaceuticals or manufacturing, the documentation burden felt overwhelming by comparison and a shrug six months later would never fly.

          • By mnau 2024-12-263:251 reply

            We are not engineers. We are craftsmen, instead of working with wood, we work with code. What most customers want is an equivalent of "I need a chair, it should look roughly like this."

            If they want blueprints and documentation (e.g. maximum possible load and other limits), we can supply (and do supply, e.g. in pharma or medicine), but it will cost them quite a lot more. By the order of magnitude. Most customers prefer cobbled up solution that is cheap and works. That's on them.

            Edit: It is called waterfall. There is nothing inherently wrong with it, except customers didn't like the time it took to implement a change. And they want changes all the time.

            • By namaria 2024-12-269:142 reply

              > We are not engineers. We are craftsmen

              Same difference. Both appellations invoke some sort of idealized professional standards and the conversation is about failing these standards not upholding them. We're clearly very short of deserving a title that carries any sort of professional pride in it. We are making a huge mess of the world building systems that hijack attention for profit and generate numerous opportunities for bad agents in the form of security shortfalls or opportunities to exploit people using machines and code.

              If we had any sort of pride of craft or professional standards we wouldn't be pumping out the bug ridden mess that software's become and trying to figure out why in this conversation.

              • By alternatex 2024-12-2610:451 reply

                That is quite a cynical take. A lot of us take pride in our work and actively avoid companies that produce software that is detrimental to society.

                • By namaria 2024-12-2610:511 reply

                  It is cynical but it is also a generalization better supported by the evidence than "we're craftsmen" or "we're engineers".

                  If you can say "I'm a craftsman" or "I'm an engineer" all the power to you. Sadly I don't think we can say that in the collective form.

                  • By nyarlathotep_ 2024-12-2618:031 reply

                    > If you can say "I'm a craftsman" or "I'm an engineer" all the power to you. Sadly I don't think we can say that in the collective form.

                    My cynicism of the software "profession" is entirely a function of experience, and these titles are the (very rare) exception.

                    The norm is low-quality, low complexity disposable code.

                    • By computerdork 2024-12-270:21

                      Hmm, thinking back, think most companies I worked (from the small to the very large tech companies) had on average pretty good code and automated tests, pretty good processes, pretty good cultures and pretty good architectures. Some were very weak with one aspect, but made up for it others. But maybe I got lucky?

              • By mnau 2024-12-2614:371 reply

                > Both appellations invoke some sort of idealized professional standards

                The key point of the comment was that engineers do have standards, both from professional bodies and often legislative ones. Craftsmen do not have such standards (most of them, at least where I am from). Joiners definitely don't.

                Edit: I would also disagree with "pumping out bug ridden mess that software's become."

                We are miles ahead in security of any other industry. Physical locks have been broken for decades and nobody cares. Windows are breakable by a rock or a hammer and nobody cares.

                In terms of bugs, that is extraordinary low as well. In pretty much any other industry, it would be considered a user error, e.g. do not put mud as a detergent into the washing machine.

                Whole process is getting better each year. Version control wasn't common in 2000s (I think Linux didn't use version control until 2002). CI/CD. Security analyzers. Memory managed/safe languages. Automatic testing. Refactoring tools.

                We somehow make hundreds of millions of lines of code work together. I seriously doubt there is any industry that can do that at our price point.

                • By generic92034 2024-12-2617:191 reply

                  > We are miles ahead in security of any other industry. Physical locks have been broken for decades and nobody cares. Windows are breakable by a rock or a hammer and nobody cares.

                  That is not such a great analogy, in my opinion. If burglars could remotely break into many houses in parallel while being mostly non-trackable and staying in the safety of their own home, things would look differently on the doors and windows front.

                  • By mnau 2024-12-2618:321 reply

                    The reason why car keys are using chips is because physical safety sucks so much in comparison with digital.

                    The fact is we are better at it because of failure of state to establish the safe environment. Generally protection and safe environment is one of reason for paying taxes.

                    • By namaria 2024-12-2623:25

                      > The reason why car keys are using chips is because physical safety sucks so much in comparison with digital.

                      Not the reason. There is no safe lock, chip or not. You can only make it more inconvenient then the next car to break in.

                      > The fact is we are better at it because of failure of state to establish the safe environment. Generally protection and safe environment is one of reason for paying taxes.

                      Exactly backwards. The only real safety is being in a hi-sec zone protected by social convention and State retribution. The best existing lock in a place where bad actors have latitude won't protect you, and in a safe space you barely need locks at all.

          • By rcxdude 2024-12-2616:42

            OTOH, the level of documentation you get for free from source control would be a godsend in other contexts: the majority of the documentation you see in other processes is just to get an idea of what changed when and why.

          • By Yiin 2024-12-262:533 reply

            there is difference between building a dashboard for internal systems and tech that if failed can kill people

            • By throwup238 2024-12-263:04

              Most software work in pharma and manufacturing is still CRUD, they just have cultures of rigorous documentation that permeates the industry even when it's low value. Documenting every little change made sense when I was programming the robotics for a genetic diagnostics pipeline, not so much when I had to write a one pager justifying a one line fix to the parser for the configuration format or updating some LIMS dependency to fix a vulnerability in an internal tool that's not even open to the internet.

            • By Mikhail_Edoshin 2024-12-265:152 reply

              Well, a hand watch or a chair cannot kill people, but the manufacturing documentation for them will be very precise.

              Software development is not engineering because it is still relatively young and immature field. There is a joke where a mathematician, a physicist and a engineer are given a little red rubber ball and asked to find its volume. The mathematician measures the diameter and computes, the physicist immerses the ball into water and sees how much was displaced, and an the engineer looks it up in his "Little red rubber balls" reference.

              Software development does not yet have anything that may even potentially grow into such a reference. If we decide to write it we would not even know where to start. We have mathematicians who write computer science papers; or physicists who test programs; standup comedians, philosophers, everyone. But not engineers.

              • By ozim 2024-12-268:421 reply

                Difference is that code is the documentation and design.

                That is problem where people don’t understand that point.

                Runtime and running application is the chair. Code is design how to make ā€œchairā€ run on computer.

                I say in software development we are years ahead when it comes to handling complexity of documentation with GIT and CI/CD practices, code reviews and QA coverage with unit testing of the designs and general testing.

                So I do not agree that software development is immature field. There are immature projects and companies cut corners much more than on physical products because it is much easier to fix software later.

                But in terms of practices we are way ahead.

                • By dambi0 2024-12-2622:06

                  Isn’t this similar to saying the valves and vessels of a chemical processing system is the design and documentation of the overall process?

                  I know that it’s frequently reposted but Peter Naur’s Programming as Theory Building is always worth a reread.

                  The code doesn’t tell us why decisions were made, what constraints were considered or what things were ruled out

            • By larodi 2024-12-267:58

              The word code comes from Latin coudex which seems mean - to hack a tree. Are we then not mere lumberjacks with the beards and beer and all :)))

      • By mgkimsal 2024-12-2522:491 reply

        > Oh, and in 6 months the unintuitive and confusing concept needs to be completely changed into - surprise, surprise - a completely different but equally unintuitive and confusing concept.

        But you have to keep the old way of working exactly the same, and the data can't change, but also needs to work in the new version as well. Actually show someone there's two modes, and offer to migrate their data to version 2? No way - that's confusing! Show different UI in different areas with the same data that behaves differently based on ... undisclosed-to-the-user criteria. That will be far less confusing.

        • By terribleperson 2024-12-2522:542 reply

          As a user 'intuitive' UIs that hide a bunch of undisclosed but relevant complexity send me into a frothing rage.

          • By chefandy 2024-12-266:102 reply

            In many problem spaces, software developers are only happy with interfaces made for software developers. This article diving into the layers of complex logic we can reason about at once perfectly demonstrates why. Developers ā€˜get’ that complexity, because it’s our job, and think about GUIs as thin convenience wrappers for the program underneath. To most users, the GUI is the software, and they consider applications like appliances for solving specific problems. You aren’t using the refrigerator, you’re getting food. You’re cooking, not using the stove. The fewer things they have to do or think about to solve their problem to their satisfaction, the better. They don’t give a flying fuck about how software does something, probably wouldn’t bother figuring out how to adjust it if they could, and the longer it takes them to figure out how to apply their existing mental models UI idioms to the screen they’re looking at, the more frustrated they get. Software developers know what’s going on behind the scenes so seeing all of the controls and adjustments and statuses and data helps developers orient themselves save figure out what they’re doing. Seeing all that stuff is often a huge hindrance to users that just have a problem they need to solve, and have a much more limited set of mental models and usage idioms they need to use figuring how which of those buttons to press and parameters to adjust. That’s the primary reason FOSS has so few non-technical users.

            The problem comes in when people that aren’t UI designers want to make something ā€œlook designedā€ so they start ripping stuff out and moving it around without understanding how it works affect different types of users. I don’t hear too many developers complain about the interface for iMessage for example despite having a fraction of the controls visible at any given time, because it effectively solves their problem, and does so easier than with a visible toggle for read receipts, SMS/iMessages, text size, etc etc etc. It doesn’t merely look designed, it it’s designed for optimal usability.

            Developers often see an interface that doesn’t work well for developers usage style, assume that means it doesn’t work well, and then complain about it among other developers creating an echo chamber. Developers being frustrated with an interface is an important data point that shouldn’t be ignored, but our perspectives and preferences aren’t nearly as generalizable some might think.

            • By terribleperson 2024-12-268:373 reply

              I'm not particularly bothered by non-developer UI. I'm bothered by the incessant application of mobile UI idioms to desktop programs (remember when all windows programs looked somewhat similar?), by UI churn with no purpose, by software that puts functionality five clicks deep for no reason other than to keep the ui 'minimal', by the use of unclear icons when there's room for text (worse, when it's one of the bare handful of things with a universally-understood icon and they decided to invent their own), by UIs that just plain don't present important information for fear of making things 'busy'. There's a lot to get mad about when it comes to modern UIs without needing to approach it from a software developer usage style perspective.

              • By chefandy 2024-12-2616:391 reply

                You're making a lot of assumptions about who's doing what, what problems they're trying to solve by doing it, and why. The discipline of UI design is figuring out how people can solve their problems easily and effectively. If you have advanced users that need to make five mouse clicks to perform an essential function, that's a bad design and the chance of that being a UI design decision is just about zero. Same thing with icons. UI design, fundamentally, is a medium of communication: do you think it's more likely a UI designer-- a professional and likely educated interactivity communicator-- chose those icons, or a developer or project manager grabbing a sexy looking UI mockup on dribble and trying to smash their use case into it?

                Minimalism isn't a goal-- it's a tool to make a better interface and can easily be overused. The people that think minimalism is a goal and will chop out essential features to make something "look designed" are almost always developers. Same thing with unclear icons. As someone with a design degree that's done UI design but worked as a back-end developer for a decade before that, and worked as a UNIX admin off and on for a decade before that, I am very familiar with the technical perspective on design and it's various echo-chamber-reinforced follies.

                It's not like all UI designers are incredibly qualified or don't underestimate the importance of some particular function within some subset of users, and some people that hire designers don't realize that a graphic designer isn't a UI designer and shouldn't be expected to work as one. But 700 times out of 1000, that's something dev said "this is too annoying to implement" or some project manager dropped it from the timeline. Maybe 250 of those remaining times, the project manager says "we don't need designers for this next set of features, right? Dev can just make it look like the other parts of the project?"

                Developers read an edward tufte book, think they're experts, and come up with all sorts of folk explanations about what's happening with a design and why people are doing it, then talk about it in venues like this with a million other developers agreeing with them. That does a whole lot more damage to UIs in the wild than bad design decisions made by designers.

                • By terribleperson 2024-12-2621:28

                  You seem to think I'm attacking UI designers. I'm not. I think software would be a lot better with professional UI designers designing UIs.

                  edit: I am making a lot of assumptions. I'm assuming that most UIs aren't really designed, or are 'designed' from above with directions that are primarily concerned about aesthetics.

              • By suzzer99 2024-12-2617:59

                +1 to all this. And when did it become cool to have icons that provide no feedback they've been clicked, combined with no loading state? I'm always clicking stuff twice now because I'm not sure I even clicked it the first time.

              • By namaria 2024-12-269:26

                I think a lot of this is bike shedding. Changing the interface design is easy. Understanding usability and building usable systems is hard.

            • By namaria 2024-12-269:231 reply

              > That’s the primary reason FOSS has so few non-technical users.

              Yeah, citation needed. If your argument that 'non-technical users' (whatever that is - being technical is not restricted to understanding computers and software deeply) don't use software that exposes a lot of data on its internals as exemplified by FOSS having few 'non-technical users' meaning people who are not software developers, this is just false. There are entire fields where FOSS software is huge. GIS comes to mind.

              • By chefandy 2024-12-2615:431 reply

                Normally in this rant I specifically note that non-software technical people are still technical. For genuinely non-technical software, what are the most popular end-user facing FOSS-developed applications? Firefox, signal, blender, Inkscape, Krita maybe… most of those are backed by foundations that pay designers and in Mozilla’s case, actually do a ton of open usability research. I don’t believe Inkscape does but they do put a ton of effort into thinking about things from the user workflow perspective and definitely do not present all of the functionality to the user all at once. Blender, at first, just made memorize a shitload of shortcuts but they’ve done a ton of work figuring out what users need to see in which tasks in different workflows and have a ton of different purpose-built views. For decades, Gimp treated design, workflow and UI changes like any other feature and they ended up with a cobbled-together ham fisted interface used almost exclusively by developers. You’ll have a hard time finding a professional photographer that hasn’t tried gimp and an even harder time finding one that still uses it because of the confusing, unfocused interface. When mastodon stood a real chance of being what Bluesky is becoming, I was jumping up and down flailing my arms trying to get people to work on polishing the user flow and figure out how to communicate what they needed to know concisely. Dismissal dismissal dismissal. ā€œI taught my grandmother how federation works! They just need to read the documentation! Once they start using it they’ll figure it out!ā€ Well, they started using it, didn’t have that gifted grandmother-teaching developer to explain it to them, and they almost all left immediately afterwards.

                Just like human factors engineering, UI design is a unique discipline that many in the engineering field think they can intuit their way through. They’re wrong and if you look beyond technical people, it’s completely obvious.

                • By chefandy 2024-12-2618:02

                  Worth noting that Gimp just made a separate UI design repo and seem to be doing a great job at confronting this systemic problem in the project.

          • By robocat 2024-12-260:221 reply

            I'm trying to learn acceptance: how not to get so angry at despicable UIs.

            Although I admit I'm kinda failing. My minor successes have been by avoiding software: e.g. giving up programming (broken tools and broken targets were a major frustration) and getting rid of Windows.

            • By shnock 2024-12-260:47

              Having given up programming, what do you do now?

      • By mcdeltat 2024-12-266:221 reply

        IMO the fact that code tends to become hard over time in the real world, is even more reason to lower cognitive load. Because cognitive load is related to complexity. Things like inheritance make it far too easy to end up with spaghetti. So if it's not providing significant benefit, god damn don't do it in the first place (like the article mentions).

        • By Simon_O_Rourke 2024-12-2611:20

          That depends on who thinks it's going to be a significant benefit - far far too many times I've had non-technical product managers yelling about some patch or feature or whatever with a "just get it done" attitude. Couple that with some junior engineering manager unwilling to push back, with an equally junior dev team and you'll end up with the nasty spaghetti code that only grows.

      • By dogcomplex 2024-12-262:28

        Sounds like a bunch of excellent excuses why code is not typically well factored. But that all just seems to make it more evident that the ideal format should be more well-factored.

      • By SkyBelow 2024-12-2614:591 reply

        >It's quite easy to imagine a well factored codebase where all things are neatly separated.

        If one is always implementing new code bases that they keep well factored, they should count their blessings. I think being informed about cognitive load in code bases is still very important for all the times we aren't so blessed. I've inherited applications that use global scope and it is a nightmare to reason though. Where possible I improve it and reduce global scope, but that is not always an option and is only possible after I have reasoned enough about the global scope to feel I can isolate it. As such, letting others know of the costs is helpful to both reduce it from happening and to convince stakeholders of the importance of fixing it after it has happened and accounting for the extra costs it causes until it is fixed.

        >The messy stuff is where the real world concepts need to be transformed into code.

        I also agree this can be a messy place, and on a new project, it is messy even when the code is clean because there is effectively a business logic/process code base you are inheriting and turning into an application. I think many of the lessons carry over well as I have seen an issue with global scope in business processes that cause many of the same issues as in code bases. When very different business processes end up converging into one before splitting again, there is often extra cognitive load created in trying to combine them. A single instance really isn't bad, much like how a single global variable isn't bad, but this is an anti-pattern that is used over and over again.

        One helpful tool is working ones way up to the point of having enough political power and earned enough respect for their designs to have suggestions of refactoring business processes be taken into serious consideration (one also has to have enough business acumen to know when such a suggestion is reasonable).

        >the original author doesn't work here anymore so no one's here to explain the original code's intent.

        I fight for comments that tell me why a certain decision is made in the code. The code tells me what it is doing, and domain knowledge will tell most of why it is doing the things expected, but anytime the code deviates from doing what one would normally expect to be done in the domain, telling me why it deviated from expected behavior is very important for when someone is back here reading it 5+ years later when no one is left from the original project. Some will suggest putting it in documentation, but I find that the only documentation with any chance of being maintained or even kept is the documentation built into the code.

        • By jeffreygoesto 2024-12-2616:55

          The "why" is the hardest part. You are writing to a future version of most probably a different person with a different background. Writing all is as wrong as writing nothing. You have to anticipate the questions of the future. That takes experience and having been in different shoes, "on the receiving side" of such a comment. Typically developers brag what they did, not why, especially the ones who think they are good...

      • By chii 2024-12-266:351 reply

        > Where just the concepts need to be whiteboarded and explained because they're unintuitive and confusing.

        they're intuitive to somebody - just not the software engineer. This simply means there's some domain expertise which isn't available to the engineer.

        • By tsimionescu 2024-12-2616:51

          Not necessarily. There are a lot of domains where you're digitizing decades of cobbled together non-computer systems, such as law, administration, or accounting. There's a very good chance that no single human understands those systems either, and that trying to model them will inevitably end up with obscure code that no one will ever understand either. Especially as legislation and accounting practices accrete in the future, with special cases for every single decision.

      • By ilvez 2024-12-2522:39

        Plus to everything said. It's an everyday life of "maintainer", picking the next battle to pick the best way to avoid sinking deeper and defending the story that exactly "this" is the next refactoring project. All that while balancing different factors as you mention to actually believe oneself, because there are countless of paths..

      • By larsrc 2024-12-2614:421 reply

        Oh, and there's massive use of aspect-oriented programming, the least local paradigm ever!

        • By feoren 2024-12-2618:192 reply

          I have never actually seen aspect-oriented programming used in the wild. Out of curiosity, in what context are you seeing AOP used?

          • By looperhacks 2024-12-2618:55

            We use it to automatically instrument code for tracing. Stuff like this is IMO the only acceptable use to reduce boiler-plate but quickly becomes terrible if you don't pay attention.

          • By computerdork 2024-12-270:27

            Also good for having default activities performed on object or subsystem. For instance, by default, always having an object have security checks to make sure it has permission to perform the tasks it should be (have seen this, and sounds like a good idea at least). And also, to have some basic logging performed to show when you've entered and left function calls. It's easy to forget to add these to a function, especially with large codebase with lots of developers

      • By haliskerbas 2024-12-2522:19

        This puts things really well. I’ll add into it that between the first white boarding session and the first working MVP there’ll be plenty of stakeholders who change their mind, find new info, or ask for updates that may break the original plan

      • By lukan 2024-12-2523:25

        It can be done. Sometimes.

        I am so proud and happy, when I can make a seemingly complicated change quickly, because the architecture was well designed and everthing neatly seperated.

        Most of the time though, it is exactly like you described. Or randalls good code comic:

        https://xkcd.com/844/

        Allmost too painful to be funny, when you know the pain is avoidable in theory.

        Still, it should not be an excuse to be lazy and just write bad code by default. Developing the habit of making everything as clean, structured and clear as possible allways pays of. Especially if that code, that was supposed to be a quick and dirty throw away code experiment somehow ended up being used and 2 years later you suddenly need to debug it. (I just experienced that joy)

      • By yearolinuxdsktp 2024-12-270:54

        Nothing about computers is intuitive. Not even using a mouse.

        A late-breaking change is a business advantage—-learn how to roll with them.

      • By jimbokun 2024-12-2619:24

        In my experience, the more convoluted code is more likely to have performance issues.

      • By rtpg 2024-12-267:441 reply

        I mean really nobody wants an app that is slow, hard to refactor, with confusing business logic etc. Everyone wants good proporties.

        So then you get into what you’re good at. Maybe you’re good at modeling business logic (even confusing ones!). Maybe you’re good at writing code that is easy to refactor.

        Maybe you’re good at getting stuff right the first time. Maybe you’re good at quickly fixing issues.

        You can lean into what you’re good at to get the most bang for your buck. But you probably still have some sort of minimum standards for the whole thing. Just gotta decide what that looks like.

        • By larsrc 2024-12-2614:37

          Some people are proud of making complex code. And too many people admire those who write complex code.

      • By namaria 2024-12-269:06

        > you also need to write it in a convoluted way because, for various annoying reasons, that's what performs best on the computer.

        That's nothing to do with hardware. The various annoying reasons are not set in stone or laws of physics. They are merely the path dependency of decades of prioritizing shipping soon because money.

    • By nevi-me 2024-12-260:444 reply

      > If someone feels compelled to read every function either the functions are poor abstractions or the reader has trust issues, which may be warranted.

      I joined a company with great code and architecture for 3 months last year. They deal with remittances and payments.

      Their architecture leads are very clued up, and I observed that they spent a lot of quality time figuring out their architecture and improvements, continuously. They'd do a lot of refactors for all the various teams, and the cadence of feature development and release was quite impressive.

      In that period though, I and another long-standing colleague made a few errors that cost the company a lot of money, like an automated system duplicating payments to users for a few hours until we noticed it.

      Part of their architectural decision was to use small functions to encapsulate logic, and great care and code review was put into naming functions appropriately (though they were comment averse).

      The mistakes we committed, were because we trusted that those functions did what they said they did correctly. After all, they've also been unit tested, and there's also integration tests.

      If it weren't for the fortitude of the project manager (great guy hey) in firmly believing in collective responsibility if there's no malice, I'd probably have been fired after a few weeks (I left for a higher offer elsewhere).

      ---

      So the part about trust issues resonates well with me. As a team we made the decision that we shouldn't always trust existing code, and the weeks thereafter had much higher cognitive load.

      • By avg_dev 2024-12-263:101 reply

        That sounds like a very difficult situation. Would you be willing to elaborate on what kinds of bugs lay in the pre-existing functions? Was some sort of operation that was supposed to be idempotent (ā€œif you call it with these unique parameters over and over, it will be the same as if you only called it onceā€) not so? I am trying to imagine what went wrong here. A tough situation, must have been quite painful. How serious were the consequences? If you don’t feel comfortable answering that is okay.

        • By nevi-me 2024-12-265:411 reply

          I can't remember the exact detail, but one instance was a function checking whether a user should be paid based on some conditions. It checked the db, and I think because the codebase and db move fast, there was a new enum added a few months prior which was triggered by our transaction type.

          So that helped function didn't account for the new enum, and we ended up sending >2 payments to users, in some cases I think over 10 to one user.

          The issue was brought to customer support's attention, else we might have only noticed it at the end of the week, which I think would have led to severe consequences.

          The consequences never reached us because our PM dealt with them. I suppose in all the financial loss instances, the business absorbed the losses.

          • By noisy_boy 2024-12-2613:411 reply

            > So that helped function didn't account for the new enum

            This is where Scala/Rust's enforcement of having to handle all arms of a match clause help catch such issues - if you are matching against the enum, you won't even be able to compile if you don't handle all the arms.

            • By matt_kantor 2024-12-2614:261 reply

              Sounds like the source of truth for the enum members may have been in the database.

              (But yes, exhaustiveness checking for sum types is a great feature.)

              • By galangalalgol 2024-12-2615:141 reply

                The only db work I've done in rust required a recompile if the db schema changed, or even the specific queries your program used, because the rust types got generated from the schema. So in those cases the db change would have driven a rust type change and rust would have verified exhaustive handling.

                • By etherealG 2024-12-2918:331 reply

                  Db changes are generally at runtime, how would you recompile rust code during the save of the data to the db? How do you rollback the change if a compile fails? How do you add the necessary code to handle new cases of the enum but not have it present in the db? This is amazingly interesting to me, would love to know more.

                  • By avg_dev 2024-12-3014:29

                    Maybe a code gen layer that generates rust types from a db schema. I don’t know rust but have seen those in other languages. I could see a DB enum type corresponding to a language specific enum type and then the language rules applying.

                    I do think this is a level of indirection myself; if the generated code was perfect and always in sync, that would be one thing, but by definition it is not the case.

      • By barrkel 2024-12-2616:101 reply

        Function names aren't wholly distinct from comments. They suffer from the same problems as comments - they can go stale and no longer reflect the code they're naming.

        • By etherealG 2024-12-3116:28

          I think the argument against comments is that while function names are a necessary form of communicating intent of the code, comments aren’t. The more forms there are the more work there is to update on each change in the code. Comments mean more to update and hence more to fail to update. They also generally can’t be detected for staleness as well as functions, although that is changing now with better ai, not only compilers etc.

      • By layer8 2024-12-2616:29

        Functions generally need to be documented, especially if there are any gotchas not obvious from the function signature. And one should always read the documentation. Good names are for discovery and recollection, and for the call-site code to be more intelligible, but they don’t replace having a specification of the function’s interface contract, and client code properly taking it into account.

      • By SkyBelow 2024-12-2615:101 reply

        >The mistakes we committed, were because we trusted that those functions did what they said they did correctly. After all, they've also been unit tested, and there's also integration tests.

        As it is stated, I don't see where it is your mistake. You should be able to trust things do what they say, and there should be integration testing that happens which adds the appropriate amount of distrust and verification. Even with adequate unit testing, you normally inject the dependencies so it wouldn't be caught.

        This seems an issue caused by two problems, inadequate integration testing and bugs in the original function, neither of which are your fault.

        Building a sixth sense of when to distrust certain code is something you see from more experienced developers at a company, but you were new so there is no reason to expect you to have it (and the system for making code changes shouldn't depend upon such intuition anyways).

        • By strogonoff 2025-01-0711:45

          > This seems an issue caused by two problems, inadequate integration testing and bugs in the original function, neither of which are your fault.

          I believe the problem may be the culture. Business logic that handles sensitive things where bugs can cost a lot of money is one place where hardcore DRY and factoring everything into small functions is not such a great idea. Yes, it may be a big function, and there is an upfront overhead of having to understand it all to make a change, and there is some duplication of code, but once you understand the function you can reason locally and such bugs would be less likely.

    • By motorest 2024-12-2522:235 reply

      > I've been thinking about the notion of "reasoning locally" recently. Enabling local reasoning is the only way to scale software development past some number of lines or complexity. When reasoning locally, one only needs to understand a small subset, hundreds of lines, to safely make changes in programs comprising millions.

      That was supposedly the main trait of object-oriented programming. Personally that was how it was taught to me: the whole point of encapsulation and information hiding is to ensure developers can "reason locally", and thus be able to develop more complex projects by containing complexity to specific units of execution.

      Half of SOLID principles also push for that. The main benefit of Liskov's substitution principle is ensure developers don't need to dig into each and every concrete implementation to be able to reason locally about the code.

      On top of that, there are a multitude of principles and rules of thumb that also enforce that trait. For example, declaring variables right before they are used the first time. Don't Repeat Yourself to avoid parsing multiple implementations of the same routine. Write Everything Twice to avoid premature abstractions and tightly coupling units of execution that are actually completely independent, etc etc etc.

      Heck, even modularity, layered software architectures, and even microservices are used to allow developers to reason locally.

      In fact, is there any software engineering principle that isn't pushing for limiting complexity and allowing developers to reason locally?

      • By KronisLV 2024-12-2610:541 reply

        > In fact, is there any software engineering principle that isn't pushing for limiting complexity and allowing developers to reason locally?

        Both DRY and SOLID lead to codebases that can be worse in this respect.

        DRY and SRP limit what will be done in a single method or class, meaning that both the logic will eventually be strewn across the codebase, as well as any changes to that will need to take all of the pieces using the extracted logic into account. Sometimes it makes sense to have something like common services, helper and utility classes, but those can be in direct opposition to local reasoning for any non-trivial logic.

        Same for polymorphism and inheritance in general, where you suddenly have to consider a whole class structure (and any logic that might be buried in there) vs the immediate bits of code that you’re working with.

        Those might be considered decent enough practices to at least consider, but in practice they will lead to a lot of jumping around the codebase, same for any levels of abstraction (resource/controller, service, mappers, Dto/repository, …) and design patterns.

        • By bccdee 2024-12-2614:511 reply

          Yeah I think that, though experienced programmers tend to understand what makes code good, they're often bad at expressing it, so they end up making simplified and misleading "rules" like SRP. Some rules are better than others, but there's no substitute for reading a lot of code and learning to recognize legibility.

          • By KronisLV 2024-12-2615:12

            > Yeah I think that, though experienced programmers tend to understand what makes code good, they're often bad at expressing it, so they end up making simplified and misleading "rules" like SRP.

            I mean, I'm not saying that those approaches are always wholly bad from an organizational standpoint either, just that there are tradeoffs and whatnot.

            > Some rules are better than others, but there's no substitute for reading a lot of code and learning to recognize legibility.

            This feels very true though!

      • By sunshowers 2024-12-2523:313 reply

        Encapsulation is the good part of object-oriented programming for precisely this reason, and most serious software development relies heavily on encapsulation. What's bad about OOP is inheritance.

        Microservices (in the sense of small services) are interesting because they are good at providing independent failure domains, but add the complexity of network calls to what would otherwise be a simple function call. I think the correct size of service is the largest you can get away with that fits into your available hardware and doesn't compromise on resilience. Within a service, use things like encapsulation.

        • By jimmaswell 2024-12-261:096 reply

          Inheritance is everyone's favorite whipping boy, but I've still never been in a codebase and felt like the existing inheritance was seriously hindering my ability to reason about it or contribute to it, and I find it productive to use on my own. It makes intuitive sense and aids understanding and modularity/code resuse when used appropriately. Even really deep inheritance hierarchies where reasonable have never bothered me. I've been in the industry for at least 8 years and a volunteer for longer than that, and I'm currently in a role where I'm one of the most trusted "architects" on the team, so I feel like I should "get it" by now if it's really that bad. I understand the arguments against inheritance in the abstract but I simply can't bring myself to agree or even really empathize with them. Honestly, I find the whole anti-inheritance zeitgeist as silly and impotent as the movement to replace pi with tau, it's simply a non-issue that's unlikely to be on your mind if you're actually getting work done IMHO.

          • By Mikhail_Edoshin 2024-12-266:131 reply

            The problem of inheritance is that it should be an internal mechanism of code reuse, yet it is made public in a declarative form that implies a single pattern of such reuse. It works more or less but it also regularly runs into limitations imposed by that declarativeness.

            For example, assume I want to write emulators for old computer architectures. Clearly there will be lots of places where I will be able to reuse the same code in different virtual CPUs. But can I somehow express all these patterns of reuse with inheritance? Will it be clearer to invent some generic CPU traits and make a specific CPU to inherit several such traits? It sounds very unlikely. It probably will be much simpler to just extract common code into subroutines and call them as necessary without trying to build a hierarchy of classes.

            Or lets take, for example, search trees. Assume I want to have a library of such trees for research or pedagogic purposes. There are lots of mechanisms: AVL trees, 2-3, 2-3-4, red-black, B-Trees and so on. Again there will be places where I can reuse the same code for different trees. But can I really express all this as a neat hierarchy of tree classes?

            • By motorest 2024-12-2710:492 reply

              > The problem of inheritance is that it should be an internal mechanism of code reuse, yet it is made public in a declarative form that implies a single pattern of such reuse.

              Not quite. A simplistic take on inheritance suggests reusing implementations provided by a base class, but that's not what inheritance means.

              Inheritance sets a reusable interface. That's it. Concrete implementations provided by a base class, by design, are only optional. Take a look at the most basic is-a examples from intro to OO.

              Is the point of those examples reusing code, or complying with Liskov's substitution principle?

              The rest of your comment builds upon this misconception, and thus is falsified.

              • By Mikhail_Edoshin 2024-12-285:22

                Polymorphism is not related to inheritance. In earlier object-oriented systems it was (and is), but only because they were trying all directions. It actually becomes clearer without inheritance and many modern systems introduce it as a separate concept of an interface.

                For example, I am sending requests to an HTTP server. There are several authentication methods but when we look at request/method interaction they are similar. So it would be convenient to have standard interface here, something like 'auth.applyTo(request)'. Yet would it be a good idea to try making different 'Auth' methods to be subclasses of each other?

                Or another example I'm currently working on: I have a typical search tree, say, AVL, but in my case I need to make references to cells in the tree because I will access it bottom-up. As the tree changes its geometry the data move between cells so I need to notify the data about the address change. This is simple: I merely provide a callback and the tree calls it with each new and changed cell address. I can store any object as long as it provides this callback interface. Does this mean I need to make all objects I am going to store in a tree to inherit some "TreeNotifiable" trait?

                Polymorphism happens when we split a system into two components and plan interaction between them. Internals of a component do not matter, only the surface. Inheritance, on the other hand, is a way to share some common behavior of two components, so here the internals do matter. These are really two different concepts.

              • By sunshowers 2024-12-2720:24

                My example complied perfectly with Liskov's substitution principle. Much better than examples like "a JSON parser is a parser". The system I worked on had perfect semantic subtyping.

                Liskov substitution won't save you, and I'm quite tired of people saying it will. The problem of spaghetti structures is fundamental to what makes inheritance distinct from other kinds of polymorphism.

                Just say no to inheritance.

          • By 59nadir 2024-12-268:54

            > [...] it's simply a non-issue that's unlikely to be on your mind if you're actually getting work done IMHO.

            Part of why I get (more) work done is that I don't bother with the near-useless taxonomical exercises that inheritance invites, and I understand that there are ways of writing functions for "all of these things, but no others" that are simpler to understand, maintain and implement.

            The amount of times you actually need an open set of things (i.e. what you get with inheritance) is so laughably low it's a wonder inheritance ever became a thing. A closed set is way more likely to be what you want and is trivially represented as a tagged union. It just so happens that C++ (and Java) historically has had absolutely awful support for tagged unions so people have made do with inheritance even though it doesn't do the right thing. Some people have then taken this to mean that's what they ought to be using.

            > I've been in the industry for at least 8 years and a volunteer for longer than that, and I'm currently in a role where I'm one of the most trusted "architects" on the team, so I feel like I should "get it" by now if it's really that bad.

            I don't think that's really how it works. There are plenty of people who have tons of work experience but they've got bad ideas and are bad at what they do. You don't automatically just gain wisdom and there are lots of scenarios where you end up reinforcing bad ideas, behavior and habits. It's also very easy to get caught up in a collective of poorly thought out ideas in aggregate: Most of modern C++ is a great example of the kind of thinking that will absolutely drag maintainability, readability and performance down, but most of the ideas can absolutely sound good on their own, especially if you don't consider the type of architecture they'll cause.

          • By bccdee 2024-12-2615:07

            The difference between inheritance and composition as tools for code reuse is that, in composition, the interface across which the reused code is accessed is strictly defined and explicit. In inheritance it is weakly defined and implicit; subclasses are tightly coupled to their parents, and the resulting code is not modular.

          • By juunpp 2024-12-261:441 reply

            So you've never worked on a code base with a 3-level+ deep inheritance tree and classes accessing their grandparent's protected member variables and violating every single invariant possible?

            • By jimmaswell 2024-12-265:03

              > 3-level+ deep inheritance tree and classes accessing their grandparent's protected member variables

              Yes, I have. Per MSDN, a protected member is accessible within its class and by derived class instances - that's the point. Works fine in the game I work on.

              > violating every single invariant possible

              Sure, sometimes, but I see that happen without class inheritance just as often.

          • By briantakita 2024-12-2617:04

            If you are reading a deep * wide inheritance hierarchy with override methods. You will have to navigate through several files to understand where the overrides occurred. Basically multiply the number of potential implementations by inheritance depth * inheritance width.

            You may not be bitten by such an issue in application code. But I've seen it in library code. Particularly from Google, AWS, various Auth libraries, etc. Due to having to interop with multiple apis or configuration.

          • By sunshowers 2024-12-262:041 reply

            I'm glad it's been useful to you!

            I can only share my own experience here. I'm thinking of a very specific ~20k LoC part of a large developer infrastructure service. This was really interesting because it was:

            * inherently complex: with a number of state manipulation algorithms, ranging from "call this series of external services" to "carefully written mutable DFS variant with rigorous error handling and worst-case bounds analysis".

            * quite polymorphic by necessity, with several backends and even more frontends

            * (edit: added because it's important) a textbook case of where inheritance should work: not artificial or forced at all, perfect Liskov is-a substitution

            * very thick interfaces involved: a number of different options and arguments that weren't possible to simplify, and several calls back and forth between components

            * changing quite often as needs changed, at least 3-4 times a week and often much more

            * and like a lot of dev infrastructure, absolutely critical: unimaginable to have the rest of engineering function without it

            A number of developers contributed to this part of the code, from many different teams and at all experience levels.

            This is a perfect storm for code that is going to get messy, unless strict discipline is enforced. I think situations like these are a good stress test for development "paradigms".

            With polymorphic inheritance, over time, a spaghetti structure developed. Parent functions started calling child functions, and child functions started calling parent ones, based on whatever was convenient in the moment. Some functions were designed to be overridden and some were not. Any kind of documentation about code contracts would quickly fall out of date. As this got worse, refactoring became basically impossible over time. Every change became harder and harder to make. I tried my best to improve the code, but spent so much time just trying to understand which way the calls were supposed to go.

            This experience radicalized me against class-based inheritance. It felt that the easy path, the series of local decisions individual developers made to get their jobs done, led to code that was incredibly difficult to understand -- global deterioration. Each individual parent-to-child and child-to-parent call made sense in the moment, but the cumulative effect was a maintenance nightmare.

            One of the reasons I like Rust is that trait/typeclass-based polymorphism makes this much less of a problem. The contracts between components are quite clear since they're mediated by traits. Rather than relying on inheritance for polymorphism, you write code that's generic over a trait. You cannot easily make upcalls from the trait impl to the parent -- you must go through a API designed for this (say, a context argument provided to you). Some changes that are easy to do with an inheritance model become harder with traits, but that's fine -- code evolving towards a series of messy interleaved callbacks is bad, and making you do a refactor now is better in the long run. It is possible to write spaghetti code if you push really hard (mixing required and provided methods) but the easy path is to refactor the code.

            (I think more restricted forms of inheritance might work, particularly ones that make upcalls difficult to do -- but only if tooling firmly enforces discipline. As it stands though, class-based inheritance just has too many degrees of freedom to work well under sustained pressure. I think more restricted kinds of polymorphism work better.)

            • By Fluorescence 2024-12-2613:201 reply

              > This experience radicalized me against ...

              My problem with OO bashing is not that it isn't deserved but seems in denial about pathological abstraction in other paradigms.

              Functional programming quickly goes up it's own bum with ever more subtle function composition, functor this, monoidal that, effect systems. I see the invention of inheritance type layering just in adhoc lazy evaluated doom pyramids.

              Rich type systems spiral into astronautics. I can barely find the code in some defacto standard crates instead it's deeply nested generics... generic traits that take generic traits implemented by generic structs called by generic functions. It's an alphabet soup of S, V, F, E. Is that Q about error handling, or an execution model or data types? Who knows! Only the intrepid soul that chases the tail of every magic letter can tell you.

              I wish there were a panacea but I just see human horrors whether in dynamically-typed monkey-patch chaos or the trendiest esoterica. Hell I've seen a clean-room invention of OO in an ancient Fortran codebase by an elderly academic unaware it was a thing. He was very excited to talk about his phylogenetic tree, it's species and shared genes.

              The layering the author gives as "bad OO" admin/user/guest/base will exist in the other styles with pros/cons. At least the OO separates each auth level and shows the relationship between them which can be a blessed relief compared to whatever impenetrable soup someone will cook up in another style.

              • By sunshowers 2024-12-2620:22

                The difference, I think, is that much of that is not the easy path. Being able to make parent-child-parent-child calls is the thing that distinguishes inheritance from other kinds of polymorphism, and it leads to really bad code. No other kind of polymorphism has this upcall-downcall-upcall-downcall pattern baked into its structure.

                The case I'm talking about is a perfect fit for inheritance. If not there, then where?

        • By oivey 2024-12-260:102 reply

          Encapsulation arguably isn’t a good part, either. It encourages complex state and as a result makes testing difficult. I feel like stateless or low-state has won out.

          • By Tainnor 2024-12-269:41

            Encapsulation can be done even in Haskell which avoids mutable state by using modules that don't export their internals, smart constructors etc. instead. You can e.g. encapsulate the logic for dealing with redis in a module and never expose the underlying connection logic to the rest of the codebase.

          • By sunshowers 2024-12-261:041 reply

            Hmm, to me encapsulation means a scheme where the set of valid states is a subset of all representable states. It's kind of a weakening of "making invalid states unrepresentable", but is often more practical.

            Not all strings are valid identifiers, for example, it's hard to represent "the set of all valid identifiers" directly into the type system. So encapsulation is a good way to ensure that a particular identifier you're working with is valid -- helping scale local reasoning (code to validate identifiers) up into global correctness.

            This is a pretty FP and/or Rust way to look at things, but I think it's the essence of what makes encapsulation valuable.

            • By oivey 2024-12-261:251 reply

              What you’re talking about is good design but has nothing to do with encapsulation. From Wikipedia:

              > In software systems, encapsulation refers to the bundling of data with the mechanisms or methods that operate on the data. It may also refer to the limiting of direct access to some of that data, such as an object's components. Essentially, encapsulation prevents external code from being concerned with the internal workings of an object.

              You could use encapsulation to enforce only valid states, but there are many ways to do that.

              • By sunshowers 2024-12-262:36

                Well whatever that is, that's what I like :)

        • By gf000 2024-12-2611:24

          Not only network calls, but also parallelism, when that microservice does some processing on its own, or are called from a different microservice as well.

          Add to it a database with all the different kinds of transaction semantics and you have a system that is way above the skillset of the average developer.

      • By 6510 2024-12-262:23

        Out of curiosity I sometimes rewrite things as spaghetti (if functions are short and aren't called frequently) or using globals (if multiple functions have to many params) it usually doesn't look better and when it does it usually doesn't stay that way for very long. In the very few remaining cases I'm quite happy with it. It does help me think about what is going on.

      • By hiAndrewQuinn 2024-12-2522:291 reply

        In theory, you could design a parallel set of software engineering best practices which emphasize long-term memory of the codebase over short-term ability to leaf through and understand it. I guess that would be "reasoning nonlocally" in a useful sense.

        In practice I think the only time this would be seen as a potentially good thing by most devs is if it was happening in heavily optimized code.

        • By K0nserv 2024-12-2610:281 reply

          An interesting point. Would there be any benefits to this non-local reasoning?

          • By hiAndrewQuinn 2024-12-2610:40

            Not unless you own and run the business, I suspect. You probably buy yourself a much higher absolute threshold of complexity you can comfortably handle in the codebase, but it's not exactly like software developers are known to take kindly to being handed an Anki deck of design decisions, critical functions, etc. and being told "please run this deck for 3 weeks and then we'll get started".

            I suspect it's much more common that codebases evolve towards requiring this nonlocal reasoning over time than being intentionally designed with it in mind.

      • By 708145_ 2024-12-2523:001 reply

        > The main benefit of Liskov's substitution principle is ensure developers don't need to dig into each and every concrete implementation to be able to reason locally about the code.

        Yeah, but doesn't help in this context (enable local reasoning) if the objects passed around have too much magic or are mutated all over the place. The enterprise OOP from 2010s was a clusterfuck full of unexpected side effects.

        • By dboreham 2024-12-2523:181 reply

          I suspect that enterprise anything is going to be a hot mess, just because enterprises can't hire many of the best people. Probably the problem we should address as an industry is: how to produce software with mostly low wattage people.

          • By brokencode 2024-12-2523:551 reply

            The eventual solution will probably be to replace the low wattage people with high wattage machines.

            • By gf000 2024-12-2611:29

              Sure, once they can solve advent of code problems on the second week..

    • By cle 2024-12-2617:06

      > I find types helps massively with this. A function with well-constrained inputs and outputs is easy to reason about. One does not have to look at other code to do it. However, programs that leverage types effectively are sometimes construed as having high cognitive load, when it in fact they have low load. For example a type like `Option<HashSet<UserId>>` carries a lot of information(has low load): we might not have a set of user ids, but if we do they are unique.

      They sometimes help. But I think it's deeper than this. A function with inputs and outputs that are well-constrained with very abstract, complex types is still hard to reason about, unless you're used to those abstractions.

      I think it's more accurate to say that something is "easy to reason about" if its level of abstraction "closely matches" the level of abstraction your brain is comfortable with / used to. This can vary dramatically between people, depending on their background, experience, culture, etc.

      I could describe the Option<HashSet<UserId>> type in terms of functors and applicatives and monads, and though it would describe exactly the same set of valid values, it has a much higher cognitive load for most people.

      > However, programs that leverage types effectively are sometimes construed as having high cognitive load, when it in fact they have low load.

      Cognitive load is an individual experience. If someone "construes" something as having high cognitive load, then it does! (For them). We should be writing programs that minimize cognitive load for the set of programmers who we want to be able to interact w/ the code. That means the abstractions need to sufficiently match what they are comfortable with.

      It's also fine to say "sorry, this code was not intended to have low cognitive load for you".

    • By DarkNova6 2024-12-2522:32

      100% agree and this not only concerns readability. The concept of "locality" turns out to be a fairly universal concept, which applies to human processes just as much as technical ones. Side-effects are the root of all evil.

      You don't see a waiter taking orders from 1 person on a table, but rather go to a table and get orders from everybody sitting there.

      And as for large methods, I find that they can be broken into smaller once just fine as long as you keep them side-effect free. Give them a clear name, a clear return value and now you have a good model for the underlying problem you are solving. Looking up the actual definition is just looking at implementation details.

    • By peterlada 2024-12-2523:271 reply

      There is an issue of reading a code that is written by somebody else. If it's not in a common style, the cognitive load of parsing how it's done is an overhead.

      The reason I used to hate Perl was around this, everyone had a unique way of using Perl and it had many ways to do the same thing.

      The reason I dislike functional programming is around the same, you can skin the cat 5 ways, then all 5 engineers will pick a different way of writing that in Typescript.

      The reason I like Python more is that all experienced engineers will eventually gravitate towards the idea of Pythonic notion and I've had colleagues whose code looked identical to how I'd have written it.

      • By mrkeen 2024-12-269:36

        Python 2, Python 3? Types or no types?

    • By jonahx 2024-12-2620:03

      > Proponents of small functions argue that you don't have to read more than the signature and name of a function to understand what it does; it's obvious what a function called last that takes a list and returns an optional value does.

      I used to be one of those proponents, and have done a 180.

      The problems are:

      1. The names are never as self-evident as you think, even if you take great care with them.

      2. Simply having so many names is an impediment in itself.

      The better way:

      Only break things up when you need to do. This means the "pieces" of the system correspond to the things you care about and are likely to change. You'll know where to look.

      When you actually need an abstraction to share code between parts of the system, create it then.

    • By jreback 2024-12-261:481 reply

      Re: trust issues...I'd argue this is the purpose of automated tests. I think tests are too often left out of architectural discussions as if they are some additional artifact that gets created separately from the running software. The core / foundational / heavily reused parts of the architecture should have the most tests and ensure the consumers of those parts has no trust issues!

      • By K0nserv 2024-12-268:441 reply

        Tests are good but moving left by lifting invariants into the type system is better.

        Compare

           fn send_email(addr: &str, subject: &str, body: &str) -> Result<()>
        
        to

            fn send_email(add: &EmailAddr, subject: &str, body: &str) -> Result<()>
        
        In the second case, the edge cases of an empty or invalid email address don't need to be tested, they are statically impossible.

        • By galangalalgol 2024-12-2615:29

          Thanks for the small concrete example. I try to explain this a lot. It also makes coverage really easy to get with fewer tests.

    • By casenmgreen 2024-12-261:461 reply

      I may be wrong, but my view of software is : you have functions, and you have the order in which functions are called. Any given function is straightforward enough, if you define its function clearly and keep it small enough - both of which can reasonably be done. Then we have the problem, which is the main problem, of the order in which functions are called. For this, I use a state machine. Write out the state machine, in full, in text, and then implement it directly, one function per state, one function per state transition.

      The SM design doc is the documentation of the order of function calling, it is exhaustive and correct, and allows for straightforward changes in future (at least, as straightforward as possible - it is always a challenge to make changes).

      • By euph0ria 2024-12-2610:461 reply

        Would love to understand this better. Is there any example you could point to?

        • By casenmgreen 2024-12-2612:341 reply

              init -> success -> red
              init -> failure -> cleanup
          
              red -> success -> red_yellow
              red -> failure -> cleanup
          
              red_yellow -> success -> green
              red_yellow -> failure -> cleanup
          
              green -> success -> yellow
              green -> failure -> cleanup
          
              yellow -> success -> red
              yellow -> failure -> cleanup
          
              cleanup -> done -> finish
          
          init/red/etc are states.

          success/failure/etc are events.

          Each state is a function. The function red() for example, waits for 20 seconds, then returns success (assuming nothing went wrong).

          To start the state machine, initializes state to "init", and enter a loop, in the loop you call the function for the current state (which makes that state actually happen and do whatever it does), and that function returns its event for whatever happen when it was run, and you then call a second function, which updates state based on the event which just occurred. Keep doing that, until you hit state "finish", then you're done.

          • By euph0ria 2024-12-270:071 reply

            Got it, thanks. But it seemed from your original post that you tend to write state machines a lot more than the usual engineer does, would that be correct? Would you use this in a crud rest API for example?

            • By casenmgreen 2024-12-2712:12

              When writing code, the amount of structure depends on the amount of code.

              More and more complex code requires more structure.

              Structure takes time and effort, so we write the minimum amount of structure which is appropriate for the code (where code often grows over time, and then by that growth becomes unmanagable, and then we need more structure, which may require a rewrite to move from the existing structure to a new, fuller structure).

              So with methods for organizing code, we go something like, in order of less to more structure,

              . lines of code . functions . libraries . OO libraries . classes

              A state machine is a form of structure, separate from how we organize code, and moderately high cost. I don't often use one, because most of the code I write doesn't need to be particularly rigorous - but for example I did write a mailing list, and that really is used, so it really did have to be correct, so I wrote out the state machine and implemented based on the state machine.

              State machines also help with testing. You can keep track of which states you have tested and which events from each state you have tested.

              I've never written a REST API in my life, so I can't tell you if I would use a state machine for that :-)

    • By mcdeltat 2024-12-266:25

      In regards to small functions, I think an important - but not often mentioned - aspect is shared assumptions. You can have many small functions with garbage abstractions that each implictly rely on the behaviour of each other - therefore the cognitive load is high. Or, you can have many small functions which are truly well-contained, in which case you may well need not read the implementation. Far too much code falls into the former scenario, IMO.

    • By LtWorf 2024-12-2522:36

      I've seen functions called getValue() that were actually creating files on disk and writing stuff.

      Also, even if the function actually does what advertised, I've seen functions that go 4-5 levels deep where the outer functions are just abstracting optional parameters. So to avoid exposing 3 or 4 parameters, tens of functions are created instead.

      I think you do have a point but ideas get abused a lot.

    • By blago 2024-12-2615:021 reply

      > Proponents of small functions argue that you don't have to read more than the signature and name of a function to understand what it does;

      Although this is often the case, the style of the program can change things significantly. Here are a few, not so uncommon, examples where it starts to break down:

      1. When you’re crafting algorithms, you might try to keep code blocks brief, but coming up with precise, descriptive names for each 50-line snippet can be hard. Especially if the average developer might not even fully understand the textbook chapter behind it.

      2. At some point you have to build higher than "removeLastElementFromArray"-type of functions. You are not going to get very far skimming domain-specific function names if don’t have any background in that area.

      More examples exist, but these two illustrate the point.

      • By K0nserv 2024-12-2615:461 reply

        Both examples stem from not understanding the problem well enough I think. My best work is done when I first write a throwaway spaghetti solution to the problem. Only through this endeavour do I understand the problem well enough to effectively decompose the solution.

        • By nuancebydefault 2024-12-2720:501 reply

          You understand your final fine grained code after your 'spaghetti' intermezzo. Others and your future you, probably less so.

          • By K0nserv 2024-12-2720:531 reply

            My point is that the factoring and abstractions one produce after the spaghetti intermezzo will be better than a blind stab at them; a greater understanding of the problem helps.

            • By nuancebydefault 2024-12-2721:10

              Agree that intermezzos - even of the spaghetti kind - help understanding.

              I thought this thread was more about (non) maintainability of code consisting of many procedures for each of which names are to be found that will make their usage self-explaining.

              From my experience, simple API's with complex and often long implementations can be very well suited. As long as those are low on side effects and normally DRY, as opposed to puristicly DRY.

    • By sunshowers 2024-12-2523:35

      This is absolutely the right way to think about things.

      I like thinking about local reasoning in terms of (borrowing from Ed Page) "units of controversy". For example, I like using newtypes for identifiers, because "what strings are permitted to be identifiers" is a unit of controversy.

    • By hinkley 2024-12-2620:05

      Types are somewhat a different dimension. Sort of the classic 1 dimensional argument about a 2 dimensional problem domain. Which quadrant you’re talking about alters whether the arguments support reality or argue with it.

      If understanding a block of code requires knowing a concept that the team feels everyone should know anyway, then it’s not such an imposition. If the code invites you to learn that concept, so much the better. The code is ā€œdiscoverableā€ - it invites you to learn more. If the concept is incidental to the problem and/or the team is objectively wrong in their opinion, then you have tribal knowledge that is encroaching on the problem at hand. And whether it’s discoverable or not is neither here nor there. Because understanding the code requires knowing lots of other things, which means either memorization, or juggling more concepts than comfortably fit in short term memory - cognitive overload.

      You know you’ve blown past this point when you finally trace the source of a bad piece of data but cannot remember why you were looking for it in the first place.

      I’m hoping the problem of cognitive load gets more attention in the near future. We are overdue. But aside from people YouTubing code reviews, I’m still unclear what sorts of actionable metrics or feedback will win out in this arena. Maybe expanding code complexity to encompass the complexity of acquiring the values used in the code, not just the local data flow.

    • By konschubert 2024-12-2613:20

      The first step for allowing local reasoning is to break your product into independent subdomains that are as independent as possible.

      For a software company, this means crafting the product ownership of your team such that the teams can act as independently as possible.

      This is where most companies already fail.

      Once this has been achieved, you can follow this pattern on smaller and smaller scales down to individual functions in your code.

    • By ajuc 2024-12-2611:03

      Last is something that is embarrassingly extractable, which makes it a bad example (you shouldn't write that function anyway in 99% of cases - surely someone wrote it already in stdlib of your language).

      It's like taking "list.map(x -> x*x)" as a proof that parallelism is easy.

      Most code is not embarrassingly extractable (or at least not at granularity of 3 lines long methods).

    • By tdiff 2024-12-2614:57

      > Proponents of small functions argue that you don't have to read more than the signature and name of a function to understand what it does; it's obvious what a function called last that takes a list and returns an optional value does.

      It's also interesting that in comment to the same article many people argue against PR process. I hardly see how else that level of discipline required not to undermine trust in names of small methods can be maintained for any team with more than 3 developers.

    • By holri 2024-12-266:543 reply

      I do not agree that typing leads to less cognitive load. Typing often leads to more and more complicated code. Dynamically typed code is often shorter and more compact. If dynamically typed code is well written, its function, inputs and outputs are clear and obvious. Clear and easy to understand code is not primarily a matter of typed or not typed code, it is a matter of a great programmer or a poor one.

      • By com2kid 2024-12-2611:111 reply

        There is a function. It takes in 4 parameters. One of them is called ID

        Is ID a string, a number, a GUID? Better check the usage within the function.

        Oh, the declaration is `id: number`

        Mystery solved.

        Even better if the language supports subtyping so it is something like id: userID and userID is a subtype of number.

        • By holri 2024-12-2613:202 reply

          In a dynamically duck typed language it should not matter if an ID is a string, a number or a GUID. The code should work with all of them. The semantically important thing is that this is an identifier. No String, number or GUI data type expresses this true meaning of the value.

          • By com2kid 2024-12-2616:051 reply

            It matters a lot even in a duck typed language.

            If there are multiple types of user IDs, I don't want to pass the wrong one into a DB call.

            This is often the case when dealing with systems that have internal IDs vs publicly exposed IDs. A good type system can correctly model which I have a hold of.

            For complex objects proper typing is even more important. "What fields exist on this object? I better check the code and see what gets accessed!"

            Even worse are functions where fields get added (or removed!) to an object as the object gets processed.

            Absolute nightmare. The concept of data being a black box is stupid, the entire point of data is that at some point I'll need to actually use it, which is a pain in the ass to do if no one ever defines what the hell fields are supposed to be laying around.

            • By holri 2024-12-2616:54

              By naming the variable ID it is crystal clear what the value is. Most of the time an explicit type only adds cognitive load to the reader, and limits the universality of the code. At an high abstraction level, most of the time a type is from a program logic point of view an irrelevant machine implementation detail. If a specific duck is required it is explicitly tested. This makes code very clear when the duck type is important and when not.

          • By anon-3988 2024-12-275:34

            That's how you get a stray string in a column of integers.

      • By K0nserv 2024-12-2610:34

        Statically typed code definitely requires more effort to read, but this is not cognitive load. Cognitive load is about how much working memory is required. Statically typed code requires less cognitive load because some of the remembering is outsourced to the source code.

        Statically typed code can lead to more complicated code; it can also accurately reflect the complexity inherent in the problem.

      • By mirekrusin 2024-12-267:151 reply

        This is true at smaller scales and flips over on larger scales (larger codebase, dependencies, team/teams sizes).

        • By holri 2024-12-269:581 reply

          A function is clear or not. I fail to see how the scale of the code, team, dependence is a factor in that.

          • By K0nserv 2024-12-2612:58

            I split local reasoning into horizontal or vertical.

            Vertical reasoning is reasoning inside a module or function. Here information hiding and clear interfaces help.

            Horizontal reasoning is reasoning across the codebase in a limited context; adding a new parameter to a public function is a good example. The compiler helps you find and fix all the use sites, and with good ability to reason vertically at each site, even a change like this is simple.

    • By meehai 2024-12-2616:36

      At work we have a pretty big Python monorepo. The way we scale it is by having many standalone CLI mini apps ( about 80) atm with most of them outputting json/parquet in GCS or bigquery tables. Inputs are the same.

      I insisted a lot on this unix (ish as it's not pipes) philosophy. It paid off so far.

      We can test each cli app as well as make broader integration tests.

    • By larsrc 2024-12-2614:41

      > If someone feels compelled to read every function either the functions are poor abstractions or the reader has trust issues, which may be warranted.

      Or it's open source and the authors were very much into Use The Source, Luke!

    • By bb88 2024-12-2522:521 reply

      The larger problem are things that have global effect: databases, caches, files, static memory, etc. Or protocols between different systems. These are hard to abstract away, usually because of shared state.

      • By mrkeen 2024-12-269:35

        Weird, I read that between the lines of parent's post. Of course local reasoning precludes global effects.

    • By movpasd 2024-12-2612:30

      I feel that one big way in which engineers talk past each other is in assuming that code quality is an inherent property of the code itself. The code is meaningless without human (and computer) interpretation. Therefore, the quality of code is a function of the relationship between that code and its social context.

      Cognitive load is contextual. `Option<HashSet<UserId>>` is readable to someone knowledgeable in the language (`Option`, `HashSet`) and in the system (meaning of `UserId` -- the name suggests it's an integer or GUID newtype, but do we know that for sure? Perhaps it borrows conventions from a legacy system and so has more string-like semantics? Maybe users belong to groups, and the group ID is considered part of the user ID -- or perhaps to uniquely identify a user, you need both the group and user IDs together?).

      What is the cognitive load of `Callable[[LogRecord, SystemDesc], int]`? Perhaps in context, `SystemDesc` is very obvious, or perhaps not. With surrounding documentation, maybe it is clear what the `int` is supposed to mean, or maybe it would be best served wrapped in a newtype. Maybe your function takes ten different `Callable`s and it would be better pulled out into an polymorphic type. But maybe your language makes that awkward or difficult. Or maybe your function is a library export, or even if it isn't, it's used in too many places to make refactoring worthwhile right now.

      I also quite like newtypes for indicating pragmatics, but it is also a contextually-dependent trade-off. You may make calls to your module more obvious to read, but you also expand the module's surface area. That means more things for people writing client code to understand, and more points of failure in case of changes (coupling). In the end, it seems to me that it is less important whether you use a newtype or not, and more important to be consistent.

      In fact, this very trade-off -- readability versus surface area -- is at the heart of the "small vs large functions" debate. More smaller functions, and you push your complexity out into the interfaces and relationships between functions. Fewer large functions, and the complexity is internalised inside the functions.

      To me, function size is less the deciding factor [0], but rather whether your interfaces are real, _conceptually_ clean joints of your solution. We have to think at a system level. Interfaces hide complexity, but only if the system as a whole ends up easier to reason about and easier to change. You pay a cost for both interface (surface area) and implementation (volume). There should be a happy middle.

      ---

      [0] Also because size is often a deceptively poor indicator of implementation complexity in the first place, especially when mathematical expressions are involved. Mathematical expressions are fantastic exactly because they syntactically condense complexity, but it means very little syntactic redundancy, and so they seem to be magnets for typos and oversights.

    • By mattmcknight 2024-12-265:103 reply

      Types? "Option<HashSet<UserId>>" means almost nothing to me. A well defined domain model should indicate what that structure represents.

      • By anon-3988 2024-12-275:39

        > A well defined domain model should indicate what that structure represents.

        "Should", but does it? If a function returns Option<HashSet<UserId>> I know immediately that this function may or may not return the set, and if it does return the set, they are unique.

        This is a fact of the program, I may not know "why" or "when" it does what. But as a caller, I can guarantee that I handled every possible code path. I wouldn't get surprised later one because, apparently, this thing can throw an exception, so my lock didn't get released.

      • By jbggs 2024-12-268:521 reply

        it seems like you're just not familiar with the domains defined by those types, or at least the names used here

        • By mattmcknight 2024-12-273:421 reply

          Are you? What would an option for a hash set of userids? I just don't find that "types" magically solve problems of cognitive load.

          • By K0nserv 2024-12-2721:02

            > What would an option for a hash set of userids?

            As an argument: An optional filter for a query e.g. "return me posts form these users"

            As a return value: The users who liked a post or nothing if it's not semantically valid for the post to be liked for some reason.

            > I just don't find that "types" magically solve problems of cognitive load.

            Cognitive load is about working memory and having to keep things in it. Without types one only has a name, say "userIds". The fact that it's possible for it to be null and that it's supposed to contain unique values has to be kept in working memory(an increase in cognitive load)

      • By bbkane 2024-12-265:181 reply

        Even that means a lot more than `{}`, who's tortured journeys I have to painstakingly take notes om in the source code while I wonder what the heck happened to produce the stack trace...

        • By mattmcknight 2024-12-273:44

          Yes, but it means less than something like UserGroup. I hear you on {} though, I'm currently looking at "e: ".

    • By mithametacs 2024-12-2618:11

      Not everything is a functional program though and side effects are important. Types can’t* represent this.

      *Not for practical programs

    • By dataflow 2024-12-2610:042 reply

      Absolutely with you on the idea in the abstract, but the problem you run into in practice is that enabling local reasoning (~O(1)-time reading) often comes at the cost of making global changes (say, ~O(n)-time writing in the worst case, where n is the call hierarchy size) to the codebase. Or to put it another way, the problem isn't so much attaining local readability but maintaining it -- it imposes a real cost on maintenance. The cost is often worth it, but not always.

      Concrete toy examples help here, so let me just give a straight code example.

      Say you have the following interface:

        void foo(void on_completed());
      
        void callback();
      
        void bar(int n)
        {
          foo(callback);
        }
      
      
      Now let's say you want to pass n to your callback. (And before you object that you'd have the foresight to enable that right in the beginning because this is obvious -- that's missing the point, this is just a toy example to make the problem obvious. The whole point here is you found a deficiency in what data you're allowed to pass somewhere, and you're trying to fix it during maintenance. "Don't make mistakes" is not a strategy.)

      So the question is: what do you do?

      You have two options:

      1. Modify foo()'s implementation (if you even can! if it's opaque third party code, you're already out of luck) to accept data (state/context) along with the callback, and plumb that context through everywhere in the call hierarchy.

      2. Just embed n in a global or thread-local variable somewhere and retrieve it later, with appropriate locking, etc. if need be.

      So... which one do you do?

      Option #1 is a massive undertaking. Not only is it an O(n) changes for a call hierarchy of size n, but foo() might have to do a lot of extra work now -- for example, if it previously used a lock-free queue to store the callback, now it might lose performance as it might not be able to do everything atomically. etc.

      Option #2 only results in 3 modifications, completely independently from the rest of the code: one in bar(), one for the global, and one in the callback.

      Of course the benefit of #1 here is that option #1 allows local reasoning when reading the code later, whereas option #2 is spooky action at a distance: it's no longer obvious that callback() expects a global to be set. But the downside is that now you might need to spend several more hours or days or weeks to make it work -- depending on how much code you need to modify, which teams need to approve your changes, and how likely you are to hit obstacles.

      So, congratulations, you just took a week to write something that could've taken half an hour. Was it worth it?

      I mean, probably yes, if maintenance is a rare event for you. But what if you have to do it frequently? Is it actually worth it to your business to make (say) 20% of your work take 10-100x as long?

      I mean, maybe still it is in a lot of cases. I'm not here to give answers, I absolutely agree local reasoning is important. I certainly am a zealot for local reasoning myself. But I've also come to realize that achieving niceness is quite a different beast from maintaining it, and I ~practically never see people try to give realistic quantified assessments of the costs when trying to give advice on how to maintain a codebase.

      • By vacuity 2024-12-2620:43

        Initial implementation and maintenance need to keep design in mind, and there should be more clarity around responsibility and costs of particular designs and how flexible the client is with the design at a given point in time. It's an engineering process and requires coordination.

      • By o_nate 2024-12-2618:46

        Add a global variable? Let's not go there, please. Anything would be better than that. In this case I would bite the bullet and change the signature, but rather than just adding the one additional parameter, I would add some kind of object that I could extend later without breaking the call signature, since if the issue came up once, it's more likely to come up again.

    • By TZubiri 2024-12-2612:551 reply

      >I've been thinking about the notion of "reasoning locally" recently. Enabling local reasoning is the only way to scale software development past some number of lines or complexity. When reasoning locally, one only needs to understand a small subset, hundreds of lines, to safely make changes in programs comprising millions.

      Have you never heard of the word of our lord and saviour oop, or functions? It's called encapsulation.

      You might have learned it through prog langs as it is an embedded ideal

      • By K0nserv 2024-12-2613:021 reply

        As another sibling comment pointed out there are many tools that enable local reasoning, encapsulation is one such tool.

        I'm not claiming the idea is novel, just that I haven't encountered a name for it before.

        • By TZubiri 2024-12-2613:341 reply

          I'm not saying that encapsulation is a tool for local reasoning, I'm saying they are the same concept.

          How is the concept of local reasoning distinct from that of encapsulation?

          • By galangalalgol 2024-12-2615:38

            I think most of us associate the word encapsulation with OOP nightmare code that spread mutable state across many small classes that often inherited from one another and hid the wrong state. Stateless and low state are the reaction to that. If you expand the term to include those aids to local reasoning then many more might agree with you.

  • By Aurornis 2024-12-2520:0712 reply

    > Mantras like "methods should be shorter than 15 lines of code" or "classes should be small" turned out to be somewhat wrong.

    These hard rules may be useful when trying to instill good habits in juniors, but they become counterproductive when you start constraining experienced developers with arbitrary limits.

    It’s really bad when you join a team that enforces rules like this. It almost always comes from a lead or manager who reads too many business books and then cargo cults those books on to the team.

    • By quesomaster9000 2024-12-2520:2218 reply

      This is the bane of my existence at the moment after ~20 years into my career, and it frustrates me when I run into these situations when trying to get certain people to review pull requests (because I'm being kind, and adhering to a process, and there is really valuable feedback at times). But on the whole it's like being dragged back down to working at a snails pace.

      - Can't refactor code because it changes too many files and too many lines.

      - Can't commit large chunks of well tested code that 'Does feature X', because... too many files and too many lines.

      - Have to split everything down into a long sequence of consecutive pull requests that become a process nightmare in its own right

      - The documentation comments gets nitpicked to death with mostly useless comments about not having periods at the ends of lines

      - End up having to explain every little detail throughout the function as if I'm trying to produce a lecture, things like `/* loop until not valid */ while (!valid) {...` seemed to be what they wanted, but to me it made no sense what so ever to even have that comment

      This can turn a ~50 line function into a 3 day process, a couple of hundred lines into a multi-week process, and a thousand or two line refactor (while retaining full test coverage) into a multi-month process.

      At one point I just downed tools and quit the company, the absurdity of it all completely drained my motivation, killed progress & flow and lead to features not being shipped.

      Meanwhile with projects I'm managing I have a fairly good handle on 'ok this code isnt the best, but it does work, it is fairly well tested, and it will be shipped as the beta', so as to not be obstinate.

      • By sarchertech 2024-12-261:498 reply

        After 20 years of doing this, I’m convinced that required PR reviews aren’t worth the cost.

        In the thousands of pull requests I’ve merged across many companies, I have never once had a reviewer catch a major bug (a bug that is severe enough that if discovered after hours, would require an oncall engineer to push a hot fix rather than wait for the normal deployment process to fix it).

        I’ve pushed a few major bugs to production, but I’ve never had a PR reviewer catch one.

        I’ve had reviewers make excellent suggestions, but it’s almost never anything that really matters. Certainly not worth all the time I’ve spent on the process.

        That being said, I’m certainly not against collaboration, but I think required PR reviews aren’t the way to do it.

        • By dullcrisp 2024-12-265:214 reply

          The point of code reviews isn’t to catch bugs. It’s for someone else on the team to read your code and make sure they can understand it. If no one else on your team can understand your code, you shouldn’t be committing it to the repository.

          • By TeMPOraL 2024-12-2616:371 reply

            Maybe. But then, sure, I can understand the code you wrote - on a syntactic/operational level. This adds Foos to bar instead of baz, and makes Quux do extra Frob() call. Whatever, that's stupid stuff below junior level. What would actually matter is for me to understand why you're doing this, what it all means. Which I won't, because you're doing some code for symbolic transformation of equations for optimizing some process, and I'm doing data exchange between our backend and a million one-off proprietary industrial formats, and we only see each other on a team call once a week.

            I'm exaggerating, but only a little. Point is, in a deep project you may have domain-specialized parts, and those specialties don't overlap well. Like, ideally I'd take you aside for an hour to explain the 101 of the math you're doing and the context surrounding the change, but if neither you nor me have the time, that PR is getting a +2 from me on the "no stupid shit being done, looks legit code-wise; assuming you know your domain and this makes sense" basis.

            • By strogonoff 2025-01-0711:02

              To do a good job on code review, reviewer must understand the challenge and how it is best to address it better than the programmer who implemented it. Necessarily!

              To do an adequate job, reviewer must understand the task at least equivalently well.

              The above is possible, and probably even desirable, but it adds a massive overhead in developer time for any non-trivial change. Enforcing good code review can approximately double development costs for the company, and inevitably creates a lot of programming & architecture work—nearly equivalent to any other programming & architecture work, minus the time spent hitting keyboard keys—for engineers, who might not exactly enjoy it.

              As a result, it is very rare, and most reviews just tick boxes and enforce taste.

              I personally hate to review substantial implementations. It is work minus the fun part. During work, I get to come up with a solution and bring it to life. Instead, during review I have to devise a solution only to use it as a reference point to assess another’s solution. This can also cause review suggestions that are difficult to enact (if my solution is sufficiently different and I think better than the original, that’s a lot of work to redo).

          • By klabb3 2024-12-2615:563 reply

            HN moment. I’ve never seen in practice that someone says ā€I don’t understand itā€ and the author says ā€good point, I will simplify itā€.

            Rather, the opposite. I often saw people make unnecessary complex or large PRs that were too much workload to review, leading the reviewer to approve, on the grounds of ā€seems like you know what you’re doing and tbh I don’t have half a day to review this properlyā€.

            Code review is a ritual. If you ask why we have it people will give you hypothetical answers more often than concrete examples. Personally I’m a proponent of opt-in CRs, ie ask for a second pair of eyes when your spidey senses tell you.

            • By throw-qqqqq 2024-12-270:10

              Our juniors write horribly complex code that senior devs have to ask to simplify. This happens all the time. And the juniors simplify and thank us for teaching and mentoring. It’s a big reason we do reviews. So we can control how dirty the code is before merging and so we can grow each other with constructive feedback. Sometimes it’s also just ā€œLGTMā€ if nothing smells.

              90% of comments in my team’s PRs come with suggestions that can be applied with a click (we use GitLab). It requires almost no effort to apply suggestions and it’s often not much extra work for reviewers to explain and suggest a concrete change.

              I agree that reviews should be used pragmatically.

            • By delusional 2024-12-270:40

              Get (or create) better colleagues. It's usually pretty easy to identify if people are approving pull requests that they don't understand. Pull them aside and have a professional talk about what a pull request review is. People want to do good, but you have to make it clear that you value their opinion.

              If you treat the people around you as valuable collaborators instead of pawns to be played to fulfill your processes, your appreciation for reviews will transform. Remember that it's their work too.

            • By Capricorn2481 2024-12-2616:45

              > I often saw people make unnecessary complex or large PRs that were too much workload to review, leading the reviewer to approve, on the grounds of ā€seems like you know what you’re doing and tbh I don’t have half a day to review this properlyā€

              That just seems like company wide apathy to me. Obviously you have to make an effort to read the code, but there are lots of ways developers can overcomplicate things because they were excited to try a pattern or clever solution. It doesn't make them bad devs, it's just an easy trap to fall into.

              These should not pass a code review just because the code "works." It's totally acceptable to say "we're not gonna understand this in 3 months the way it's written, we need to make this simpler" and give some suggestions. And usually (if you're working with people that care about the workload they make for others) they will stop after a few reviews that point this out.

              We've done this at our company and it's helped us immensely. Recognizing whether the code is unnecessarily complex or the problem is inherently complex is part of it, though.

          • By deeviant 2024-12-2614:432 reply

            I feel if you ask 5 people what "the point" of codes review is, you'd get 6 different answers.

            • By zimpenfish 2024-12-2618:02

              And a 7th complaining about the formatting of the question.

          • By sarchertech 2024-12-2621:41

            I watched pre merge code reviews become a requirement in the industry and catching bugs was almost always the #1 reason given.

            The times I've seen a 2nd set of eyes really help with the understandability of code, it was almost always collaboration before or while the code was being written.

            I would estimate something like 1 out of 100 PR reviews I've seen in my life were really focussed on improving understandability.

        • By kevmo314 2024-12-263:202 reply

          Wow someone who finally has this same unpopular opinion as I do. I'm a huge fan of review-optional PRs. Let it be up to the author to make that call and if it were really important to enforce it would be more foolproof to do so with automation.

          Unfortunately every time I've proposed this it's received like it's sacrilegious but nobody could tell me why PR reviews are really necessary to be required.

          The most ironic part is that I once caught a production-breaking bug in a PR while at FAANG and the author pushed back. Ultimately I decided it wasn't worth the argument and just let it go through. Unsurprisingly, it broke production but we fixed it very quickly after we were all finally aligned that it was actually a problem.

          • By mattmanser 2024-12-2622:031 reply

            I'll bite.

            To catch stupid mistakes like an extra file, an accidental debug flag, a missing compiler hint that has to be added to migration scripts etc.

            To ensure someone who doesn't quite understand the difference between dev and production build pipelines doesn't break it.

            To ensure a certain direction is being followed when numerous contractors are working on the code. For example a vague consistency in API designs, API param names, ordering, etc.

            To check obvious misunderstandings by juniors and new hires.

            To nix architect astronauts before their 'elegant' solution for saving a string to a database in 500 lines gets added.

            To check the code is actually trying to solve the ticket instead of a wrong interpretation of the ticket.

            To get introduced to parts of the codebase you haven't worked on much.

            But as with anything you get from it what you put in.

            • By sarchertech 2024-12-272:461 reply

              None of those are good reasons why PR reviews are necessary. They are examples of things that it's theoretically possible a PR review might catch. But there's no information there about how likely those things are to be caught.

              Without that absolutely critical information, no cost benefit analysis is possible.

              In my experience across many companies, PR reviews almost never catch any of those bugs or bring any of those benefits.

              • By mattmanser 2024-12-2811:111 reply

                Well, I catch those things.

                Now you have some information.

                If you worked with me it would be worth doing mandatory PRs.

                One trick, and I'm not being sarcastic, is to read every line. Even if you don't understand the change as a whole that catches things.

                Another trick, and again not sarcastic this is genuine advice. Read every line of your own PRs before you submit them. It's surprising how much I catch doing that. Same with git commits. It's also noticeable which of your colleagues don't do this as their PRs are the ones with obvious mistakes.

                All of this is much easier and more effective with multiple PRs per day, and breaking bigger tickets into smaller one.

                If you're constantly doing big, multi-day commits you're doing development wrong. You lose a lot of the benefits of source control and the tooling around it.

                I still do big commits sometimes, especially when refactoring, but even then it's very important to keep those big commits focused on a particular change. If I find bugs or tweak a feature I've been meaning to change, I try and make a new branch and push that to main and then rebase my feature branch off main. Or cherry pick them out before the refactor PR.

                • By sarchertech 2024-12-2811:35

                  > Read every line of your own PRs before you submit them

                  I do. Everyone should. I’m also a fan of small focused PRs.

                  > If you worked with me it would be worth doing mandatory PRs.

                  Given the number of PRs I’ve merged and the number of mistakes that reviewers have caught, I think it’s very unlikely that you’d catch those things at a frequency high enough to justify the cost.

                  I’m not doubting that you can find those things or that you have found them. But again I have merged thousands of PRs with hundreds of reviewers across numerous companies in multiple languages and I have never had a reviewer catch a major bug.

                  That’s a large enough sample size with no effect at all, that I’m going to need hard evidence to make me believe that people are finding these things at a high enough rate to justify the cost.

          • By sarchertech 2024-12-2621:351 reply

            >Unfortunately every time I've proposed this it's received like it's sacrilegious but nobody could tell me why PR reviews are really necessary to be required.

            Obvious signs of cargoculting in my opinion.

        • By sneak 2024-12-2613:391 reply

          Required PR reviews means that if someone steals your credentials, or kidnaps your child, you can't get something into production that steals all the money without someone else somewhere else having to push a button also.

          It's the two-person rule, the two nuclear keyswitches.

          • By sarchertech 2024-12-2621:34

            This is definitely not why PR reviews are required. Most companies don't really know why they require them, but I've definitely never heard one say it was because they were afraid of malicious code from stolen credentials.

            There's so many other ways you can inject malicious code with stolen credentials that doesn't require a PR in every production environment I've ever worked in. There's much lower hanging fruit that leaves far fewer footprints.

        • By ozim 2024-12-269:163 reply

          Unfortunately for compliance reasons PRs are required.

          Funny part is that not even in highly regulated markets.

          ISO270001 or SOC2 are pretty much something every software company will have to do.

          • By __turbobrew__ 2024-12-2617:50

            Yes, this is why we have required PR reviews at my company. It is to meet compliance controls.

            We recently talked about not requiring reviews for people in L5 and above levels but ultimately got shut down due to compliance.

          • By sarchertech 2024-12-2621:291 reply

            SOC2 doesn't require code reviews. SOC2 is just a certification that you are following your own internal controls. There's nothing that says required PR reviews have to be one of your internal controls. That's just a common control that companies use.

            • By ozim 2024-12-2622:151 reply

              I would argue that "common control that companies use" falls under "industry standard" and I would say it would make it harder to pass certification without PR reviews documented on GitHub or something alike. So it does not require but everyone expects you to do so :)

              • By sarchertech 2024-12-273:01

                The reason that this is common is that a company hires a SOC2 consultant who tells them that PR reviews are required despite that fact that this is a complete fabrication.

                Locking yourself into an enormously expensive process with no evidence of its efficacy just because you don't want read up on the process yourself or push back on a misinformed auditor is a terrible business decision.

          • By kevmo314 2024-12-2617:291 reply

            Curious because I am not familiar: are PRs required or are PR reviews required?

            • By ozim 2024-12-2619:37

              Well "Peer Review" or "Code Review" is required - pull requests are easiest way to have it all documented with current state of art tooling. Otherwise you have to come up with some other way to document that for purpose of the audit.

        • By JKCalhoun 2024-12-2618:35

          I agree with you. If you give each dev a kind of sand-box to "own" within a project they'll learn to find their own bugs, write both simple and robust code, lots of param checking — grow as an engineer that way.

        • By jasonlotito 2024-12-2621:172 reply

          Allowing anyone to promote anything to production without any other eyes on it is problematic. Not realizing this is extremely telling.

          The presumed claim that no one at the company benefited from a second set of eyes is amazing, too.

          • By sarchertech 2024-12-2621:48

            >Allowing anyone to promote anything to production without any other eyes on it is problematic.

            In my experience the people who are promoting things to production that shouldn't be will find a way to do it. They'll either wear down the people who want to stop it, or they'll find someone else to approve it who doesn't know why it shouldn't be approved or doesn't care.

            My hypothesis is that requiring any 2nd random engineer in the company to approve production code doesn't provide enough value to justify the cost.

            There may be other controls that are worth the cost.

            However, our industry has been shipping software for a long time without this requirement, and I've seen no evidence that the practice has saved money, reduced the number of bugs, or improved software quality by any other metric. I think it's time we examine the practice instead of taking it on faith that it's a net benefit.

            >Not realizing this is extremely telling.

            Nice way of saying, I don't agree with you so I must be an idiot.

          • By jvans 2024-12-2621:57

            but there isn't actually a second set of eyes because the second set of eyes you're thinking about is complaining about formatting or slamming the approve button without actually looking

        • By tdrz 2024-12-269:191 reply

          The fact that there is a PR review process in place, makes commiters try harder. And that's good!

          • By gizzlon 2024-12-269:341 reply

            Or try less because they have to spend time doing pr reviews

            • By queuep 2024-12-2613:53

              Yes, same for QA sometimes.. dev sets bar lower as the QA can test it. Just makes a bunch of back and forth. And when stuff breaks nobody feels responsible.

        • By zimpenfish 2024-12-2618:01

          > I have never once had a reviewer catch a major bug

          Just in 2024, I've had three or four caught[0] (and caught a couple myself on the project I have to PR review myself because no-one else understands/wants to touch that system.) I've also caught a couple that would have required a hotfix[1] without being a five-alarm alert "things are down".

          [0] including some subtle concurrency bugs

          [1] e.g. reporting systems for moderation and support

      • By charlie0 2024-12-262:391 reply

        I'm one of the rare individuals who really tries to review code and leave helpful comments. I've been on the receiving end of really big PRs and can say I understand why you're being told to break things up into smaller chunks.

        Most of the devs who submit large PRs just don't have a good grasp of organizing things well enough. I've seen this over and over again and it's due to not spending enough time planning out a feature. There will be exceptions to this, but when devs keep doing it over and over, it's the reviewer's job to reject it and send it back with helpful feedback.

        I also understand most people don't like the friction this can create and so you end you with 80% of PRs being rubber stamped and bugs getting into production because the reviewers just give up on trying to make people better devs.

        • By sneak 2024-12-2613:401 reply

          The reviewer's job is primarily to ensure business continuity, and only marginally to make people better devs.

          • By fmbb 2024-12-2617:28

            When I review code I never think I am there to make people better devs.

            I’m reviewing the code because I don’t want shit code merged into the code base I am responsible for operating. I’m going to be the one debugging that. Don’t just merge shit you feel like merging.

      • By NotBoolean 2024-12-2520:384 reply

        I don’t have your experience but I personally think some of this feedback can be warranted.

        > Can't refactor code because it changes too many files and too many lines.

        This really depends on the change. If you are just doing a mass rename like updating a function signature, fair enough but if you changing a lot of code it’s very hard to review it. Lots of cognitive load on the reviewer who might not have the same understanding of codebase as you.

        > Can't commit large chunks of well tested code that 'Does feature X', because... too many files and too many lines.

        Same as the above, reviewing is hard and more code means people get lazy and bored. Just because the code is tested doesn’t mean it’s correct, just means it passes tests.

        > Have to split everything down into a long sequence of consecutive pull requests that become a process nightmare in its own right

        This is planning issue, if you correctly size tickets you aren’t going to end up in messy situations as often.

        > The documentation comments gets nitpicked to death with mostly useless comments about not having periods at the ends of lines

        Having correctly written documentation is important. It can live a long time and if you don’t keep an eye on it can becomes a mess. Ideally you should review it before you submitting it to avoid these issues.

        > End up having to explain every little detail throughout the function as if I'm trying to produce a lecture, things like `/* loop until not valid */ while (!valid) {...` seemed to be what they wanted, but to me it made no sense what so ever to even have that comment

        I definitely agree with this one. Superfluous comments are a waste of time.

        Obviously this is just my option and you can take things too far but I do think that making code reviewable (by making it small) goes a long way. No one wants to review 1000s lines of code at once. It’s too much to process and people will do a worse job.

        Happy to hear your thoughts.

        • By lazyasciiart 2024-12-2521:154 reply

          > This is planning issue, if you correctly size tickets you aren’t going to end up in messy situations as often.

          No, it’s ā€œthis refactor looks very different to the original code because the original code thought it was doing two different things and it’s only by stepping through it with real customer data that you realized with the right inputs (not documented) it could do a third thing (not documented) that had very important ā€œside effectsā€ and was a no-op in the original code flow. Yea, it touches a lot of files. Ok, yea, I can break it up step by step, and wait a few days between approval for each of them so that you never have to actually understand what just happenedā€.

          • By withinboredom 2024-12-2522:342 reply

            so, it's not just a refactoring then; it's also bug fixes + refactoring. In my experience, those are the worst PRs to review. Either just fix the bugs, or just refactor it. Don't do both because now I have to spend more time checking the bugs you claim to fix AND your refactoring for new bugs.

            • By rcxdude 2024-12-2522:572 reply

              There are certainly classes of bugs for which refactoring is the path of lowest resistance

              • By edflsafoiewq 2024-12-260:241 reply

                The most common IME are bugs that come from some wrong conceptual understanding underpinning the code. Rewriting the code with a correct conceptual understanding automatically fixes the bugs.

                • By ludston 2024-12-261:20

                  The classic example of this is concurrency errors or data corruption related to multiple non-atomic writes.

              • By t-writescode 2024-12-260:101 reply

                And there are multi-PR processes that can be followed to most successfully convert those changes in a comprehensible way.

                It'll often include extra scaffolding and / or extra classes and then renaming those classes to match the old classes' name after you're done, to reduce future cognitive load.

                • By rcxdude 2024-12-2616:381 reply

                  I'm unconvinced that adding extra code churn in order to split up a refactor that fixes bugs into a bugfix and a refactor is worthwhile

                  • By withinboredom 2024-12-2619:132 reply

                    One metric I like to give my team is to have any new PR start a review in less than 15 minutes and be completed within 15 minutes. So, the longest you should wait is about 30 minutes for a review. That means teams either go "fuck it" and rubber stamp massive PRs -- which is a whole different issue -- or they take it seriously and keep PRs small to get their PRs reviewed in less than 30 minutes.

                    In most cases where I see responses like this, they're not surprised to wait hours or days for a PR review. In that case, it makes sense to go big, otherwise you'll never get anything done. If you only have to wait half an hour, max, for a PR review; the extra code churn is 1000% worth it.

                    • By t-writescode 2024-12-2619:561 reply

                      This is where my stance is.

                      As a developer, I want my PRs to actually be reviewed by my coworkers and to have issues caught as a second layer of defense, etc.

                      As a reviewer, I effectively stopped approving things I couldn't give at least a cursory, reasonable glance (and tried to encourage others to follow suit because if we're not reviewing things, why not just push directly to main).

                      As a consequence, I have:

                        * tried to review most things within like half an hour of their announcement
                          in the shared MR channel
                      
                        * requested a pair programming session and offered to do a pair programming
                          session for any large and semi-or-fully automated refactoring session,
                          like running a linter or doing a multi-file variable rename
                          (the pair programmer immediately comments on and approves the MR when it
                          appears)
                      
                        * tried to limit my PRs to approximately 400 lines (not a rigid rule)
                      
                      There were some specific instances of people not liking the "you must pair program if you're going to touch 400 files in one PR" requirement; but otherwise, I would like to think those on my team liked the more regular PRs, more people doing the PRs, etc, that resulted from this and some healthy culture changes.

                      I would also like to feel like the more junior devs were more willing to say anything at all in the PRs because they could follow the change.

                      • By withinboredom 2024-12-2620:11

                        I’ve seen this and variations done by teams to implement the metric. Usually, the ā€œbiggestā€ friction comes from ā€œhow do we know a PR needs to be reviewed within the time frame?ā€ To which I always want to answer: ā€œyou have a mouth, put noises through it.ā€ Sigh, sometimes I miss the military… anyway, toxic behavior aside, this is usually the biggest thing. I have to remind them that they go get coffee or smoke at least every hour, but rarely at the same time; so maybe then might be a good time to just do a quick check for an open PR. Or turn on notifications. Or if it’s urgent, mention it in the dev team channel.

                        But yeah, it’s hard to get the culture rolling if it isn’t already in place nor has anyone in the company worked with a culture like that.

                    • By rcxdude 2024-12-2621:062 reply

                      I'm all for low-latency reviews, but this target seems crazy: a perfect recipe for a lot of apparent activity for little actual progress. Maybe it depends on the project, but for a lot of projects 15 minutes of review time means you basically are only going to accept trivial changes.

                      • By t-writescode 2024-12-2621:24

                        As it turns out, most of the work that most developers do is updating or enhancing CRUD apps. There's already a plan and an intent that just needs to be typed out.

                        I've found 15-30 minutes to be plenty of time to review about a day's worth of code. It's enough time to process what the code is doing and iterate over the tests, in general.

                        Here's a scary thought: if something small takes 15-30 minutes to appropriately process ... how much longer do *large* changes take? Can someone keep all that in their mind that whole time to comprehend and process a huge change?

                        And a better question, will they?

                      • By withinboredom 2024-12-2621:59

                        > 15 minutes of review time means you basically are only going to accept trivial changes.

                        Um, yes. This is 100% the point. There is no amount of refactoring, bug fixing, or features that cannot be expressed as a chain of trivial changes.

                        What you usually see happen is that instead of spending a week experimenting with 15 different refactors, is that an engineer opens a PR with what they think they're going to try first. Other engineers point out how they had tried that before and it didn't work; but maybe this other way will. So, they end up "working together" on the refactor instead of one developer getting lost in the sauce for a week seeing what sticks to a wall.

                        In essence, about the same amount of time is spent; but the code is higher quality and no architecture reviews during code reviews (which is another rule that should exist on a team -- architecture reviews should happen before a single line of code is touched).

            • By lazyasciiart 2024-12-271:39

              No, I very deliberately did not describe any bug fixes.

          • By justatdotin 2024-12-261:181 reply

            > only by stepping through it with real customer data that you realized with the right inputs (not documented) it could do a third thing (not documented) that had very important ā€œside effectsā€ and was a no-op in the original code flow

            sounds like the 'nightmare' was already there, not in the refactor. First step should be some tests to confirm the undocumented behaviour.

            Some of your complaints seem to be about peer review ('approval'). I found my work life improved a lot once I embraced async review as a feature, not a bug.

            As for 'break it up step by step' - I know how much I appreciate reviewing a feature that is well presented in this way, and so I've got good at rearranging my work (when necessary) to facilitate smooth reviews.

            • By lazyasciiart 2024-12-271:35

              > sounds like the 'nightmare' was already there, not in the refactor

              I admit that I am pretty allergic to people who avoid working with imperfect code.

          • By grey-area 2024-12-2521:452 reply

            The way I normally approach this is one big pr for context and then break it into lots of small ones for review.

            • By jaredsohn 2024-12-2523:16

              I've found processes like this to work better, too. Basically, the one big pr is like building a prototype to throw away. And the benefit is it has to get thrown away because the PR will never pass review.

            • By F-W-M 2024-12-2523:121 reply

              A PR with self-contained smaller commits would be possible as well.

              • By t-writescode 2024-12-260:11

                Yes, though it does depend on how good the commenting system is; and, for something like that, you're still probably going to want a meeting to walk people through such a huge change.

                And you'd better hope you're not squashing that monstrous thing when you're done.

        • By quesomaster9000 2024-12-2520:472 reply

          I do object to the notion of something being a planning issue when you're talking about a days worth of work.

          Implement X, needs Y and Z, ok that was straightforward, also discovered U and V on the way and sorted that out, here's a pull request that neatly wraps it up.

          Which subsequently gets turned into a multi-week process, going back & forth almost every day, meaning I can't move on to the next thing, meanwhile I'm looking at the cumulative hourly wages of everybody involved and the cost is... shocking.

          Death by process IHMO.

          • By bspammer 2024-12-2521:173 reply

            > Implement X, needs Y and Z, ok that was straightforward, also discovered U and V on the way and sorted that out, here's a pull request that neatly wraps it up

            This sounds very difficult to review to be honest. At a minimum unrelated changes should be in their own pull request (U and V in your example).

            • By tacitusarc 2024-12-261:441 reply

              I work as a tech lead, so I get a lot of leeway in setting process. For small PRs, we use the normal ā€œleave comments, resolve commentsā€ approach. For large PRs, we schedule 30m meetings, where the submitter can explain the changes and answer questions, and record any feedback. This ensures everyone is on the same page with the changes, gives folks a chance to rapidly gather feedback, and helps familiarize devs who do not work in that area with what is going on. If the meeting is insufficient to feel like everyone is on the same page and approves the changes, we schedule another one.

              These are some of the best meetings we have. They are targeted, educational, and ensure we don’t have long delays waiting for code to go in. Instead of requiring every PR to be small, which has a high cost, I recommend doing this for large/complex projects.

              One additional thing to note on small PRs: often, they require significant context, which could take hours or even days, to be built up repeatedly. Contrast that with being able to establish context, and then solve several large problems all at once. The latter is more efficient, so if it can be enabled without negative side effects, it is really valuable.

              I want my team to be productive, and I want to empower them to improve the codebase whenever they see an opportunity, even if it is not related to their immediate task.

              • By quesomaster9000 2024-12-261:57

                One minor piece of insight from me is about release management vs pull-requests.

                As you say it's much easier to schedule a 30 minute meeting, then we can - with context - resolve any immediate nitpicks you have, but we can also structure bigger things.

                'Would this block a release?'

                'Can we just get this done in the PR and merge it'

                'Ok, so when it's done... what is the most important thing that we need to document?'

                Where the fact that even after it's merged, it's going to sit in the repo for a while until we decide to hit the 'release' button', this lets people defer stuff to work on next and defines a clear line of 'good enough'

            • By shakna 2024-12-260:05

              How do you rework a core process, then? If you rework a major unit that touches just about everything... Sharding something like that can break the actual improvement it is trying to deliver.

              Like... Increase the performance of a central VM. You'll touch every part of the code, but probably also build a new compiler analysis system. The system is seperate to existing code, but useless without the core changes. Seperating the two can ruin the optimisation meant to be delivered, because the context is no longer front and center. Allowing more quibling to degrade the changes.

            • By pbh101 2024-12-2521:43

              Agree. Another item here that is contextual: what is the cost of a bug? Does it cost millions, do we find that out immediately, or does it take months? Or does it not really matter, and when we’ll find the big it will be cheap? The OP joining a new company might not have the context that existing employees have about why we’re being cautious/clear about what we’re changing as opposed to smuggling in refactors in the same PR as a feature change.

              I’m going to be the guy that is asking for a refactor to be in a separate commit/PR from the feature and clearly marked.

              It doesn’t justify everything else he mentioned (especially the comments piece) but once you get used to this it doesn’t need to extend timelines.

          • By justatdotin 2024-12-261:25

            Yes, wrapping other discoveries into your feature work is a planning issue that might impact on the review burden.

        • By callc 2024-12-2521:311 reply

          > This is planning issue, if you correctly size tickets you aren’t going to end up in messy situations as often.

          I think the underlying issue is what is an appropriate ā€œunit of workā€. Parent commenter may want to ship a complete/entire feature in one MR. Ticketing obsessed people will have some other metric. Merge process may be broken in this aspect. I would rather explain to reviewer to bring them up to speed on the changes to make their cognitive load easier

          • By gjadi 2024-12-2522:45

            This. The solution to long and multiple reviews to MR is single pair review session where most of the big picture aspects can be addressed immediately and verbally discussed and challenged.

            IMHO it is the same as chat. If talking about an issue over mail or chat takes more than 3-5 messages, trigger a call to solve it face to face.

        • By 8note 2024-12-260:45

          code reviews that are too small, i think are worse than ones that are too big, and let through more bugs.

          10 different reviewers can each look at a 100 lin change out of the 1000 line total change, but each miss how the changes work together.

          theyre all lying by approving, since they dont have the right context to approve

      • By flakes 2024-12-2522:041 reply

        > The documentation comments gets nitpicked to death with mostly useless comments about not having periods at the ends of lines > End up having to explain every little detail throughout the function

        For these cases I like to use the ā€˜suggest an edit’ feature on gitlab/github. Can have the change queued up in the comments and batch commit together, and takes almost no additional time/effort for the author. I typically add these suggestion comments and give an approve at the same time for small nitpicks, so no slow down in the PR process.

        • By F-W-M 2024-12-2523:101 reply

          I good process would be to just push the proposal to the branch in review.

          • By flakes 2024-12-260:39

            I still want to let the author have the final say on if they decide to accept or reject the change, or modify it further. Editing the branch directly might cause some rebasing/merge conflicts if they’re addressing other peoples comments too, so I don't typically edit their working branch directly unless they ask me to.

      • By lifeisstillgood 2024-12-2521:162 reply

        I am trying my best to build in an inordinate amount of upfront linting and automated checks just to avoid such things - and then I still need to do a roadshow, or lots of explanations- but that’s probably good.

        But the good idea is to say ā€œwe all have the same brutal linting standards (including full stops in docs!) - so hopefully the human linger will actually start reading the code for what it is, not what it saysā€

        • By whstl 2024-12-2521:421 reply

          I'm also a fan of linting everything. Custom linter rules ftw.

          This and documenting non-lintable standards so that people are on the same page ("we do controllers like this").

          This is how I like to build and run my teams. This makes juniors so much more confident because they can ship stuff from the get go without going through a lengthy nitpicky brutal review process. And more senior devs need to actually look at code and business rules rather than nitpicking silly shit.

          • By t-writescode 2024-12-260:17

            > This makes juniors so much more confident because they can ship stuff from the get go without going through a lengthy nitpicky brutal review process.

            I had not considered that linters could greatly help new developers in this way, especially if you make it a one-button linting process for all established development environments.

            Thanks for the insight! I will use this for the future.

        • By justatdotin 2024-12-261:32

          if a colleague wants to argue over placement of a curly boy, I'll fight to the death.

          if it's a linter, I shrug and move on.

      • By notShabu 2024-12-2521:46

        there is huge incentive for people who don't know how to code/create/do-stuff to slow things down like this b/c it allows them many years of runway at the company.

        they are almost always cloaked in virtue signals.

        almost every established company you join will already have had this process going for a long time.

        doing stuff successfully at such a company is dangerous to the hierarchy and incurs an immune response to shut down or ostracize the doing-of-stuff successfully so the only way to survive or climb is to do stuff unsuccessfully (so they look good)

      • By spion 2024-12-2521:28

        Indeed, cognitive load is not the only thing that matters. Non-cognitive toil is also a problem and often enough it doesn't get sufficient attention even when things get really bad.

        We do need better code review tools though. We also need to approach that process as a mechanism of effectively building good shared understanding about the (new) code, not just "code review".

      • By tdiff 2024-12-2612:09

        As a reviewer I've seen numerous examples of PRs that were basically out of sync with the rest of the project, did not solve the problem they were supposed to solve, or added buggy or unmaintainable code.

        Arguments like "but it works in majority of cases" are a way to delegate fixing issues to somebody else later. Unless noone will be using that code at all, in which case it should not be merged either.

      • By MarkMarine 2024-12-2522:122 reply

        I’m 15 years in and I feel basically the same. I end up making a feature or change, then going back and trying to split it into chunks that are digestible to my colleagues. I’ve got thousands of lines of staged changes that I’m waiting to drip out to people at a digestible pace.

        I yearn for the early stage startup where every commit is a big change and my colleagues are used to reviewing this, and I can execute at my actual pace.

        It’s really changed the way I think about software in general, I’ve come around to Rich Hickey’s radically simple language Clojure, because types bloat the refactors I’m doing.

        I’d love to have more of you where I work, is there some way I can see your work and send some job descriptions and see if you’re interested?

        • By withinboredom 2024-12-2522:302 reply

          > I end up making a feature or change, then going back and trying to split it into chunks that are digestible to my colleagues.

          If you are doing this AFTER you've written the code, it is probably way easier to do it as you go. It's one thing if you have no idea what the code will look like from the beginning -- just go ahead and open the big PR and EXPLAIN WHY. I know that I'm more than happy to review a big PR if I understand why it has to be big.

          I will be annoyed if I see a PR that is a mix of refactoring, bug fixes, and new features. You can (and should) have done those all as separate PRs (and tickets). If you need to refactor something, refactor it, and open a PR. It doesn't take that long and there's no need to wait until your huge PR is ready.

          • By quesomaster9000 2024-12-2522:531 reply

            Solving creative problems is often iterative, and one things I'm very concerned about when doing engineering management is maintaining momentum and flow. Looking at latency hierarchies is a really good example, you have registers, then cache, then memory, SSD, network etc. and consulting with another human asynchronously is like sending a message to Jupiter (in the best case).

            So, with an iterative process, the more times you introduce (at best) hour long delays, you end up sitting on your arse twiddling your thumbs doing nothing, until the response comes back.

            The concept of making PRs as you go fails to capture one of the aspects of low-latency problem solving, which is that you catch a problem, you correct it and you revise it locally, without exiting that loop. Which is problematic because not only have you put yourself in a situation where you're waiting for a response, but you've stopped half-way through an unfinished idea.

            This comes back to 'is it done', a gut feel that it's an appropriate time to break the loop and incur the latency cost, which for every developer will be different and is something that I have grown to deeply trust and and adjust to for everybody I work with.

            What I'm getting at is the iterative problem solving process often can't be neatly dissected into discrete units while it's happening, and after we've reached the 'doneness' point it takes much more work to undo part of your work and re-do it than it took to do originally, so not only do you have the async overhead of every interaction, but you have the cognitive burden of untangling what was previously a cohesive unit of thought - which again is another big time killer

            • By withinboredom 2024-12-266:55

              What I mean is, you make your commit, cherry pick it over to the main branch, and open a draft pr. It doesn't break your flow, it doesn't stop anything, and is pretty quick. It also gives you a quick gut-check to see the PR; if you think your team members won't understand "why" it needs to be refactored, then you have one of two problems:

              1. your refactoring is probably going in the wrong direction. Team members will be able to help here more than ever. Let them bikeshed, but don't stop working on your main refactor yet. Revist later and integrate their changes.

              2. the PR is too small. it will have to be part of a larger PR.

              In my experience, people tend to have the first problem, and not the second one, but they think they have the second one. There are many of these "massive refactoring" PRs I've reviewed over the last 20 years where the refactoring makes the code worse, overall. Why? Because refactoring towards a goal (implementing a feature, fixing a bug, etc.) doesn't have the goal refactoring should have: improving code maintainability. So, the refactored code is usually LESS maintainable, but it does what they wanted.

          • By __MatrixMan__ 2024-12-2523:411 reply

            If you make refactor PRs as you go, do you end up merging redactors towards a dead end and then--once you realize it's a dead end--merging even more refractors in the other direction?

            I usually wait until I have the big PR done and then merge redactors towards it because then at least I know the road I'm paving has a workable destination.

            • By t-writescode 2024-12-260:08

              This is why I design the heckin' huge change at the start, and then cherry pick the actual change (and associated tests) into a ton of smaller PRs, including "refactor here", "make this function + tests", "make this class + tests", "integrate the code + tests", and so on, as many times as necessary to have testable and reviewable units of code.

              If I went about and made a ton of changes that all went into dead ends, honestly, I would get pretty demoralized and I think my team would get annoyed, especially if I then went through and rolled back many of those changes as not ending up being necessary.

        • By hellisothers 2024-12-2523:161 reply

          These same people also want to see your GitHub history filled with deep green come review time. I start to wonder if they think high levels of GitHub activity is a proxy of performance or if it’s a proxy of plying the game the way they insist you play.

          • By MarkMarine 2024-12-263:22

            Dunno where you get that from, but that was not my intent and is not a metric I use to judge who I’d like to be my coworkers.

      • By epolanski 2024-12-2521:47

        You seem to be describing a company where bureaucracy is a feature not a bug.

        Been there. Left, live thousands times better.

      • By gre 2024-12-260:291 reply

        The process is introducing more room for bugs to somehow creep in. Damn.

        • By quesomaster9000 2024-12-260:49

          This is a big problem with reviews where the author is capitulating because they, with gritted teeth, acknowledge it's the only way to get the desired result (jumping over a hurdle).

          So you blindly accept an ill-informed suggestion because that's the only way you can complete the process.

      • By jschrf 2024-12-260:051 reply

        Aye. Sign of the times. You're 20+ years in, so I'm preaching to the choir and old-man-yelling-at-cloud here.

        Cargo culting + AI are the culprits. Sucks to say, but engineering is going downhill fast. First wave of the shitularity. Architects? Naw, prompt engineers. Barf. Why write good code when a glorified chatbot could do it shittier and faster?

        Sign of our times. Cardboard cutout code rather than stonemasonry. Shrinkflation of thought.

        Peep this purified downvote fuel:

        Everything is bad because everyone is lazy and cargo cults. Web specifically. Full-stop. AI sucks at coding and is making things recursively worse in the long run. LLMs are nothing more than recursive echo chambers of copypasta code that doesn't keep up with API flux.

        A great example of this is the original PHP docs, which so, so many of us copypasta'd from, leading to an untold amount of SQL injections. Oopsies.

        Simalarily and hunting for downvotes, React is a templating framework that is useful but does not even meet its original value proposition, which is state management in UI. Hilariously tragic. See: original example of message desync state issue on FB. Unsolved for years by the purported solution.

        The NoSQL flash is another tragic comedy. Rebuilding the wheel when there is a faster, better wheel already carefully made. Postgres with JSONB.

        GraphQL is another example of Stuff We Don't Need But Use Because People Say It's Good. Devs: you don't need it. Just write a query.

        -

        You mention a hugely important KPI in code. How many files, tools, commands, etc must I touch to do the simplest thing? Did something take me a day when it should have taken 30s? This is rife today, we should all pay attention. Pad left.

        Look no further than hooks and contexts in React land for an example. Flawed to begin with, simply because "class is a yucky keyword". I keep seeing this in "fast moving" startups: the diaspora of business logic spread through a codebase, when simplicity and unity is key, which you touch on. Absolute waste of electricity and runway, all thanks to opiniation.

        Burnt runways abound. Sometimes I can't help but think engineering needs a turn it off and then on again moment in safe mode without fads and chatbots.

        • By sgarland 2024-12-262:421 reply

          > Everything is bad because everyone is lazy and cargo cults.

          It’s an interesting series of events that led to this (personal theory). Brilliant people who deeply understood fundamentals built abstractions because they were lazy, in a good way. Some people adopted those abstractions without fully comprehending what was being hidden, and some of those people built additional abstractions. Eventually, you wind up with people building solutions to problems which wouldn’t exist if, generations above, the original problem had been better understood.

          • By quesomaster9000 2024-12-263:281 reply

            The road is paved with good intentions, it's not they were lazy but they had intent to distill wisdom to save time. Then yes, the abstractions were adopted without fully comprehended what was hidden, and those people then naively built additional layers of abstractions.

            So yes, if the original problem had been better understood, then you wouldn't have a generation of React programmers doing retarded things.

            Having watched many junior developers tackle different problems with various frameworks, I have to say React is conducive to brainrot by default. Only after going through a fundamentals-first approach do you not end up with one kind of spaghetti, but you end up with another kind because it's fundamentally engineered towards producing spaghetti code unless you constantly fight the inertia of spaghettification.

            It's like teaching kids about `GOTO`... That is, IMO, the essence of React.

            • By sgarland 2024-12-2615:05

              > it's not they were lazy but they had intent to distill wisdom to save time.

              Yes – I was referring to lazy in the sense of the apocryphal quote from Bill Gates:

              ā€œI choose a lazy person to do a hard job, because a lazy person will find an easy way to do it.ā€

              > Only after going through a fundamentals-first approach do you not end up with one kind of spaghetti, but you end up with another kind because it's fundamentally engineered towards producing spaghetti code unless you constantly fight the inertia of spaghettification.

              I’ve been guilty of this. Thinking that a given abstraction is unnecessary and overly-complicated, building my own minimal abstraction for my use case, and then slowly creating spaghetti as I account for more and more edge cases.

      • By spockz 2024-12-2521:38

        This sounds more like a case where you need a ā€œbreak-the-glassā€ like procedure where some checks don’t apply. Or the checks should be non blocking anyway.

      • By jesse__ 2024-12-261:29

        I've had a similar experience several times over the years. Even at companies with no working product that ostensibly wanted to 'move fast and break things'. And I do the same thing; quit and move on. I'm pretty convinced people like that more-or-less can't be reasoned with.

        My question is .. is this getting more common as time goes on, or do I just feel like it is..

      • By shinycode 2024-12-2521:221 reply

        No wonder why software development used to be expensive if 50 lines of code takes multiples days for several people …

        • By LtWorf 2024-12-2522:412 reply

          Well maybe they do critical systems.

          • By shinycode 2024-12-2523:25

            Valid point, it’s even mandatory in this case. Sometimes people do it for the sake of it. Maybe because there nothing else to make them feel important ? In critical systems I hope it’s the case though

          • By DavidPiper 2024-12-2523:44

            Narrator: "They don't."

            (Glib, but in my experience, mostly true.)

      • By romellem 2024-12-263:41

        > mostly useless comments about not having periods at the ends of lines

        Oh my god, this sounds like a nightmare. I definitely would not be able to tolerate this for long.

        Did you try to get them to change? Were you just not in a senior enough position for anyone to listen?

      • By SeptiumMMX 2024-12-2520:54

        You always need to look at the track record of the team. If they were not producing solid consistent results before you joined them, it's a very good indicator that something's fishy. All that "they are working on something else that we can't tell you" is BS.

        If they were, and you were the only one treated like that, hiring you was a decision forced upon the team, so they got rid of you in a rather efficient way.

      • By nosefurhairdo 2024-12-2520:51

        That's rough. Of course some amount of thoughtfulness towards "smallest reasonable change" is valuable, but if you're not shipping then something is wrong.

        As for the "comments on every detail" thing... I would fight that until I win or have to leave. What a completely asinine practice to leave comments on typical lines of code.

    • By charles_f 2024-12-260:15

      I like to call these smells, not rules. They're an indication that something might be wrong because you've repeated code, or because your method is too long, or because you have too many parameters. But it might also be a false positive because in this instance it was acceptable to repeat code or have a long method or have many parameters.

      Sometimes food smells because it turned bad, and sometimes it's smelly because it's cheese.

    • By nradov 2024-12-2520:23

      It's the same with writing. The best authors occasionally break the rules of grammar and spelling in order to achieve a specific effect. But you have to learn the rules first, and break them only intentionally rather than accidentally. Otherwise your writing ends up as sloppy crap.

      (Of course some organizations have coding conventions that are just stupid, but that's a separate issue.)

    • By deergomoo 2024-12-2520:111 reply

      Same deal with DRY, the principle is obviously correct but people can take it too literally. It's so easy to get yourself in a huge mess trying to extract out two or three bits of code that look pretty similar but aren't really used in the same context.

      • By skeeter2020 2024-12-2520:26

        The problem with DRY and generic rules around size, etc. really seems to be figuring out the boundaries, and that's tough to get right, even for experienced devs, plus very contextual. If you need to open up a dozen files to make a small change you're overwhelmed, but then if you need to wade through a big function or change code in 2 places you're just as frustrated.

    • By dennis_jeeves2 2024-12-260:50

      >It almost always comes from a lead or manager who reads too many business books and then cargo cults those books on to the team.

      Worse, they behave as though they have profound insights, and put themselves on an intellectually elevated pedestal, which the rest of their ordinary team mortals cannot achieve.

    • By diekhans 2024-12-2620:401 reply

      Hard rules are the problem. There is a lot of "it depends."

      After over 40 years of programming, I continue to reduce the size of functions and find it easier to write and understand when I return to them. Ten lines are now a personal guideline.

      However, a linear function with only tiny loops or conditionals can be easily understood when hundreds of lines are long, but not so much with nested conditionals and loops, where there is natural decomposition into functions.

      I observed that the same guidelines became rules problems when test coverage became popular. They soon became metrics rather than tools to think about code and tests. People became reluctant to add sanity check code for things that could should never happen because it brought down code coverage.

      • By asdff 2024-12-2620:53

        There are certainly functions written too cleverly to be apparent how they manage to work at all in a few lines. By my own hand six months ago sometimes. The solution is an unsexy one but always works: write a books worth of comments near that function code that explains absolutely everything and why it was done.

    • By raincole 2024-12-2521:412 reply

      I've seen a book promoting the idea that methods should not be longer than 5 lines.

      Of course now I know these ridiculous statements are from people hardly wrote any code in their lives, but if I'd read them at 18 I would have been totally misled.

      • By quesomaster9000 2024-12-260:11

        Weirdly if you do break everything down into purely functional components it's entirely possible to uncompromisingly make every concept a few lines of code at most, and you will end up with some extremely elegant solutions this way.

        You wouldn't be misled at all, only that the path you'd go down is an entirely different one to what you expected it to be.

      • By arzke 2024-12-2612:591 reply

        > I know these ridiculous statements are from people hardly wrote any code in their lives

        Some people who actually wrote a decent amount of code in their lives are sharing that opinion, so your comment just sounds like an ad-hominem attack.

        • By wholinator2 2024-12-2617:42

          I disagree that it's an attack, I've also never heard anyone say methods should be less than 5 lines. 5 lines is an insane limit, 15 is much more reasonable. This kind of enforcement reeks to me of unnecessarily "one-lining" complicated statements into completely unreadable garbage. I mean seriously though, 5 lines? Why not 4, or 3, or 6? 15 lines of well thought out code is infinitely preferable to 3 different 5-line monstrosities. Who(m'st've) among us that actually writes code would preach such a guideline, and can i please see their code for reference. Maybe they are just better than us, i still don't think that makes it a reasonable general rule. And i disagree that calling that out as crazy counts as a personal ad-hominem attack against this nebulous entity

    • By asdff 2024-12-2620:50

      Doing things the right way always introduces a shackle to your ankle. Oh am I to package my functions as discrete packages I call via library name carefully crafted to install into some specific folder structure that I now have to learn and not make mistakes with. Or I can do it ā€œimproperlyā€ and just write a function and start using it immediately.

      Not everything has to be designed like some standardized mass produced part ready to drop into anything made in the last 40 years. And what is crazy is that even things written to that standard aren’t even compatible and might have very specific dependencies themselves.

    • By psychoslave 2024-12-2521:13

      If a function is longer than what I can display on a single screen, it better has to be argumented with very exceptional relevant requirements, which is just as straight forward to judge for anyone with a bit of experience.

    • By ozim 2024-12-269:05

      In my experience it usually devs that do that to themselves after reading stuff on the internet and thinking ā€œI want to be a professional and I want to show it to everyone.ā€

      Then rules stay and new people just continue with same silly rules instead of thinking if those are really that useful.

    • By zahlman 2024-12-266:18

      I'm an experienced developer and I enforce these kinds of rules upon myself without giving it much thought, and I very much prefer the results.

    • By dboreham 2024-12-2523:52

      You're confusing this with a software development process problem. It's really just good old fashioned psychological abuse.

  • By nbyron 2024-12-2523:542 reply

    The spirit of this piece is excellent, and introduces some useful terms from psychology to help codify - and more importantly, explain - how to make tasks less unnecessarily demanding.

    However, as someone who spends their days teaching and writing about cognitive psychology, worth clarifying that this isn’t quite correct:

    Intrinsic - caused by the inherent difficulty of a task. It can't be reduced, it's at the very heart of software development.

    Intrinsic load is a function of the element interactivity that results within a task (the degree to which different elements, or items, that you need to think about interact and rely upon one another), and prior knowledge.

    You can’t really reduce element interactivity if you want to keep the task itself intact. However if it’s possible to break a task down into sub tasks then you can often reduce this somewhat, at the expense of efficiency.

    However, you can absolutely affect the prior knowledge factor that influences intrinsic load. The author speaks of the finding from Cowan (2001) that working memory can process 4+—1 items simultaneously, but what most people neglect here is that what constitutes an ā€œitemā€ is wholly depending upon the schemas that a given person has embedded in their long-term memory. Example: someone with no scientific knowledge may look at O2 + C6H12O6 -> CO2 + H2O as potentially up to 18 items of information to handle (then individual characters), whereas someone with some experience of biology may instead handle this entire expression as a single unit - using their knowledge in long-term memory to ā€œchunkā€ this string as a single unit - ā€˜the unbalanced symbol equation for respiration’.

    • By energy123 2024-12-260:53

      Another good article, not directly related to work tasks, but related to unnecessary complexity: https://news.ycombinator.com/item?id=30802349

    • By ilrwbwrkhv 2024-12-262:271 reply

      Another interesting thing is when there is inherent complexity in the system, things remain simple.

      For example in game programming, nobody is doing function currying.

      And yet in React and frontend land because it is a button on screen which toggles a boolean field in the db, there are graphs, render cycles, "use client", "use server", "dynamic islands", "dependency arrays" etc. This is the coding equivalent of bullshit jobs.

      • By afarviral 2024-12-2710:23

        What's the alternative in front-end? I had assumed those things were needed to essentially reverse engineer the web to be more reactive and stateful? Genuinely want it to be simpler.

HackerNews