That https://grugbrain.dev website I think is pretty ignorant. I know the idea was to be silly, and follow the meme of “old, wizened guru programmer realizes things are actually easier and better in software if you keep things simple”… except things aren’t simple, sorry, that’s the gig. I’m concerned people will read this as their desired career end state.
Yes, some good lessons and ideas in there, but some are just ignorant and wrong. Like all programming lessons and ideas. And they change over time as well as you change. Let’s tackle the bad parts in order they’re written and add nuance to where they didn’t.
Complexity
Nobody who’s ever had to maintain their own + others created complexity intentionally creates complexity. Let’s assume benevolent intent here. Programmers like puzzles. Some programmers like playing with new tech. Some programmers have no choice and HAVE to sneak in new tech so they can get experience with it on their resume so they can keep their job, or get a new one.
Worse, most programming is about trade offs, and many decisions made were the right ones made at the time, but later are perceived as wrong. Additionally, projects tend to grow AND atrophy over time. If they’re not, and they’re done, the programmers are typically working on something else and don’t have time, or mental energy, to improve things on the older projects. Sometimes the programmers that come to the project later have none of that context, and have an incorrect view of the project, thinking it’s “complex”, and not knowing why it’s way it is now. See Chesterton’s Fence.
Programmers who have a few projects under their belt where they’ve had to maintain code, they intentionally will avoid complexity, this is obvious. However, to solve business problems, we need to write code, use libraries, use frameworks, and use infrastructure. That’s all complex in it’s own right, and more complex when put together. You have to create complex things, no way around it. How you manage that complexity is part of our job. Sometimes we don’t, but assume that’s an accident, or came from good intentions. Sometimes it was done when we were at a certain skill level, and later learned and got better. Again, no ill will, here. Perhaps the coder, like many, had no one, Tech Lead, Engineering Manager, or Senior invested in their growth. They may have been on their own and did the best they could with what they had.
Saying No
Saying no to features makes no sense. Building features are what we do in software development. Push back against bad features, features that make no sense, features that intentionally are wrong because the Product Owner/Business Analyst doesn’t know any better? Sure absolutely. The key here, though, is not to be standoffish, but rather work with your stake holder to do the right features.
Problem? We can’t. There is a ton of literature on the web and in books showing that what we know about features is wrong. Our understanding, our sense that it’s correct for our users, our idea of how to implement “correctly” in code, and estimations… are all wrong and fabricated. Again, this isn’t coming from a bad place. You have to make your best guess or inference else you’d never build anything. You have to give an estimate to stake holders so they can communicate a timeline. Everyone who’s been at this awhile, even non-programmers, knows all estimates are bullshit and you won’t get better at it over time. Less worse? Yes. More accurate? No. Unless it’s super simple, super repeatable, known, and minuscule work.
Yes, there are attempts like Flow Metrics, Monte Carlo Simulations, and the No Estimates movement to help give better estimates. Those are hard, outside the domain of coding and more in the Project Management realm, and not immediately helpful to verifying the features are right.
The better plan of action is to say YES, but only after y’all have done your best to get a good sense of what you’re agreeing to, KNOWING collectively y’all will be wrong, and that is exciting, not horrible. Saying “yes” will not condone technical debt and complexity into your code; the opposite. You’ll lay some tracks, learn, iterate, and then refactor. Thinking “more features == more complexity” is a gross simplification, and warping of what’s really going on in code, and is NOT at all a solution. Learn testing, refactoring, abstraction, continuous deployment; tools we use to battle complexity demons. The word “No” can help too, sure, but it’s not the ONLY weapon and isn’t used in ALL contexts.
Thinking you’ll say “yes”, then under deliver, and later wished you had said “no” means you’re not learning how to do software better; you’re instead choosing to do less software. That’s not a magic word, that’s ignoring reality. Get better at software vs. doing less software. You get better at something by doing MORE OF IT.
Sometimes the complexity can be moved where it’s less complex. “Move that into the API” so the UI is simpler. “Move that into the UI” so the API is simpler. Sometimes the complexity can’t shrink, but where it’s placed can help a lot. As you learn more parts of the stack, you can learn opportunities where certain parts can remove complexity, or perhaps just ensure it doesn’t hurt others better than other parts.
Factoring
The refactoring advice is just not very helpful. When you first start out programming, you have no idea about “emergent understanding”. Meaning, you don’t understand what you’re building. Over time, you learn “this is what the user mostly wants, this is how they do it, and this is how my code works”. Note the “over time” and it’s also in hindsight; that can be 2 weeks, 2 months, not “Day 1 of Sprint Planning”. After you do this enough times, you realize that it will take weeks for you to fully understand both the feature AND the best way to code it within a reasonable time frame. So instead of trying to spend inordinate amount of time “getting the architecture correct the first time”, you intentionally wait for it to emerge as your own understanding of both the feature and the shape of the code manifests over time.
Except that’s a gross over simplification and feels like wizened, old smart guru dev advice and it’s actually a disservice. You should be refactoring ALL. THE. TIME.
When people say “refactoring”, like all words in programming, it’s quite meaningless. The word is contextual, and all projects and developers are different despite the appearance “we’re all writing software”. For example, one programmer who does not practice TDD, does not use CICD, and has non negotiable deadlines because their projects are on fixed bid contracts typically 4 months in length has a completely different view of the world than someone who does TDD, CICD, is paid by the hour, and is halfway through their 1 year gig.
So what does “refactoring” mean really? Mine is from the Test Driven Development crowd; it’s step 3 of “Red Green Refactor”; make the test fail, make it pass with the least amount of code possible, then continually make it better till you can’t anymore.
What does “you can’t anymore” mean? That is also contextual, and impossible to define because it means something different for someone else on a different project in a different company at a particular skill level. My current client using Node.js: When it works, follows our code standards, is documented, and doesn’t look ‘obviously messy’ (whatever that means). For a Go dev on a high TPS API serving multiple other API’s and UI’s? COMPLETELY different.
What is clear, though, is we continually do this on small parts of the code, throughout the entire code base as we learn more. As things emerge. Both. This includes the small things like unit tests on functions or class methods as well as large things like “man, these acceptance tests are hard because the database setup code is hard to work with, let’s improve that”.
What Grug is trying to say here, but comes across as a moron, intentionally, in an attempt to be more clear and concise, but failing because programming is a complex topic, is working in large architecture batches before you have a clear understanding of what you’re building, is bad. I agree. However, I don’t agree that Grug’s use of the word refactoring here is helpful, and doesn’t match the Red Green Refactor definition, which IS helpful.
Additionally, Grug is super hard on himself in this paragraph, and Grug should have the opposite view; PROUD of himself. Made an abstraction too early? Great! He learned it was the wrong abstraction, and can refactor it back out again, or create a new one. You are NOT locked into an abstraction, and changing code to be better as you learn IS NORMAL, and you should keep going. Those aren’t mistakes; those aren’t bad; they’re ALL good. The more you make, the faster you’re course correcting to understanding what you should be doing. It’s a good sign.
The diss against Big Brain devs who over abstract is more complex than Grug lets on. There’s a ton to unpack from Grug’s trauma here, which I don’t have room for. You can’t just summarize “abstract too early == bad”. For you JavaScript devs, the == vs === was intentional here. Suffice to say, those who don’t have good test practices can abstract too early or make the wrong one, and be quite stuck. It can be super hard to un-abstract it, and change it to a different abstraction. If you don’t have testing and refactoring skills, yeah… that can be super hard. Worse on legacy code with little to no tests, but tons of executive eyeballs.
Angry at the developer who hasn’t had to maintain over complexity, so they instead say, “No” to prevent the dev from hurting themselves. This is actually a disservice. Unless the developer maintains their own mess, they’ll never learn, never grow. Sometimes learning the hard way is the right way. The challenge as a Tech Lead is to limit the blast radius of this complexity, but still allow the dev the opportunity to learn. Good on Grug for trying various tactics, specifically working code within 1 day. The ideal is 15 minutes, but that takes practice. Assuming Grug knows how to get code working in 15 minutes, he should be working with that other dev to teach them how. However, by the sound of it, they practice feature branching, hence “Grug does his work, other dev does their work, we merge later”. Better to pair program or mob program, but some don’t want too.
There’s a lot of important concepts Grug either doesn’t elucidate on, or perhaps he isn’t even aware of, in the “demo in 1 day” strategy. For starters, it forces the developer to work in small batches. That is one of the best skills any dev to learn, regardless of language used.
Second, working code trumps ideas about working code. Often a coder’s ambitions greatly exceed the getting working code that actually does it. Forcing working code at some point grounds you, brings you back to reality, and more importantly, resets your current thing you should be working on vs. waiting days to find out you went way off course.
Remember, if you “know better”, then teach your co-workers. Their skills can help you. They’re on your team. When they get better, they help you. Your investments, even if you suck at teaching, will pay off. At worst, you still pointed them in the right direction.
Avoid prototypes. This makes it ok to write crap code, then suddenly it’s being shipped to production. Of course, say the word prototype to get PM’s off your back or so it’s easier to swallow for clients, but do not compromise engineering discipline.
Test After vs Test First
Grug doesn’t get TDD. If Grug wasn’t so convinced he knows what he’s doing, then he could learn how it teaches you to discover and model your domain. I had the same problem as Grug, for YEARS. How can I test first if I don’t know what I’m testing, hell, what I’m even building!? TDD has emergent properties too, though. The code you write is not concrete. You reserve the right to delete, nuke, and rewrite. Keep in mind, we’re not saying rewrite in the context of “Take this 5 year old code base and start over”, we’ll talking about taking these 4 lines of code, and making them 6 to make the code more clear and easier to debug, or 4 to make it more efficient, or 0 because we coded it wrong the first time.
Test After sucks. Code that isn’t tested first is harder to test. Those who like it do so early in the project because they’re still getting benefits from the tests, the code is SUPER clear so they “know” how it works, so writing the tests to validate the obvious behavior makes testing for them “simple”. This is also why they hate tests later, as we’ll see. To them, the state and mocks, and sometimes weird test doubles they have to create are just the nasty part of the work. In my younger years, this used to infuriate me, but now I’m just happy they’re testing at all given it’s so rare in our industry. However, I still have found, and so has research, test first is better. Yes, it’s hard to learn. Do it. Someone help Grug.
Grug HAS found that no one knows what amount of testing is right. Even TDD extremists know there comes a point where adding 5 more tests won’t help and so it’s not worth anyone’s time.
When someone says “unit tests makes refactoring hard”, it’s clear they’re doing Test After dev, their implementation is coupled heavily, and they changed the code first, and were mad the “tests didn’t keep up with the changes”. Could be there are a lot of mocks or spies, too. What you’re supposed to do is change the test first to design a new implementation you think is better, and if other tests need to update, either your design is too tightly coupled, the tests suck, or it’s ok because … your API drastically changed. It is especially true with data models that can affect an entire system, even if you’re using those fancy Bounded Contexts where you map types between each. Yes, code changes, and the tests that asserted something are no longer relevant because they’re testing the wrong thing now. It’s almost as if Grug forgot why he was testing?
Grug thinks tests are good early in project but worthless as time goes on? My god, seriously, someone help Grug. They ensure you don’t hate your code base after 2 years. If you do, it’s probably true you hate your tests. You should start with that first: figure out how to not hate your tests. Dave Farley and Justin Searls on YouTube have you covered.
The point is the help you design better code, help you refactor existing code, and if you change API’s, they let you know if you broke anything. Acceptance tests help ensure your features continue to function, regardless of how you’re deciding to change, or change because you learned something new, the details underneath.
Ignore the whole integration/end to end tests. The words are nonsense, the Test Pyramid makes no sense anymore. Think of it in 2 stages: First write unit tests to help design better code. If you accomplish that, then write acceptance tests, using a BDD style if you want to (with or without a BDD library, doesn’t matter) to ensure your various features work, doing your best to keep the stateful things out of the tests (database, API data, login tokens, etc).
Grug and I both agree Mocks are the worst, and should be avoided (same with Spies). If you want to test state, use an Acceptance test; functions or class methods should return if something worked or not; no need to “ask this dude over here if the code your testing over there works ok”.
Agile
Grug might not like Agile, or it’s many fans being akin to snake oil salesmen, or practioneers not having software development experience… but the reality is, it’s here, and there ARE better ways to work. If you’re willing to read the various articles that software devs and PM’s have put out, there are some wonderful ideas that can be modified to work for teams vs. a prescription that works for all. The space constantly changes, and what you read 5 years ago may be out of date. It’s painful, yes, but worth it. But yes, Grug, no silver bullet, agree. I get why you’re wary. Me too.
Refactoring Again
On the Refactoring section, It’s clear Grug is close to groking small batch development, he’s just missing the name for it, as well as seeing how the tests can help with those “not far from shore” architecture changes. Many Test After devs or those having tightly coupled tests/lots of mock setup are burned by the very tests supposed to be helping them make small changes to the code base. Also neat to note Grug has found the opposite extreme of too much abstraction hiding failures.
While Grug is wary of “complexity demon” like OSGi, or today’s Kubernetes/Microservices/Serverless Lambdafest, it’s important you give others an opportunity to learn those lessons safely. I like Grug’s “working demo in 1 day of Microservices working on K8’s”.
Chesterton’s Fence
Hilarious later Grug see’s how tests can help with Chestertons’ Fence, “months to years” after the dev see’s some code they don’t know about… yet tests aren’t valuable later in the project? Not trying to be a jerk and catch Grug being a hypocrite, rather just point out, yes, from your own experience, tests can help document why something is there, and how it works, MUCH better than documentation. Documentation can lie, code doesn’t. And tests actually use the code, so are much better, and real, documentation years after it was written still being accurate. Yes, caveats with tests that don’t assert anything, or “Mocks that emulate the world”.
Microservices
Just because you don’t like Microservices doesn’t mean they’re not good. If you’ve ever been responsible for 8 to 32 independent software teams, consistent of 6 – 8 developers, writing their own UI and API’s… yes, Microservices are amazing to get that department/organization productive. The team can treat their microservice as a monolith, sure, but ultimately they can independently deploy, and their API’s are versioned, and their feature development doesn’t break some other teams. That’s amazing. Yes, many teams have adopted Microservices that should not have. Grug is good to be wary, but don’t hate it just because you don’t fully understand it. They have tradeoffs that can help. Having 30+ people in a monolith is the worst.
Type Systems
Grug and I completely agree on type systems. As someone who likes soundly typed functional languages like Elm and ReScript, I’ve seen in them, even in TypeScript, types that make zero sense, are impossible for many people to read, but the original dev thinks they make perfect sense, and ensure we can’t get into an impossible state. I have no idea how to fix beyond not going too much farther than fixing primitive obsession. Once you go into Currying, Phantom Types, or in TypeScript’s case using Type vs Interface, it needs to be a team decision, just like architecture (Hexagonal, Entity Framework, Redux vs. Hooks, etc). However, type styles can sometimes be enforced with linting rules. Each person and team is different, and just like Agile practices, those seem to work best when tailored.
Expression Complexity
Same view on Expression Complexity, pick linting rules as a team, enforce. We shouldn’t be talking about this stuff in 2023 beyond a team discussion, and then months later reassessing when a new team member comes in. The days of bike shedding syntax should be automated away.
DRY – Don’t Repeat Yourself, or… Not
While DRY is a good idea, there is zero metrics to know if you’re doing too much DRY or not enough. You can DRY something, and have no bugs, but not be aware it was the DRY’ing of the code that prevented them. Only after you get bugs, then later do you DRY the code to prevent further ones, do you go “Man, DRY rocks”. On the flip side, some code is so DRY, it’s nearly impossible to work with, even if it was tested first, and it’s just easier to ignore the DRY’d abstraction, and redo 90% of the implementation, bugs and duplication be damned. Tests and refactoring can help here. However, assuming positive intent here, I agree with Grug, you can over steer in either direction here. DRY if you can, but if you can’t, don’t freak out. Exact same viewpoint on Separation of Concerns. It sounds like some scientific term, but it’s super contextual and fuzzy. Helpful to be aware of it, yes, but don’t stress it too bad.
Closures
Closures are too big of a topic to debate her. Suffice to say, in dynamic languages like Python, JavaScript, Lua, Ruby… this is how you do things. In the early days of JavaScript, we couldn’t use modules without closures. Imagine saying to a Java developer “you can’t use modules or classes in a module and later import them” Yeah, that wouldn’t go over well. On the flip side, we had C developers come to JavaScript, get confused writing their imperative code, whine about hoisting, and demand block scope via the let keyword. Closures worked, but not well enough for imperative coders. Those who tested first, or were functional wouldn’t dream of using closures because they hide state… well, unless we want curried functions. Suffice to say, Closures are how the language works, we need them, can’t escape them, but yes, horrible sinful code is created using them. Test first, use team approved linting rules, those will help remove those nasty versions.
Optimizing
Optimization is also contextual. If you’re writing Node.js, and optimizing, it’s typically for hot areas (like RegEx blocking event loop, or other nasty surprises making your Lambdas spike in time). At low scale NO ONE CARES. At large scale, milliseconds can mean millions of dollars, either in server costs, or eCommerce sales lack of conversions. So it “depends” if it’s a big deal or not. If you use Go or Rust or C, you probably care deeply about fast code because you have too whereas in Python or Node.js, probably not (within reason; Promise.all vs. 5 async awaits is low hanging fruit, man).
However, yes, junior devs need to cut their teeth on optimizing a for loop, and never delivering, and learning “gee, maybe I shouldn’t have spent 2 days on something that was working in 2 hours”. You should help them time box this and then later at a hackathon work with them all day to make it wicked fast. If that’s their passion, don’t kill it! Encourage it, just aim it more safely.
API’s
API’s won’t be solved. Even if you test first, some developers like how the API feels while others think it’s horrible. We all have preferences, and sadly, you’ll prefer some and hate others. Good news; you can abstract over top! Or pair/mob program and come to a consensus.
Grug is smarter than me in Parsers, so ok, whatever Grug says here.
Front-end Dev
As primarily a Front-end dev, I’ll ignore the silliness of a Java developer lecturing me about complexity for now. (Google ProblemFactory jokes). Yes, a lot of front-end tooling is super complex. However, front-end is more complex than some back-end work. The abstractions many create are the best they can to hiding the complexities of building UI’s on top of things that were never designed for this (HTML, CSS, JavaScript) and improve at the “design by committee” speed, yet the innovations on TOP are sus because they’re not official cannon.
The only way to remedy this, however, is show easier ways to develop front-ends, educate on the different types (server-side generated pages vs. single page applications vs. documentation generators), and allow devs to play and learn how hard/easy it is to change and maintain these stacks over time.
Also, front-end iterates and changes quickly, that’s just how things are for the past 2 decades, especially the web. Like regular “abstractions” in code, we’re learning which ones are good, which ones are, and where they’re appropriate. Are we building something with lots of client state like Figma or Trello? What about very little beyond form validation like CNN? A hybrid? One team or many? Do the API’s support the front-end or do we need an orchestration layer? It’s a mess, that’s just how things are here.
Fads
I understand Fads can be annoying, but better to fight Cynicism than later be disappointed vs. being left behind or using old tech that is painful to work with.
“Old, boring tech”… yeah, raise your hand if you enjoy using Asynchronous Modules via amd.js in 2023? Oh look, no hands raised. At the time it rocked because the alternatives were non-existent. Now? We have native modules, and a consistently supported CommonJS.
Also, keep in mind some fads are ressurecting old ideas, like Elixir/Erlang, or OCAML’s recent resurfacing on social media, or Python’s popular resurgence because of Data Scientists and speed on Serverless.
Fear of Looking Dumb
For Fear of Looking Dumb, yes, you should admit when you don’t know something. But dude, this is software, we’re constantly learning. If the world is moving to “Some Complicated New Thing”, go learn it. This is the gig.
A lot of great advice from Grug. Just temper it with the above.
Top comments (0)