The obvious problem
To resolve this however, how would one refactor this? Do you create 4 functions now?
trainModelWithZerosInitOptimSGD(...)
trainModelWithZerosInitOptimAdam(...)
trainModelWithRandomInitOptimSGD(...)
trainModelWithRandomInitOptimAdam(...)
What you've done here is the equivalent of rewriting:
int Add(int a, int b)
into:
int OnePlusOne()
int OnePlusTwo()
int TwoPlusOne()
int OnePlusThree()
// ...
Which is clearly the opposite of reusability.
What's the point of the advice?
Whenever you deal with any kind of good practice advice, my advice to you is to understand the reason, don't just apply the letter of the rule. Applying things without understanding them leads to many misunderstandings and code that is just as bad as (if not worse than) the original.
So why are multiple boolean arguments bad? Well, very simply put, because it's really difficult to understand the values used in such a method call once it is written.
myModel = train_model(myModel, true, false)
It is not at all obvious what these boolean settings do. Let's use another example. Tell me what we're doing here:
var result = RaiseToPower(2, 3);
Are we calculating 2³ or 3²? Well, you might be able to reason that the left-to-right order is preserved, so (2,3)
must mean 2³. Yeah, that sounds reasonable. But what about this?
var result = CalculateRoot(3, 2);
Are we calculating √3 (also written as ²√3) or ³√2? Here, we come to an interesting issue. Either we retain the left-to-right nature of the parameters (which means we're doing ³√2 here), or we retain the "base number, then power" order that we used in our RaiseToPower
signature (which means we're doing ²√3 here).
In either case, we are now breaking one of the natural assumptions that we made about RaiseToPower
. It's always going to confuse someone.
The key here is one of reasonable readability. If there is a better way to communicate the meaning of the values, then it should be used. The goal here is to maximize readability without making it too hard to write at the same time.
Solving the core problem (solution 1)
For example, C# allows you to name your parameters. This means that you can use the name that is defined in the method signature in the method call, e.g.:
myModel = trainModel(myModel, useSGD: true, initWithZeros: false);
This already resolves the issue of readability without actually getting rid of the boolean parameters themselves. Is it the best solution? Maybe not, but I would consider this sufficiently readable.
A broader best practice solution (solution 2)
However, if focus on other good practice advice, in this case inversion of control, the solution to that would inherently also solve the boolean parameter issue.
Skipping the lesson on inversion of control, the solution entails passing the weight and optimizer into the method, rather than a flag value which then selects the related weights/optimizer:
def train_model(model, weights, optimizer):
model.initWeights(weights)
model.train(optimizer)
# some really fancy logic
return model
To be honest, your particular example is rendered to nothing when this advice is applied. I added this suggested solution anyway because it is great in scenarios where the method contains more logic than just the flag-checking logic (i.e. the some really fancy logic
that I added to the example)
As a matter of fact, your example method's purpose is intrinsically tied to the passing in of boolean flags, to the point where its existence itself violates the boolean argument guideline.
Finding the right pattern (solution 3)
I think in this case the builder pattern is a more appropriate solution for you.
In essence, instead of doing this:
myModel = train_model(myModel, true, false)
Your consumer should be doing something like this:
myTrainedModel = (
myModel
.train()
.withWeights('random')
.withOptimizer('SGD')
.run()
)
I have also opted to skip the whole stringly typed parameter discussion here, I'm not a fan of these string values but this is out of scope of the question you asked.
if I want to add a method to save model weights
another flag... Do you see where this leads you to? You can not encode with flags every singlemodel
state permutation. Moreover, some choices are not binary. The opposite of true is false, but the opposite of SGD is not Adam... The opposite ofrandom
is fixed, notall Zeros
. Flags are code smells if they reflect this kind of design weakness.train_model(model, init_weights_method, optimizer_type)
?foo(true, false, false, true, true)
is a bug waiting to happen when someone tries to change an argument-value and accidentally changes the wrong one because they all look the same :)