It’s ingrained into nearly every programmer’s psyche, “break big things into smaller things.” When a function gets too big, break it out into smaller ones. When a class gets too complex, break it down. It’s the essence of the Single Responsibility Principle: do one thing well.
We do this to isolate complexity. To abstract. To make problems small enough to be understood. While this good practice is etched into our brains, the actual art of composing complexity out of small, simple components is not so well developed in many of our minds. How should we break things out? Where are the component parts that can be separated? When is a little copying better than a little dependency?
As I’ve grown as a developer, I feel as though I’ve honed my instincts to answer those questions better than I once did. But, what is it that I try to do and try to help others do as well? Rather than just instinct, what mantra, like “do one thing well”, should I tell myself and others?
My short, easy-to-remember phrase to help me think about how to componentize code is "be a sieve, not a funnel".
A sieve filters, it extracts the rough from the fine. It removes impurities. It refines.
A funnel does none of that. It simply transfers one large volume of liquid and funnels it into another volume.
I used to be a funnel.
I would see functions and methods that seemed “too big” and I would instinctively break them down (that’s what I’m supposed to do, right?). But I didn’t refine them. I would simply extract 10 lines of a function, move it nearly verbatim to a new function, and call it good. I would funnel all the complexity into an innocuous function call.
What’s wrong with that? It’s goto . By doing that, you’ve not made your code any easier to maintain or more flexible, you’ve simply reordered it. You’ve forced the next person to maintain your code to jump from function to function in order to follow your thoughts.
Now, I try to be a sieve.
Imagine the following few lines in a larger block of code:
Past me would have seen this code and thought “ah, that is one thing, I’ll break it into a function!” I would have happily funneled that code into another function, like so:
Besides not using
This was no more difficult to write, it took me no more time to refactor. But the code has been refined. That cludgy, hard-coded value
Code
Process
function my_big_function() { // Doing lots of things... $blue_items = []; foreach ($items as $item) { if ($item->color() == 'blue') { $blue_items[] = $item; } } // Doing many more things... }
function my_big_function() { // Doing lots of things... $blue_items = get_blue_items($items); // Doing many more things... } function get_blue_items($items) { $blue_items = []; foreach ($items as $item) { if ($item->color() == 'blue') { $blue_items[] = $item; } } return $blue_items; }
array_filter()
, what’s wrong with that? The problem is that I’ve simply reordered my code, it’s no more flexible than it was before. I’ve just funneled 7 lines from one place to another.
Be a sieve by removing the specific code from the function and making it changeable.
function my_big_function() { // Doing lots of things... $blue_items = get_items_by_color($items, 'blue'); // Doing many more things... } function get_items_by_color($items, $color) { $extracted = []; foreach ($items as $item) { if ($item->color() == $color) { $extracted[] = $item; } } return $extracted; }
”blue”
has been sifted out, in fancy terms, it’s been parameterized. Just as before, we can say that the last function does just “one thing” too, but it can do one thing in different ways. This is how we design for change.
When you refactor code, be sure to pass in information to a function that’s specific to a single use-case, not hide it within the function. In our calling function, doing so makes it really no more difficult to read.
Designing in this way is how we make our code more malleable. Think about how we would change the earlier get_blue_items()
to get_red_items()
? We’d have to change the function itself, but we’d also need to change its name everywhere else that it is used as well. That is the essence of technical debt.
When you componentize code and then re-compose it, sift out the code and values that tie it to one particular implementation, whether it’s for just one use-case or not. While this example was rather trivial, think about how you would extend this idea to classes and methods. Even larger pieces of functionality. Even if you only are doing one thing, for one client, in one situation, try to frame the problem in a way that let’s you configure your code with parameters and environment. Make use of interfaces. Sift out the domain-specific, don’t hide it within smaller classes, methods, or functions.
It’s a small thing, but it feels good. It’s the art of doing one thing well.
Be a sieve, not a funnel.
Skip to footer
Comments