Need advice on commenting (doxygen)

Started by
12 comments, last by RobTheBloke 10 years, 10 months ago

What does the function actually do?

Does it have any side effects?

What are the valid values it can take? (i.e. it takes BaseObject* as an arg, but that type must be a OneLeggedYetiMonster*)
Most importantly, how will the function fail? (will it throw an exception, what are the error codes returned, and most importantly what arguments will cause those errors?)

Docs are most useful when they remove the need to go hunting through code for answers.

As an example, I've recently inherited a codebase where there are no return error codes, and every error is thrown instead. This means you have a bunch of member functions that look like this:

void load(const char* filename);
void update(float time);

Now some people claim their code is self-documenting, however in my experience, those people often produce some of the worst code interfaces known to mankind! (not always, but it's quite common imho). So whilst I have a pretty good idea what load/update do, and the arguments are self-explanatory, what isn't mentioned in the function names (or comments, because there aren't any), is that: passing in an out of range time value to update, causes all data to be destroyed, all external references to be invalidated, and an InvalidTimeValueRangeError to be thrown - unless of course you have set a kClamp flag on the object, in which case a TimeValueNotClampedError will be thrown. All of that is perfectly obvious from the function names. Obviously. :/

Sadly with this code I'm maintaining at the moment, because all of the functions return void, and no documentation exists for any of the functions (because it's "self-documenting"), I now find myself simply not trusting any of the code at all (even though it's fairly stable, and has been used in production for a few years). Every time I now make a call into the lib, I find myself digging through the code to make sure it's actually doing what I think it should be, before writing some doxygen comments to help the next person who ends up maintaining it! If I didn't go to those lengths, it would be extremely easy for me to make a commit, which would then blow up a week or two later due to some edge case I hadn't considered might occur.

The nice thing about doxygen comments is that it provides a contract between you, and the users of your code. It's kinda nice to be able to give someone a library, and then say to them "everything you need to know is in the docs". However it really sucks when that person has to constantly step through your code to find out what it's actually doing. Some people may say that in those cases the code is at fault (which is probably true), however I personally will accept a few WTF moments in an interface design, if it's actually explained how those WTF's work (because getting the job done quickly is usually your main aim). So if you have a function with 16 possible code paths, but only 3 of them will do what you expect - you should really prioritise explaining why those other 13 will cause a failure. If you have a simple getter/setter method, then it probably doesn't need as much doxygen TLC!

For a class, a higher level description about how to use it, and it's relationship with the rest of the system will be useful in the long run (and refer people to the relevant functions with detailed descriptions). I also like using the \name flags within a large class to group the functionality a little bit, i.e.

[source]

class Foo

{

public:

/// \name ctors

/// \brief probably don't need this comment

/// \brief default constuction

Foo();

/// \name getters / setters

/// \brief probably don't need this comment either

const char* getName() const;

/// \name collision detection

/// \brief probably needs some high level description here

/// \brief this could benefit from some info

bool someCollisionTest(SomeArg arg) const;

};

[/source]

Advertisement

Thanks again for all the suggestions and tips, this is really helping me out a lot. I've just got some concern about the "self-documenting"-topic, I'd further like to discuss. Just as RobTheBloke said, I've also come across code that claimed to be self-documenting, but really left me without much clue of how to actually use the function. Lets take frobs code as a starting point, to describe my view on what I personally migh be missing:


bool Inventory::CanAdd(ObjectGuid)

Obviously that function only checks whether an item of given id can be added to the inventory. But why can it fail? Is it because the players inventory slots are full? Is it because the player has the maximum amount of this item? Is the player only allowed to have that item once? Does the player carry around too much weight? You see, I'm completely talking out of my own perspective here, but that is information I'd really like to have when working with this inventory-system. Maybe this is information already documented for the inventory class itself, but even then a little hint on where to find the documentation would be nice. Even if its an interface, a hint for the "correct" implementation would be nice.


bool Inventory::TryToAdd(ObjectGuid)

Again, while the method seems to be single responsible and have a descriptive name, wouldn't one want to at least have a hint what this method does, in case the adding fails, for example? Even if it does nothing in that case, wouldn't a at least some descriptive comment like "Checks whether item can be added to inventory, does nothing if this fails." rather be wanted to at least ensure that this function does what it says, and doesn't throw a FailedToAddException or deletes the world in case the adding failed? Especially talking about non-complete documented and probably not-optimized codebases. Isn't it seen as an advantage as a user of such an API being able to say from documentation "Well, that function does exactly what I'd expect it to do" instead of "Uhm, so is this function just that plain simple as its name says, or is it simpy undocumented and crashes and burnes my hard drive if I call it in the wrong situation?" Specially talking about subjective perception. What might seem obvious for me might not be obvious to someone else. I'm asking, since a good percentage of you guys seem to rather tend toward letting code be self-documentative instead of adding at least some plain simple comment that on a more narrow basis states: Yep, I'm function X and I do Y, nothing else, which I'd prefer. Why is that?

So is it really just me being nitpicking or wanting information noone would expect when using a libary, or does our own perspective of whats self-explanatory sometimes misguide us into scrapping documentation under the dellusion of being more straight-forward? Well seeing that I already got a huge bunch of input, I should be pretty much able to work on it, but still I'd here some opinions from you guys. I'm always in the mood for some exchange of ideas.

So whilst I have a pretty good idea what load/update do, and the arguments are self-explanatory, what isn't mentioned in the function names (or comments, because there aren't any), is that: passing in an out of range time value to update, causes all data to be destroyed, all external references to be invalidated, and an InvalidTimeValueRangeError to be thrown - unless of course you have set a kClamp flag on the object, in which case a TimeValueNotClampedError will be thrown. All of that is perfectly obvious from the function names. Obviously. :/

What an incredibly stupid system.

The choices are either succeed, or fail and rollback gracefully.

So whilst I have a pretty good idea what load/update do, and the arguments are self-explanatory, what isn't mentioned in the function names (or comments, because there aren't any), is that: passing in an out of range time value to update, causes all data to be destroyed, all external references to be invalidated, and an InvalidTimeValueRangeError to be thrown - unless of course you have set a kClamp flag on the object, in which case a TimeValueNotClampedError will be thrown. All of that is perfectly obvious from the function names. Obviously. :/

What an incredibly stupid system.

The choices are either succeed, or fail and rollback gracefully.


Well, there is some sane logic buried in there somewhere (given the context of it's usage). It's just very difficult to ascertain what that logic is from looking at the interface. It's one of those libs your saddled with for historical reasons (and which is now underpinning a tonne of other tools). But yeah, I'm binning the whole lot, however it takes time to safely migrate the tool-set onto the new system. Still. Pays the bills.... ;)

This topic is closed to new replies.

Advertisement