Good Documentation

 ·  iOS, Development  ·  Tagged Documentation, Comments, Best Practice, HeaderDoc and Doxygen

Every day I find myself relying on well documented code to work efficiently and productively. Good documentation helps me understand what a class or method does, what it was created for, or what that other developer was thinking when they wrote the module my code hinges on. Unfortunately, I have a history of preferring to read good documentation much more than I’ve preferred writing it, often because it seems too difficult and time consuming.

Lately however, I've been working on writing better documentation, and I've found it's much easier than I expected. In this article, I'll share some tips to help you document your own code more quickly and easily. We'll look at what makes or breaks documentation, in which cases it is most vital, and what my process is for writing better docs.

First, What is Bad Documentation?

The first step to writing good documentation is understanding what makes bad documentation bad. We've all been burned by bad documentation before. It takes many forms: it can be too complicated, omit important details, or even worse can be just plain wrong. Basically, the defining characteristic of bad documentation is that it fails to give the reader the information they need to use a piece of code correctly.

The Good (Documentation) Stuff

Good documentation informs the reader of the details they need to know. When someone reads the documentation of your method or class, they're looking for details about how that code behaves and works-details like:

  • What does the code do?
    • Sometimes, the most helpful thing for a reader is understanding what functionality is available to them in a piece of code and whether or not that code can help them accomplish their goals. A conceptual description of a class or method can go a long way to doing this.
  • Parameters and Edge Cases
    • Readers not only need to know what values to pass to a method, but how the method behaves if they pass in unexpected things, such as nil, or a value outside of an expected range.
  • Side Effects
    • If invoking a method changes the application or object state or does something non-obvious, like caching a result, this is something the reader will want to know about.
  • Failure Behavior
    • Does your method have expected failure cases? If so, the person using it will want to know about these cases so they can handle them accordingly.
  • Execution Environment
    • Is your method not thread safe? Does your method need to be run on the main queue? Does it use a lot of resources or run for a very long time? Again, these are great things to call out in documentation.
  • Return Values
    • If your return value is non-obvious, it's especially important to note what that value means, and what it tells you about the method's execution.

While this might seem like a lot of information to document, you don’t have to write a novel for each of your methods. Good documentation is concise. Don’t burden your readers with any details they don’t need. Think about it like encapsulation: tell them what they need to know to use your code while avoiding too much detail about the implementation.

Writing Good Documentation

Even armed with the knowledge of what good documentation is, approaching a chunk of code to document can be daunting. Here are a few steps I use to document a method from scratch.

  1. Figure out what your method does.
    • You should be able summarize what your method or class does in a single sentence, as if you were describing it out loud to a friend.
    • If you're having trouble describing a method in a single sentence, that method might be doing too much. Try refactoring it into smaller, more basic components.
  2. Look at method parameters.
    • Document what each parameter is used for to give the reader an intuitive understanding of how to use it.
    • If there are any edge cases or other interesting behaviors of your parameters, be sure to include those in your documentation.
  3. Scan your code for side effects, error cases, and execution assumptions.
    • Document the things in your code that affect the caller, like thrown exceptions or changes to object state.
    • Looking for error/edge cases to call out in your documentation can also help you write better code, because it helps you think critically about how your code handles uncommon situations.
  4. Look at how your code behaves.
    • Does your method behave differently based on the application’s or object’s state? Be sure to document each of the different behaviors and the conditions that trigger them.
  5. Finally, look at your return value.
    • Make sure you communicate what the return value signifies, and the different values it can have.

By following these steps and describing your code in straightforward terms, you can quickly and easily churn out some good documentation. Let's give it a try!

Example Time

Let’s apply the steps above to write some documentation for some real code. For our example, we’ll use URLMock’s ‑[UMKMockHTTPMessage setBodyWithJSONObject:], which sets the body of an HTTP message to the JSON representation of an object.

Aside: I'll be using the HeaderDoc format (which you can read about below) to structure my comment blocks, so you'll see things like @abstract or @param which are tags helpful to the format's auto-generation engine.

