As a programmer, I spend almost all my time hanging out with code. This code may be my own, other co-workers’, or code from an open source project. And I may be contributing this code at different points in that project's lifecycle: starting out, initial development, maintenance, debugging, adding new features, etc. Given the amount of time I have been working on various projects as a programmer, I spend more time – and place more value on – considerations around the ability for anyone to step into this code and maintain it, debug it, or refactor it. For lack of a better idea, the term that has stuck with me is (albeit made up): "refactorability".
My idea here is to consider up front the intent of the code I'm writing. I'm trying to keep things away from "right" and "wrong" and leaning towards being "better". All code could be written differently, but does my code make it painful for myself or others?
Keep Things Simple
The principles around Keep It Simple Stupid (KISS) have been around since at least the 1960s. It's short and sweet, but essentially, keep your solutions simple. It is very easy to think of new abstractions, ways your code could be more robust, use some new programming techniques you've wanted to try, or a new third-party package (or even programming language!). Before heading down these paths, I try to consider:
- Am I adding complexity without any real benefit? Is using a new technique or language a good fit for me as well as my co-workers or client?
- Is the code an appropriate solution for the problem?
- Is this approach going to be difficult to explain? To document? Or to test?
- Am I adding a dependency? While you may think this is strictly related to packages and package managers, code can have dependencies within itself (function A is needed for function B, etc.).
Any of these could spell difficulties for the future. If your solution is hard to explain, adds dependencies, or buries solutions in a small novella of code, that's going to be difficult to maintain and debug. All of these could lead you to a path where refactoring (moving and updating code) and adding new functionality could become a nightmare. This should become the voice in the back of your mind: every line of code or dependency you add is something to learn, trace, and figure out. So make it count and make it straightforward.
Code is an API
Part KISS, part modularity, the next principle I try to follow is to treat my code like it is an API. Especially when you're developing solutions in frameworks, it is easy to add code that performs certain actions within other code that just ties into the framework or the user interface.
Using the ubiquitous todo application example, imagine we have an interface where a user can click a button to add a task. And it makes sense that this button contains the code to actually add a task. But now, if I need to add a task from some other screen or someone else needs to use my code in a new way, they can't just add a task… they have to click the button!
If I treat the add task functionality as an interface to my application, then it makes more sense for it to be a separate, callable and reusable piece of code than mixed into code that shares responsibility. Smaller pieces of code with narrow responsibilities are easier to understand (KISS) but it's also more intuitive that code for adding a task is in the addTask function than that it is in the button click handler.
In this case, I try to consider:
- Does this code share more than one responsibility (for example: responding to a button click, adding a task, and updating the list of tasks)?
- Would this code be more reusable if it were separated?
- If you're writing tests, would this code make it easier or more difficult to test? In our example, to test adding a task you would need a test to click a button (or call a button handler). If we separate the functionality, we can test the add task portion without needing a user interface.
- Related to the previous question, what does this code require (arguments, dependencies) where it's located now? What would this code require if it was moved?
- Would it be easy for someone to find your code here if they're unfamiliar with the code base? For Drupal developers, it's sometimes easier to keep your code within a hook like a form alter, but would another developer instinctively know to look there instead of a function labeled more accurately?
Nesting and anonymous functions
To build off of the code as an API approach, nesting functions and anonymous functions can also be a sign that things are getting complex. While a feature of many programming languages, the caveat here is to treat nesting, anonymous functions, and function composure the same as loops.
It's common practice when refactoring, that if you see a bunch of loops together, there maybe a problem with that code (usually called a "code smell"). In general, if you have loops within another loop, the complexity may be high and there may be consequences to this. The same can be said when you're using nesting or anonymous functions or function composure.
It is very easy to find myself writing code that starts out with a single anonymous function, but ends with three or four:
This example exhibits both shared responsibility as well as nesting functions. We have one function "showAddTask" that is calling code to create a form, add event listeners, respond to those event listeners, etc. This is just an example, but it would be very difficult to test one of these components, to debug them, or even to realize that a function that creates a form also responds to a specific event!
Much the same as the previous sections, I try to consider:
- Does this code coupling make sense? Are the parameters passed between functions going to be problematic later (too many parameters or need to pass a parameter that is only used deep within the nesting)?
- Would another developer know to look here if they inherited this code? Would I know to look here in 6 months?
- Could I easily explain the use of nesting, function composition, lambdas, etc.? Am I making code that is too closely coupled to a chain of events?
- Am I relying on variables from parent functions when I should get that data somewhere else? Is this functionality directly coupled to the parent and going to be a pain to move later?
Refactoring is a huge topic and an important topic. I don't always get these things right, but I find the conscious process of considering intent of code leads to code that is easier to maintain. This code is usually easier to debug and easier to test. Projects will change directions as work moves forward and they’ll often require refactoring code for new things, so try not to build walls that will interfere with changes down the line.