In my experience, code with too many functions is more difficult to grok than spaghetti code. It's like trying to read a book with each sentence reference a different page. So, I try to code like I would write, in digestible chunks.
> As I get older my code gets a little more verbose
I've seen too many of my previous projects die right when I moved on. Now I tend to write code as if it were written by a beginner: verbose and boring, with no magic.
There seems to be two types of programmers: one that can read a line of code like or theCar.fuel() and trust that you in the current context understand enough of what the call does that you can continue reading on the current level of code. This type of programmers don’t mind abstractions even if a function is called in only one place.
The other type of programmer must immediately dig into the car.fuel code and make sure she understands that functionality before she can continue. And of course then each and every call becomes a misdirection from understanding the code, and of course for them it is better is everything is spelled out on the same level.
I’ve seen quite a bit of code written by the second type of programmers, and if you don’t mind scrolling and don’t mind reading the comment chapter headers (/* here we fuel the car */) instead of all the code itself, it can be reasonably readable. But there’s never comprehensive testing coverage for this kind of code, and there’s usually code for fuelling the car in four different places because programmers 2-4 didn’t have time to read all the old code to see if there was something they could reuse, and just assumed that no one had to fuel the car before since there wasn’t any car.fuel() method.
My feelings about people that can’t handle abstractions is that they just don’t have had to create or maintain anything complex. Very few real world systems can be kept in ones mind in full.
It is just ... I have seen both types of code and if written by someone else, coffee that at least attempt to segment things into chunks that clearly don't influence each other (functions with local variablea) is massively easier to read.
// #1, in essence
result = a => map => reduce => transform
is easier to read and understand, while others feel that // #2, in essence
aThing = a => map
aggregation = aThing => reduce
result => aggregation => transform
is easier to read and understand. Folks in camp #1 think camp #2 is creating too much abstraction by naming all the data each step of the way, and camp #2 thinks camp #1 is creating too much abstraction by naming all the functions each step of the way.Really it’s just these two mental modalities butting up against each other, because you will separate your layers in different ways for increased clarity depending on which camp you fall into. What makes things clearer for camp #1 makes things less clear for camp #2, and vice versa.
That’s my suspicion anyway: the premise of the discussion is just a little off.
This is so true. The worst code that I've dealt with is the code that requires jumping to a ton of different files to figure out what is going on. It's usually easier to decompose a pile of spaghetti code than to figure out how to unwrap code that has been overly abstracted.
Compared to someone writing inline functions that do too much, the wasted brain hours don’t even come close
In my experience refactoring out anything nested >3 levels immediately makes the code more readable and easier to follow - I'm talking about c++ code that I recently worked on.
Decomposing to functions and passing as const or not the required variables to functions that then do some useful work makes it clear what's mutated by the sub functions. Make the error handling policy clear and consistent.
Enforce early return and RAII vigorously to ensure that no resources (malloc,file handles,db connections, mutexes, ...) are leaked on error or an exception being thrown.
And suddenly you have a code base that's performant, reliable and comprehensible where people feel confident making changes.
That more than anything is what kills readability, because context switching imposes a huge cognitive load. Isolating layers of abstraction almost always means small, isolated, single-purpose functions.
I agree up to a point, but I find this kind of separation a little… idealistic? I prefer the principle that any abstraction should hide significantly more complexity than it introduces.
At the level of system design, there probably are some clearly defined layers of abstraction. I’d agree that mixing those is rarely a good idea.
But at the level of individual functions, I have too often seen numerous small functions broken out for dogmatic reasons, even though they hid relatively little complexity. That coding style tends to result in low cohesion, and I think the cost of low cohesion in large programs is often underestimated and can easily outweigh the benefit of making any individual function marginally simpler. If you’re not careful, you end up trading a little reduction in complexity locally for a big increase in complexity globally.
// v1, mixing layers of abstraction
x = a if exists, else first()
y = b if exists, else second()
result = third(x,y)
// v2, abstraction
result = getResult(a,b)
In v1, we have the semantics of x and y, so we understand that a “result” is obtained through the acquisition of x and y. Whether we need to understand this is a judgement call. But v2 opens a different “failure to understand” modality: “getResult” is so blackboxed that the only thing it really accomplished is indirection, without improving readability.I love Clean Code, but I think it sometimes prematurely favors naming a new function and the resultant indirection.
Because it is, just on multiple plates.
Yes!!! I've been trying to teach Juniors that if the function itself has 4 levels of abstraction, even if the names are readableFunctionThatDoesXwithYSideEffect ..... it is harder to understand, Ctrl+clicking downards into each little mini-rabbit -hole. Just keep the function as a 80-liner, not a 20-liner with 4 levels of indirection w/ functions that are only used once (inside parent function) ugh.
http://number-none.com/blow/blog/programming/2014/09/26/carm...
Referencing carmwack always has to be in the context of high performance code.
Even carmack himself has started like functional code. Which normally leads you down small pure functions.
For a procedural code base, avoid subprocedures that are only called once.
For a pure functional codebase, e.g. Haskell, locally-scoped single-use functions can be beneficial.
It might even make it harder to test: if you break a function wrong, then you might end up with a group of functions that only work together anyway.
For example: when you break a big function into 3 smaller ones. If the first acquires a resource (transaction, file) and the third releases it, then it might be simpler to test the whole group rather than each one separately.
This works really well as long as you use pure functions, because their impact on behaviour is clearly restricted.
Theres nothing wrong with charming magic in your code, if it really does something special and is not just used for the sake of it - it only gets into dark magic, when you forget or are too lazy to add proper documentation in the end. Which ... happened to me, too many times.
But otherwise very much yes. Clarity and simplicity should be always goal number one. But since simplicity is hard to reach at times and time is short, it is always about the balance.
"Code first for the machines, then for others that will maintain your code and lastly for yourself."
And that I think for me nicely strikes the balance.
And I rather have clear, maintainable code - which is easier to work with and therefore less filled with bugs.
The difference between code and books is that programmers can freely and naturally define functions. I wonder if some people complaining about too many functions never actually learned how to read code in the first place.
In a way, it kind of _is_ spaghetti code. Even if there's no back references, it turns a single train of thought into a string of entrances and exits.
Generally used with a negative connotation. C2 also discusses how the layers can become entangled/stuck with one another and difficult to replace, which seems to fit the metaphor.
For describing layered code in a non-negative fashion, just saying "layered (or "modular") seems most typical.
How do I apply this to taking over someone else's 4 year old Magento project? We're out here doing our best, and sometimes our learning environments are in that context.
I would say, don't stress about it too much. There is no perfect way. Everyone makes misstakes. And about when to make abstractions and when not, is mostly about experience. There are modules worth optimizing and abstracting. And others are not. You definitely will make wrong decisions about it and later found out, this optimisation was a waste of time, or that quick and dirty approach really cost you much later on, we all did that and still do.
Much worse than making a wrong (design) decision is making no decision at all - because mostly you have to decide for something and then just go with it. Overthinking things seldom helps. What helps me sometimes is, putting a special hard problem to the side if I am stuck and solve something easier first. Then after some time, when I get back to it, things are much more clear.
But I also wasted too much time thinking about the right approach in a neverending, neverprogressing loop to achieve perfection.
Now my question is not, is it perfect or shiny, but: Is it good enough?
What matters is, that shit gets done in a way that works.
A CEO from my past often muttered that "perfect software comes at infinite cost". It's key, imo, to identify which components of what you are building _must_ be perfect. The rest can have warts.
Well, but by the words of your former CEO (and my opinion) those parts would then have infinite costs, too... if they really need to be perfect. I mean, it is awesome, when you do a big feature change and it all just runs smooth without problems, because your design was well thought out, but you cannot think of every future change to come - and when you still try, chances are you get stuck and waste your time and risk the flow of the whole project. I rather tend to think about the current needs first and the immediate future second, but everything after that, I spend not much thought anymore.
Agreed. What I mean by "perfect" is: for a given part/component/decision/etc, take the time (an always-limited resource) to learn as much as possible and contemplate more than just the seemingly obvious path forward. Take security for example. I'd rather 'waste time' now making sure I'm covering any gaps in that realm before shipping.
OTOH maybe some jacked-up abstraction/incorrect tool choice/ugly-ui/etc is something that can wait a few sprints or longer. At least you can plan when to deal with these. Security breaches tend to plan your day for itself on your behalf. :)
Which is why I enjoy languages that let me do this without getting too hung up on performance. It's curious that you bring up Python, because idiomatic Python (especially where math libs are concerned) seems to vastly favor brevity/one-liners over all else. It's nice to hear that a veteran is favoring clarity.
I can’t speak to math libs, but in my experience with server-side development, Python devs tend to (often even religiously) cite PEP style guides favoring explicitness and verbosity. I think there may have been a shift as Python got a lot of uptake in scientific and ML communities, and I hope that hasn’t seriously impacted the rest of the Python community because, while I don’t especially love the language/environment, I deeply appreciated the consistency of valuing clear and readable code.
Explicit is better than implicit, always has been, always will be. Granted, I've been writing backend/server-side Python code for 15 years now, so that might be one of the reasons.
For example, if you're writing a plotting library geared towards data scientists, you're almost forced to pick brevity over verbosity even if that means violating some of Python's core philosophies. Data scientists usually come from non-compsci backgrounds and almost 90% of the codes they write, don't go to production. So, they usually prefer tools that help them get the job done quickly and they write tools following the same philosophy.
If I were building a library for something like that, I’d build the core idiomatically, then expose an idiomatic API with aliases for brevity. I’d make sure the alias is documented in the docstring, and types refer to the idiomatic names. I know TIMTOWTDI isn’t entirely “pythonic”, but it’s a small compromise for probably a good maintainability boost.