Avoid too many function arguments
Code maintainability is a set of guidelines that help making our software easier or cheaper to maintain in long term. It is a cognate to the clean code which is about being able to understand other’s code. Today, I’m going to talk about avoiding too many function arguments. I will show how it translates to SOLID principles, and I will also share one mistake I made when applying this guideline to my code.
tl;dr;
Avoid making too many function or constructor arguments. The typical limit is 3 or 4 arguments. When you apply this limit, you can uncover various issues with SOLID principles in your code, such as Single Responsibility or Open/Closed principles.
Motivation
As a ‘code unit‘, I understand class functions or constructors: in short, everything that contains actual code. The units talk to the outside world through the input arguments, and the returned result. As the list of arguments grows longer, we may see the following effects:
- our unit tends to do too much, because it has access to a broader set of dependencies,
- the line number of the unit grows in order to make use of all those arguments,
- the cyclomatic complexity grows as a result of the two effects above,
- all 3 previous effects cause that the unit is more vulnerable to changes, because there are more things that may force such a change.
Relationship to SOLID
I’ve been working with SOLID principles for a very long time. However, at start I mostly followed my intuition. There was no pointer that would tell me “hey, take a look here”. I paid a closer attention to the number of arguments after reading the book “Building maintainable software” book by Joost Visser, a head of research at Software Improvement Group. I started using the guidelines from that book and I quickly noticed that they have something in common with SOLID.
In my code, I had a simple use case. Two classes that worked together to produce the result, and several issues with testing them. The method calls used too many function arguments – I remember that 5 or 6 were used. When I decided to remove some of them, I noticed that I need an extra class for that, a kind of bridge. The new class took over a part of the state from both classes. This removed the need to pass so much data. After the refactoring, each function had no more than 2 or 3 arguments.
Single Responsibility Principle
If we don’t use mutable static fields and any hidden side effects (and we don’t, do we?), both the class and its functions process solely the values from arguments. They represent both the data, and references to other services (dependencies). By refactoring my code, I realized that the number of things the class has to do is related to the number of function arguments. The more stuff we have to pass, the bigger probability that our class simply does too much. In this case, my original design violated Single Responsibility Principle, but I did not see that until I finished the refactoring.
Open/Closed Principle
Having too many function arguments can also point to issues with other SOLID principles. Not every refactoring results in creating new classes. A good example is a class that fetches the data from 5 services, and runs some calculations on them (example in Kotlin):
class SophisticatedProcessor(
private val serviceAClient: ServiceA,
private val serviceBClient: ServiceB,
private val serviceCClient: ServiceC,
private val serviceDClient: ServiceD,
private val serviceEClient: ServiceE
) {
fun compute(): Result {
val dataA = serviceAClient.fetchAsynchronously()
val dataB = serviceBClient.fetchAsynchronously()
val dataC = serviceCClient.fetchAsynchronously()
val dataD = serviceDClient.fetchAsynchronously()
val dataE = serviceEClient.fetchAsynchronously()
return Mono.zip(dataA, dataB, dataC, dataD, dataE)
.map { (resultA, resultB, resultC, resultD, resultE) ->
doSomeComputations(resultA, resultB, resultC, resultD, resultE)
}
}
}
The number of called services can change over time as the business evolves. But every time we want to add one, we need to expand the constructor and basically modify this class. This is an indicator of violating Open/Closed Principle: extending the functionality should be possible without changing SophisticatedProcessor
. Adding additional classes probably will not help here, but instead we should consider adding some plugin mechanism.
How many function arguments is too many?
The general consensus among bloggers seems to be 3 or 4 arguments as a reasonable limit. Personally I use 4, based on the mentioned book “Building maintainable software”. However, this is not a hard limit – in fact, most functions have only 1 or 2 arguments anyway, and functions with 3 or 4 are (and should be) a minority. In addition, in most projects there is always a very small number of functions that violate this guideline, for example because an external API requires doing so.
In short…
The limit of 3 or 4 arguments should not be a hard limit. The goal is to keep the number of violations at very low levels, and most of your functions should have 1 or 2 arguments anyway.
My mistake
So where’s that mistake I made? Let’s go back to the example with extracting the third class. I said that in the original version, I had a couple of issues with writing tests for those two classes. After refactoring, I created unit tests with so much ease that I considered it as a great success. Looking at the number of arguments became my habit. This in turn resulted in creating more smaller classes than before. Me and my team created unit tests for almost each of them, but over time, it led us to a small disaster. Despite having thousands of tests, and impressive coverage, much of the logic remained untested, because it was built by composing many small classes. Our unit tests did not check that. In addition, we lost the ability to refactor the code and add new features quickly, because each change required rewriting many tests.
Eventually, we ended up with creating an extra test suite that tested the entire business logic as a single unit, but with all the external services and API-s replaced with mocks. Despite some initial investment, maitaining it turned out to be much easier. The tests were almost immune to internal refactoring, and they proved that the service actually did the right thing. My original mistake was that when I started creating more smaller classes, I should have also updated my approach to automated testing. I didn’t and things that usually worked before, later became a serious bottleneck.
In short…
When you change the approach to organizing your code, remember to review the approach to writing tests, too!
Conclusion
The great part about being an engineer is we do not solve zero-one problems. Usually, there are few ways to do the thing, and each of them has its pros and cons. SOLID principles are a good example. Things like ‘responsibility’ cannot be fully measured, and we need some approximations that tell us whether we move in the right direction. Avoiding too many function arguments turned out to be a useful tool for me. I’ve been paying attention to it for many years so far. I made a couple of mistakes on the way, but they gave me a valuable lesson how various pieces of code relate to each other. If you are wondering where to start, just pick up some function with too many arguments. Try to reduce their number, and don’t be afraid of mistakes.