When you go back to edit your code, it's like calling a 1-800 number and getting re-routed to 15 different departments to finally find the person you need to talk to.
I see this all the time with "business rules" problems.
If you have a situation where you can't make levelled abstractions, you've got some thorny interconnections in your logic (and those may be fundamental!!!). The way I handle this is with a "gauntlet pattern." You still can split your logic up into parts, but you just do it by "rejecting" certain logical chunks at a time within a function and comment above each state of the gauntlet.
It looks something like this:
// marketing told me we should never do this ever under any circumstances
if (!a) {
return CASE_1;
}// if the user enrolled like this, we should check if there's this bad property about that user
if (!b) {
return CASE_2;
}// oh, you crazy people in finance
if (c > 4 && d < 0) {
return CASE_3;
}return CASE_4;
The key thing is not to get hung up on duplication or having exact control flow like how the business thinks about it. You want 1) to return the largest percentage or simplest cases out first, 2) keep everything flat as possible without layers of branching, and 3) be able to associate tests to each one of those early-reject lines.
The nice thing about this is the reduction of cognitive load. By the time you get to the 3rd case or whatever, your mind can already know certain things are true. It makes reasoning about rules much easier in my experience.
Splitting a functions which consists of multiple logical steps is most times a good idea because it makes testing much simple and tends to show you where you accidentally had subtle cross-cutting concerns or unintended cross-talk between sub-domains (domains in sense of modelling, not http).
What is important is to properly name functions (and if you can't you probably shouldn't write that function).
Also it's important to learn how to live with abstraction, i.e. to reason about code wrt. a specific problem without needing to jump into the implementations about every function /method it calls.
Through the later point is much easier with a reasonable use-full type system. And with this I mean useful for abstraction without making abstraction to hard and without allowing to many unexpected things. Languages like rust or scala have such a type system (as long as you don't abuse it) but e.g. C++ fails this even through it has a powerful type system.
At least this are is opinion.
And without question if you can't cleanly split something out, then don't split it out. If you can split it out but not name it reasonable either your understanding it lacking or it should never have been split out.
Assuming you just need helper functions for f(), compare
int f(int x)
{
return g(h(x));
}
int g(int x)
{
return ...
}
int h(int x)
{
return ...
}
to f = g . h
where g = ...
h = ...
Turns out my language choice was the problem, not over-abstraction. If you can fit it all on one screen, then have at it tbh.That's what makes point-free Haskell such a sweet spot for me: terse, symbollic control flow and nice combinators, but types to guide you. I try to use it as a better J.