README Masterpieces
Jump to:
When meeting candidates at Lookahead we often discuss what makes for an extraordinary code test. My general advice is to “make it your masterpiece”. Matt has written about this before so be sure to check that out.
Diving a bit deeper, I break down our rubric for evaluating code tests into two categories: the code, and everything else. “Everything else” is itself twofold: version control, and documentation.
In this post, I will touch on documentation, in particular what I believe are important things that should be included in every README. Where possible I will try to illustrate with examples.
I’m a documentation enthusiast and I believe a comprehensive README adds tremendous value to a codebase. It’s the face of your project and the gateway through which a new contributor enters the development cycle. Sadly there doesn’t seem to be a great deal of rigour around what READMEs should contain, and too many projects don’t have one at all. Having reviewed an awful lot of code tests, I’d like to take a shot at identifying and explaining what I feel are the elements that every README should contain.
Let’s begin with an outline:
- Title & Description
- Environments
- System Dependencies & Configuration
- Application Installation Instructions
- Operating Instructions
- Testing Instructions
- Overview
- Discussion
- Contributing
Below is some short commentary on each of these points.
TITLE & DESCRIPTION
State your application’s name and describe its main functionality. A high-level description of what it does is an obvious but often overlooked place to start. If it’s part of a larger system, you can also discuss how it fits into the larger picture.
ENVIRONMENTS
Provide details about the various environments the code is expected to run in, including:
- OS and version
For example:
This application was developed on Ubuntu 14.04 x86_64
Generally speaking you’ll have test/staging and production environments that are similar (if not identical) to your development environment, however that’s not always the case. It may be necessary to provide additional commentary on specific environments.
- Compatible/tested environments
Ideally, you should test your code in a variety of environments and list those that are found to be compatible, for example:
Ubuntu 12.04, ruby 2.1.0p0
Ubuntu 14.04, ruby 2.1.2p95
This provides a useful lock for developers who are setting up a development environment or deploying the application.
- Incompatible environments
It can be useful to explicitly exclude environments that you know won’t support the application. For example:
ruby < 2.1.0
Whereas before I knew that 2.1.0p0
was supported, I now know that anything below that won’t work.
SYSTEM DEPENDENCIES & CONFIGURATION
Before we install and run the application, we need to ensure our environment is correctly configured and necessary system-level dependencies have been installed.
You should provide:
- a list of system-level dependencies,
- instructions on how to check for their presence/version,
- installation instructions, or a link to instructions.
For example, for applications that require Ruby you might write:
ruby => 2.1.0p0
To check your version run:
$ ruby -v
To learn how to install ruby visit [https://www.ruby-lang.org/en/installation/]https://www.ruby-lang.org/en/installation/().
While installing Ruby doesn’t require much configuration, other dependencies will. For example, if you’re writing a web application to be served by Nginx, you should provide information about how to configure the server, etc.
APPLICATION INSTALLATION INSTRUCTIONS
After you’ve ensured your environment is properly configured and the requisite system dependencies have been installed, you should provide instructions for installing the application itself. For example, if you’re writing a Ruby application you might provide instructions like:
To install the application, cd
to the root directory and invoke:
bundle install
USAGE INSTRUCTIONS
Provide the invocations for running/starting the application. Discuss salient configuration options, common configuration cases, and the like. For example:
To run the application in interactive mode, invoke:
$ app
To run the application with an input file, invoke:
$ app < /path/to/input
To run the application with debugging flags, invoke:
$ app -d
And so forth.
TESTING INSTRUCTIONS
Provide the invocations for executing the application’s test suite.
OVERVIEW
Provide a detailed overview of the application’s functionality as it relates to concerns such as input format, business domain(s), output format, and so forth. For example:
The application is designed to read x
from $stdin
and print y
to $stdout
.
INPUT FORMAT
x
’s passed into the program are expected to be be comma-separated values with the following format
`, ,
Example:
0, hello, #FFFFFF
<property1>
is expected to be a whole value, thus only digits are allowed.
<property2>
is expected to be a string.
<property3>
is expected to be an HTML Color Hex code.
OUTPUT FORMAT
A y
will be printed with the values computed from the x
passed into the application, for example: 0 HELLO WHITE
WHAT’S HAPPENING?
Clearly this application is useless. All it does is transform input and print it out. The transformations are not even useful.
EXAMPLES
Input: 1, hello, #FF0000
Output: 1 HELLO RED
Even with such a useless illustration, hopefully you get the picture of what an “Overview” should include.
DESIGN
Provide a step-by-step description of how your application progresses from initialization, to taking and processing input, to doing work on that input, to printing output. For example:
DESIGN
1. LAUNCH
$ app
app
is an executable in your load path. It is a Ruby script with the following lines:
require 'app' App:Application:new.run
2. LIB/APP/APPLICATION.RB
The #run method reads in x
’s from the input source (defaults to $stdin
) and passes them to a parser. The parser returns a structured item which is passed to y
for transformation.
3. ???
Describe how the y
is passed around from class to class, until it finally reaches…
4. LIB/APP/WHATEVER-EXITS.RB
After the y
is printed, the application exits.
Certainly I could follow the path by reading your code, but do your readers a favor and spell it out for them. I’ve found that writing in plain english has the dual effect of communicating your application’s design and, if it’s too complicated to express in plain English, revealing needless complexity in your design.
DISCUSSION
High-level discussion about your design choices and how you arrived at them is extremely helpful. For example, if you implemented a particular software pattern, what is it? How does it solve the problem? Why did you choose it over other possibilities? For example:
For this application I chose to implement the <pattern>
pattern because of <reasons>
. I considered implementing <alternative pattern>
but decided it was not the best option because of <further reasons>
.
You should also discuss the parts of your implementation that you’re proud of. For example:
I was extremely happy with how <class>
was implemented. I feel it has a clever <feature>
, it’s interface is <superlative>
, and is otherwise <good|great|amaze>
.
You can also discuss details you’re not so sure about, for example:
I am not happy with <class>
, I wasn’t sure about what I wanted to accomplish with it. It now resembles an <anti-pattern|ogre>
that makes me deeply upset to think about.
If I encounter some code that’s… less than optimal, I be less judgy if the author is reflective about shortcomings in their README. No implementation is perfect, and demonstrating your thoughtfulness goes a long way in showing your maturity as a developer.
LICENSE
Google it.
CONTRIBUTING
A whole topic in itself.
CONCLUSION
READMEs say a lot. They tell how sympathetic you are to other developers, how comprehensive your general approach to software development is, how well you can communicate design decisions and issues, and generally how mature your development habits are. I guess the point is: IT’S IMPORTANT. Take the time to write a README masterpiece and your project will immediately level up.