For example, imagine an initialisation method split into a series of small ones: in the context of method itself, you clearly know that object's state is still invalid, but in an ordinary private method you probably go from assumption that object is already initialised and is in a valid state. The only solution I've seen for this is...
Your concern is well-founded. There is another solution.
Take a step back. What fundamentally is the purpose of a method? Methods only do one of two things:
- Produce a value
- Cause an effect
Or, unfortunately, both. I try to avoid methods that do both, but plenty do. Let's say that the effect produced or the value produced is the "result" of the method.
You note that methods are called in a "context". What is that context?
- The values of the arguments
- The state of the program outside of the method
Essentially what you are pointing out is: the correctness of the result of the method depends on the context in which it is called.
We call the conditions required before a method body begins for the method to produce a correct result its preconditions, and we call the conditions which will be produced after the method body returns its postconditions.
So essentially what you are pointing out is: when I extract a code block into its own method, I am losing contextual information about the preconditions and postconditions.
The solution to this problem is make the preconditions and postconditions explicit in the program. In C#, for instance, you could use Debug.Assert
or Code Contracts to express preconditions and postconditions.
For example: I used to work on a compiler which moved through several "stages" of compilation. First the code would be lexed, then parsed, then types would be resolved, then inheritance hierarchies would be checked for cycles, and so on. Every bit of the code was very sensitive to its context; it would be disastrous, for instance, to ask "is this type convertible to that type?" if the graph of base types was not yet known to be acyclic! So therefore every bit of code clearly documented its preconditions. We would assert
in the method that checked for type convertibility that we had already passed the "base types acylic" check, and it then became clear to the reader where the method could be called and where it could not be called.
Of course there are lots of ways in which good method design mitigates the problem you've identified:
- make methods that are useful for their effects or their value but not both
- make methods that are as "pure" as possible; a "pure" method produces a value that depends only on its arguments, and produces no effect. These are the easiest methods to reason about because the "context" they need is very localized.
- minimize the amount of mutation that happens in program state; mutations are points where code gets harder to reason about
Best Answer
I've never seen a guideline, but in my experience a function that takes more than three or four parameters indicates one of two problems:
It's difficult to tell what you're looking at without more information. Chances are the refactoring you need to do is split the function into smaller functions which are called from the parent depending on those flags that are currently being passed to the function.
There are some good gains to be had by doing this:
if
structure that calls a lot of methods with descriptive names than a structure that does it all in one method.