> We're not here to write code, but to solve problems.
In my opinion having had multiple technical job roles (car stereo/alarm installer, website builder, military officer, CEO, CTO etc…) this is always the job.
The job is *always* to make the organization more effective and efficient full stop. Your role in that is what you choose and negotiate with your team throughout your life; boundaries change pre/during/post employment.
When you join a company you usually (not always) have a niche role, to fill in a gap that is preventing effective organizational execution.
If you’re mentally flexible to understand that your narrow focus is not the actual output, that is a temporal means to an output, then you transform how you view the concept of work and relationships
I have an engineer on my team that's always asking "what if this or that happens in the future?" to which I've started to reply "what if it does NOT?"
I know, I know... wow. Not much insightful. But for some reason with this particular engineer this is the starting point to talk about actual requirements. This question in particular triggers the conversation of going back to product to figure out what they truly know they want right now, not in a maybe future. What are the actual hard, known requirements, instead of wishful thinking and "if everything goes well we will need this" type of mentality of ~hopeful~ optimistic PMs.
This is a great article, and I largely agree but I feel like we're giving ourselves an excuse to be lazy. Because I've absolutely seen this principle swing in the opposite direction, where someone writes slop code, without ever having had a real conversation with the end user(s) and consequently the software goes out the door without having considered top 5 most common edge cases that would have been so obvious if a little more effort had been put in.
Have to admit the lazy thing threw me, but I can see how the “doing less” I’m arguing for could be taken that way. The “less” is not about avoiding handling edge cases that are possible now, but about avoiding putting in layers of code to handle cases possible only in some future versions of the code (with some limited exceptions that I mention at the bottom of the post)
In fact, it’s crossing my mind that people might not want to be accused of being lazy, and that is a motivation to over-engineer solutions.
I find that strong typing often obviates the need for unit tests.
Software breaks when data transforms in a way that typing can't solve. When data goes across a wire, or into a database, it leaves your space. Anything you do to your code risks breaking it. Integration tests solve that, but at a very high cost.
I don't have a great solution for that. It just comes down to experience: how do things change over time? You take guesses. You try to be flexible, but not so flexible that you aren't solving the problem at hand. (It doesn't do you any good to hand the user a C compiler and say "this is flexible enough to handle all of your future needs.")
Experience is, unfortunately, the worst teacher. It gives the lesson after it gives the test.
It's all pretty solid except for the part about OO.
Inheritance almost never works in "the real world" but I find being able to tie functions to the data they're expected to work on to be pretty helpful.
It's sort of like typing, really, functionX can only take FooBar variables vs making methodX on class FooBar.
Like everything else you can "do it wrong" and you shouldn't be a slave to any particular software ideology.
> Remember you can still add in that complication tomorrow
is directly undermiend later with:
> When should you create stuff just in case?
> ...
> 1. There is a reasonable chance it will be useful later
> 2. It will be difficult to add in later
> 3. It won't meanginfully slow down the meeting of more likely requirements
whenever i've pushed to overengineer its because i've developed a strung hunch that points 1 and 2 are true and i'm being defensive about my time and effort next week.
and if you're not allowed to push back because of 1 and 2 it's a sign of some sort of organizational problems where product folks sit at the top of the hierarchy and hand down dictates to builders without consulting with builders as equal partners.
> Remember that code that clearly solves just those problems is in no way a hack.
I think you can’t stress this point enough. In my experience anything that is not implemented by any norm or „clean“ or in an unusual way is considered a hack. Even if it perfectly solves the problem with the least amount of cruft to it. That makes me sad.
> One of the worst pieces of advice that I ever received was that every function should be unit tested.
Obviously not every function should be -- many are so obvious and straightforward that there's nothing to test -- but every function that does anything vaguely "algorithmic" should be. Unit testing is really important for catching logic errors.
> Instead, write higher level tests close to the client/user facing behaviour that actually give you protection against breaking things unintentionally
Yes, these are good. But they're a different kind of test. There are tests for correctness, and tests that the program runs. You need both.
In fact, sometimes you even need to split up functions smaller than they otherwise would be, just so you can test an inner logic portion independently.
Yeah, no. Every time I saw code written by someone who attempted to avoid OOP it ended up with passing a huge 'context' parameter to most functions, effectively reinventing Python's OOP but worse.
Use pure functions as the starting point, but when you find yourself start passing complex structure around (any abstract word in parameter names, like 'context', 'data', 'fields' is a sign for that) just use OOP.
I am amazed by the number of articles like this, that essentially say "you should not write bad code, you should write good code", while somehow implying "listen to me, I know better" (otherwise I wouldn't write the article...).
The truth is that writing good code takes experience. Those who live by the rule "thou shalt not over-engineer" risk writing bad code. Those who live by the rule "thou shalt know all the patterns and use them" risk writing bad code.
You should strive to write code that others can understand and maintain, period. If you need to justify your lack of "something" ("It's not a hack because..." or "I don't use OOP because..." or "I duplicated this code because..."), then it feels like it says something about your opinion of your own code, IMHO.
Its easy to mix up "what seems easier to work with" vs "what's actually easier to work with". Pulling things out to config, splitting into microservices, additional layer of abstraction, rules engines, etc., they seem like they'll be easier to work with at a high level because it gets logic out of the core, but then when you're actually working with them, or worse, when someone else is working with them and doesn't have your context, now there are five places to go look for logic and deciding which piece needs changed, instead of just one obvious place.
I wish more engineers I worked with had a stronger personal belief that design and planning is a favour they do for themselves. Defining clear requirements and resolving unknowns (or at least identifying them) is the foundation that if you don’t build, you’ll be building your project many times.
16 comments
[ 2.7 ms ] story [ 25.0 ms ] threadIn my opinion having had multiple technical job roles (car stereo/alarm installer, website builder, military officer, CEO, CTO etc…) this is always the job.
The job is *always* to make the organization more effective and efficient full stop. Your role in that is what you choose and negotiate with your team throughout your life; boundaries change pre/during/post employment.
When you join a company you usually (not always) have a niche role, to fill in a gap that is preventing effective organizational execution.
If you’re mentally flexible to understand that your narrow focus is not the actual output, that is a temporal means to an output, then you transform how you view the concept of work and relationships
I know, I know... wow. Not much insightful. But for some reason with this particular engineer this is the starting point to talk about actual requirements. This question in particular triggers the conversation of going back to product to figure out what they truly know they want right now, not in a maybe future. What are the actual hard, known requirements, instead of wishful thinking and "if everything goes well we will need this" type of mentality of ~hopeful~ optimistic PMs.
Architectural decisions sometimes close doors and making future changes very difficult.
The only thing we know for certain is that change will happen.
In fact, it’s crossing my mind that people might not want to be accused of being lazy, and that is a motivation to over-engineer solutions.
Software breaks when data transforms in a way that typing can't solve. When data goes across a wire, or into a database, it leaves your space. Anything you do to your code risks breaking it. Integration tests solve that, but at a very high cost.
I don't have a great solution for that. It just comes down to experience: how do things change over time? You take guesses. You try to be flexible, but not so flexible that you aren't solving the problem at hand. (It doesn't do you any good to hand the user a C compiler and say "this is flexible enough to handle all of your future needs.")
Experience is, unfortunately, the worst teacher. It gives the lesson after it gives the test.
Inheritance almost never works in "the real world" but I find being able to tie functions to the data they're expected to work on to be pretty helpful.
It's sort of like typing, really, functionX can only take FooBar variables vs making methodX on class FooBar.
Like everything else you can "do it wrong" and you shouldn't be a slave to any particular software ideology.
> Remember you can still add in that complication tomorrow
is directly undermiend later with:
> When should you create stuff just in case? > ... > 1. There is a reasonable chance it will be useful later > 2. It will be difficult to add in later > 3. It won't meanginfully slow down the meeting of more likely requirements
whenever i've pushed to overengineer its because i've developed a strung hunch that points 1 and 2 are true and i'm being defensive about my time and effort next week.
and if you're not allowed to push back because of 1 and 2 it's a sign of some sort of organizational problems where product folks sit at the top of the hierarchy and hand down dictates to builders without consulting with builders as equal partners.
I think you can’t stress this point enough. In my experience anything that is not implemented by any norm or „clean“ or in an unusual way is considered a hack. Even if it perfectly solves the problem with the least amount of cruft to it. That makes me sad.
Obviously not every function should be -- many are so obvious and straightforward that there's nothing to test -- but every function that does anything vaguely "algorithmic" should be. Unit testing is really important for catching logic errors.
> Instead, write higher level tests close to the client/user facing behaviour that actually give you protection against breaking things unintentionally
Yes, these are good. But they're a different kind of test. There are tests for correctness, and tests that the program runs. You need both.
In fact, sometimes you even need to split up functions smaller than they otherwise would be, just so you can test an inner logic portion independently.
Yeah, no. Every time I saw code written by someone who attempted to avoid OOP it ended up with passing a huge 'context' parameter to most functions, effectively reinventing Python's OOP but worse.
Use pure functions as the starting point, but when you find yourself start passing complex structure around (any abstract word in parameter names, like 'context', 'data', 'fields' is a sign for that) just use OOP.
The truth is that writing good code takes experience. Those who live by the rule "thou shalt not over-engineer" risk writing bad code. Those who live by the rule "thou shalt know all the patterns and use them" risk writing bad code.
You should strive to write code that others can understand and maintain, period. If you need to justify your lack of "something" ("It's not a hack because..." or "I don't use OOP because..." or "I duplicated this code because..."), then it feels like it says something about your opinion of your own code, IMHO.