- (void)setBodyWithJSONObject:(id)JSONObject
{
    NSData *JSONData = [NSJSONSerialization dataWithJSONObject:JSONObject options:0 error:NULL];
    if (!JSONData) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:UMKExceptionString(self, _cmd, @"Invalid JSON object")
                                     userInfo:nil];
    }

    self.body = JSONData;
    if (![self valueForHeaderField:kUMKMockHTTPMessageContentTypeHeaderField]) {
        [self setValue:kUMKMockHTTPMessageUTF8JSONContentTypeHeaderValue 
        forHeaderField:kUMKMockHTTPMessageContentTypeHeaderField];
    }
} 

And as I said earlier, the best place to start documentation is with a description of what the code does. I'll use the @abstract tag that HeaderDoc provides specifically for short descriptions like this.

/**
  @abstract Sets the receiver's body to a serialized form of the specified JSON object.
*/
- (void)setBodyWithJSONObject:(id)JSONObject {...}

Next, lets look at the method's JSONObject parameter. The noteworthy things about it are that it is used as the JSON body for the receiver, and that it must be a valid JSON object (as seen by the error handling around the NSJSONSerialization calls). The documentation of +[NSJSONSerialization
dataWithJSONObject:options:error:]
states that its JSON object parameter must not be nil and must be a valid JSON object, we'll echo those requirements in the parameter documentation. This time I'll use the HeaderDoc tag @param.

/**
  @abstract Sets the receiver's body to a serialized form of the specified JSON object.

  @param JSONObject The JSON object to serialize and set as the receiver's body. Must not be nil, and 
  must be a valid JSON object.
*/
- (void)setBodyWithJSONObject:(id)JSONObject {...}

Now we move on to steps 3 and 4 from above. I see that the method handles an error case by throwing an exception, and has a side effect of invoking ‑setValue:forHeaderField: if a specific header value isn't already set, so I'll need to call those behaviors out. Note that the kUMKMockHTTPMessageContentTypeHeaderField and kUMKMockHTTPMessageUTF8JSONContentTypeHeaderValue constants correspond to "Content-Type" and "application/json; charset=utf-8", respectively.

/**
  @abstract Sets the receiver's body to a serialized form of the specified JSON object.

  @param JSONObject The JSON object to serialize and set as the receiver's body. Must not be
      nil, and must be a valid JSON object.

  @throws NSInvalidArgumentException if JSONObject is not a valid JSON object.

  @discussion If the receiver does not already have a value for the Content-Type header field,
      sets the value of that header to "application/json; charset=utf-8".
*/
- (void)setBodyWithJSONObject:(id)JSONObject {...}

Lastly, Step 5 would have me look at the return value, but this method doesn't return anything so we're done! We've generated some pretty useful documentation, and it wasn't that hard and didn't take too long.

Parting Pointers

Crafting helpful and concise documentation may seem intimidating at first, but hopefully with the tips and steps I've explained, you'll be motivated to write your own good documentation. Before I go, I'll leave you with a few more tips which can help you improve the scope and efficiency of your documentation.

  • Pick a Format
    • It's a good idea to pick a documentation format before starting. Sticking to a consistent format helps us maintain readability as we write our documentation.
    • HeaderDoc, appledoc and doxygen are all popular formats worth checking out.
  • Use Consistent Terminology
    • When describing a complex system, using consistent terminology for the same ideas or objects helps the reader relate similar functionality across multiple methods.
  • Mark designated initializers
    • When subclassing and creating your own initializer, make sure to mark the designated initializers for your class. Subclasses need to know what the designated initializer is so that they know to override it.
  • Create Self-Documenting method names
    • You don't need to write chapters of documentation for every little method. Care to take a guess at what a method like ‑(NSInteger)numberOfItems would do on your custom ItemContainer class? There are some cases where you don't need to go overboard with documentation.
  • Write Documentation as if the reader only has access to your headers, not your implementations.
    • Again, this is the encapsulation point. Documentation should tell the user exactly what they need to know to use your API; no more, no less.

Now you are armed with all the knowledge and experience you need to go forth and write your own documentation. If you have your own documentation tips, message me on Twitter at @dfowj! Thanks for reading!