Here's a related question pair posed by OP Sir Falk, plus a new issue.
The way I understand this, there are 2 main criticisms:
- Going into pre- / post-conditions and invariants. Do I understand this correctly, that you would prefer more comments on the function input and output variables, for example acceptable ranges or the nature of variables like the units etc?
First let me quibble with the word "comment".
I am ambivalent about comments embedded in the body of a function.
They can be helpful, when they explain the "why" rather than the "how".
They can also have very little value add, when they say what the code
already eloquently said.
They can also be strongly unhelpful, when (initially accurate)
comments drift out of sync with the evolving code,
perhaps not keeping up with negation of a condition,
or perhaps omitting an important step for maintaining data integrity.
At the top of each function (or method) I always look for
a """docstring""", /** javadoc */, or in this context a dart
documentation comment.
It describes the
contract, the
single responsibility
that this function promises to accomplish.
Now, sometimes the method name makes this obvious
and there's no need for a javadoc sentence,
as in the case of a java getter.
We simply verify that it produces no side effects and move on.
But sometimes we need a full English sentence to
understand caller's and callee's responsibilities,
more text than conveniently fits in an identifier.
... acceptable ranges or the nature of variables like the units ...
Yes. Kind of.
If I see an input parameter with a name like fooLength
,
that's probably enough.
The context, the business domain, will describe a Foo for me.
The meaning is obvious: clearly SI units of meter,
since there was no fooLengthFurlongs
annotation.
The range is obvious: non-negative, and never a lot bigger
than a typical Foo.
In C if I see a time_t
, the meaning is similarly obvious:
seconds since the 1970 epoch, expressed in UTC, unless there's
annotations related to displaying it with some specified zone offset.
So again, I know the meaning of the quantity,
I know exactly how to interpret those bits.
When I saw you write now = DateTime.now()
, I was similarly certain
about how to interpret that many microseconds since the epoch.
In contrast, the undocumented .loginDay
,
plus the lastLoginDay
and lastDay
integers,
left me scratching my head.
It might have just been a matter of "missing review context"
which a GitHub URL would have cleared up --
poke around in the source to see how
this project models the notion of "day", perhaps using one
epoch or another, perhaps starting at midnight UTC or
perhaps using some local zone offset.
What I'm saying is that, as a reviewer, I had no idea what it meant,
and as a future maintenance engineer I would have no idea how to
diagnose / fix a bug or add a feature involving such days,
based on what I had read so far.
Using a type more specific than int
would have immediately
cleared up how to interpret that quantity.
You essentially asked whether I require that explicit
"pre- / post-conditions and invariants" be written down.
No.
But it wouldn't hurt.
For example, an assert fishingLineLength < 1e3
at top of function
tells me that a reel of nylon will never hold more than a kilometer
of material. In other words, caller did the Wrong Thing,
caller violated a pre-condition, by passing in a giant value.
I wouldn't need to see a >= 0
assertion, but if someone found
that verifying that was useful, great, add the check.
Functions seldom implement post-condition assertions.
But as a reader, I always want to understand what promise was made,
and I want to be able to verify by reading the source that the
promise was carried out. For example, a signature of
def sqrt(x: float) -> float:
implicitly tells me x
shall be
non-negative, and the post-condition is that square of return value
shall be "pretty close" to the input x
.
A signature of def logOverdraft(amount):
tells me that,
by the time we exit, we definitely should have logged that amount
somewhere.
Loop variants and invariants are seldom written down,
because we deliberately strive to write clear code.
When reading strcpy
's source, it's pretty clear what
the src
and dst
pointers are doing, no need for a comment.
But with "tricky" code that manipulates several relationships,
I often find myself wishing the author had written more
in the middle of the loop about what specifically the
loop always makes true.
- Better naming in a way that promotes more “natural” reading of the code, more like an English text, for example renaming the function title or using helper functions like isEligibleCustomer. Is that correct?
Yes, that is correct.
To repeat Doc Brown's observation, a well-named temp var
like rewardedDaysRemaining
would obviate the need for
a comment that describes the meaning of an intermediate expression.
Similarly, pushing a boolean expression down into a predicate helper
gives us the opportunity to name the expression,
revealing its meaning. It also gives us a place to
add a documentation sentence, if warranted,
and an opportunity to let an automated test suite
exercise its several corner cases.
Suppose I'm lost, I encounter Alice and Bob, and I ask for directions.
Alice gives me a sequence of "walk north 100m, then east 200m,
then south 50m", interjected by Bob's commentary of
"hang a right at the light, then another right at the Dunkin sign."
Alice gives the (opaque!) function calls, interspersed with
Bob's English sentences. I would rather get them all together
in one smoothly flowing narrative. I would like the code to
read as a story, rather than the comments reading as a story.
The newly surfaced issue is:
Also, comparing the 4 variants, I should have maybe mentioned that there is no documentation except for the code (not my choice), and no requirements document. Requirements are basically done "on the fly".
– Sir Falk
Well, that's trouble.
Code is either correct, or it is not.
It conforms to its spec, or it doesn't.
There can be no code defect, no bug report, absent a spec.
Every C program has an implicit spec that it shall
not segfault or core dump.
A more subtle aspect is it shall free() what it malloc()s.
Beyond that, we can say little more about an execution than
"it ran!"
In your case, you have non-trivial requirements describing
when a customer is eligible for rewards.
And those requirements are encoded in the source code.
Therefore, when the code runs, it cannot possibly
do the wrong thing. It's a tautology. What it does, is correct.
It seems unlikely that all stakeholders should be interacting
with the code to understand how eligibility was handled
last year and how revised rules will be used in the coming years.
Recommend that you write a ReadMe.md, a Confluence wiki page,
or some other English language document that stakeholders
are better equipped to interact with.
Minimally, you have a need to explain to customers what
affects their eligibility.
And they won't be reading the dart code.