Self-documenting code helps explain a piece of code to other developers on a project without the need for actual documentation. The readability of our code is an essential part of making code easier to understand for developers that didn’t write the code. You could argue that it’s even crucial for yourself since you might visit your own written code months later and ask yourself, “What does this do again?”
While self-documenting code is excellent, it doesn’t permanently remove the need for actual documentation. On the other hand, there are many examples of where documentation exists while it doesn’t provide additional context. Writing quality documentation for your code is an important skill to own as a developer when working on a team, so it’s my job in this article to share with you my learnings of over 10+ years of app development.
What is self-documenting code?
Self-documenting code uses human-readable names and takes away the requirement to add documentation. Its advantages can be listed as follows:
- Improved readability of your codebase
- Code is easier to understand
- Forces to think of single responsibility
An example could be the isEnabled
property on a button:
struct FormButton {
let isEnabled: Bool
}
Anyone can tell that the isEnabled
property reflects the enabled state of the button. In this case, adding documentation would not add any value since the code is self-describing already:
struct FormButton {
/// A Boolean value indicating whether the button is in the enabled state.
let isEnabled: Bool
}
Method names can be self-explanatory too:
let numbers = [0, 1, 2, 3, 4, 5]
extension Array where Element == Int {
func evenNumbers() -> [Element] {
filter { number in
number % 2 == 0
}
}
}
print(numbers.evenNumbers()) // [0, 2, 4]
The above extension on an array filters out any odd numbers, resulting in a variety of even numbers only. Adding documentation would not add any extra value:
/// Filters out all odd numbers, resulting in a collection of even numbers only.
func evenNumbers() -> [Element] {
filter { number in
number % 2 == 0
}
}
Recognising and improving redundant documentation
It’s important to understand that redundant documentation doesn’t mean it is unnecessary. Instead, it might indicate documentation of low quality.
An example could be this form submission method:
struct FormSubmitter {
enum FormError: Error {
case emptyName
}
let name: String
/// Submits the form.
func submit() throws {
guard !name.isEmpty else {
throw FormError.emptyName
}
// Continue form submission...
}
}
You could argue that the submit method documentation is not adding any extra context. While this is true, it doesn’t mean we can remove the documentation and be okay with it. In this case, it’s much better to improve the documentation by explaining edge cases like the possible thrown error:
/// Submits the form using the input `name`.
/// - throws: A `FormError.emptyName` if the input `name` is empty.
func submit() throws {
guard !name.isEmpty else {
throw FormError.emptyName
}
// Continue form submission...
}
Adding detailed documentation improves the readability of the code and makes it easy to reason about the implementation of the submit method by just looking at the Quick Help panel:
Breaking code apart to increase readability
You can improve readability by breaking code apart and using self-documenting code. For example, our form submission method could have had more restricted requirements for the input name:
func submit() throws {
guard !name.isEmpty, name.count > 5, name.rangeOfCharacter(from: .whitespacesAndNewlines) == nil else {
throw FormError.invalidName
}
// Continue form submission...
}
The guard statement verifying the requirements becomes harder to read since it contains three different assertions. We could break this code apart into separate properties and increase readability:
func submit() throws {
let isNameLongEnough = name.count > 5
let nameContainsWhitespacesAndNewlines = name.rangeOfCharacter(from: .whitespacesAndNewlines) != nil
guard isNameLongEnough, !nameContainsWhitespacesAndNewlines else {
throw FormError.invalidName
}
// Continue form submission...
}
The exciting part of breaking code apart is making your code easier to understand and read. While writing this article, I broke down the example and realized the redundancy of the isEmpty check above. Within the first guard statement example, it wasn’t immediately apparent that the count check was already taking care of the empty check. In other words, breaking down your code improves readability and makes it easier to recognize parts that you can improve.
The added value of single responsibility
If your class or method contains many different responsibilities, it’s probably tough to write a good method name.
func downloadImageAndApplyFilterAfterCroppingAndPresentGallery()
The above method name is an extreme example. However, we could use this to explain single responsibility and show you the difference if we split this method into multiple ones.
func userDidSubmitImage() {
downloadImage()
cropImage()
applyFilterToImage()
presentGallery()
}
By splitting each part into a separate method, we automatically create a more readable enclosing method. It’s also easier to understand the code as every single method is now responsible for only one thing.
Use extensions on existing types
Extensions on existing types can immediately improve your readability and explain your code.
- It’s clear on which object a method is performed
- Code is easier to re-use throughout the codebase
- Keeps your classes small by moving logic into a file containing the extension
The example showed earlier contained method names in which we had to name “image” all the time: downloadImage
, cropImage
, applyFilterToImage
. Mentioning the image keyword would be redundant by writing an extension to the image type:
extension UIImage {
func cropped() -> UIImage { ... }
func applyFilter() -> UIImage { ... }
}
func userDidSubmitImage() {
let image = downloadImage()
.cropped()
.applyFilter()
presentGallery()
}
The extension methods clarify that we’re modifying the image we’ve just downloaded. In some cases, you can improve code readability by combining extensions with type aliases. All together, it makes your code a lot more self-documenting.
Stop using trailing closures
You can use trailing closures when a function’s final argument is a closure expression. It allows you to skip the argument label for the closure as part of the function call.
/// Including the argument labbel:
viewController.dismiss(animated: true, completion: {
})
/// Skipping the argument label:
viewController.dismiss(animated: true) {
}
Although you probably know it’s a completion callback in most of these cases, it does not necessarily have to be the case. And even if so, a just started colleague might not know what is going on. In that case, they need to go to the method declaration to find out what is going on.
Think about it when you implement a method containing a trailing closure and ask yourself whether it’s clear when the closure executes.
Stop using a type alias for a closure
It’s pretty common to write a type alias representing a closure.
typealias CompletionCallback = (_ result: Result<String, Error>) -> Void
func download(_ url: URL, completion: CompletionCallback) {
// ...
}
However, it’s hard to tell what is in the CompletionCallback
closure when navigating through the methods. Therefore, it’s better to write out the closure, even if used in many places. If your closure is quite complex and it, therefore, feels better to use the type alias instead, you might want to rethink your method’s responsibility to see whether it’s not doing more than it should.
Naming is hard
Naming is hard, and we all know it. How many “Managers” do you have in your codebase?
Unfortunately, I don’t have the golden bullet to solve this problem. What helps me explicitly write what a class is doing, combined with a single responsibility. Instead of DownloadManager
I would call it a Downloader
, for example. Use semantic naming and try to describe what a method is doing.
Should I stop writing documentation and comments?
No, definitely not! Self-documenting code helps other developers to understand your codebase. It doesn’t mean there shouldn’t be any comments or documentation, but only necessary ones. Writing a lot of documentation like “Downloads the image” for a method called downloadImage()
only makes your code harder to read.
Therefore, start by writing self-documenting code and see whether any extra explanation is needed. Don’t just comment or document for the sake of documenting every single method or class.
It’s also important to discuss this with your development team and agree upon rules around self-explanatory code. Rules around documentation can be added value to your coding guidelines.
Self-explanatory code and public APIs
Opinions differ when talking about self-documenting code in public APIs. The earlier example demonstrating the isEnabled
property of a button suggested not adding any documentation since the code is self-explanatory. Yet, looking at the official Apple documentation, we can still see quite a bit of documentation.
You might want to reconsider adding documentation when writing public APIs for packages or frameworks. Some would say redundant documentation indicates that a developer took time to write documentation and would’ve written down edge cases if needed. Since third parties will use your code, there’s no standard agreement on omitting redundant documentation as you have within your development team. Therefore, the lack of documentation could raise questions for the implementors:
- Is documentation missing since it was not adding any value?
Or:
- Did the creators of the framework forget to document this code and am I missing any edge cases?
You could prevent confusion by clearly indicating the use of self-documenting code inside the readme of your package. Otherwise, you could decide to add documentation to each public API while consciously adding extra context on top of the self-explanatory code.
Conclusion
Self-documenting code is a great tool to increase the readability of your code by taking away the need for documentation. You can also improve readability by breaking code apart into self-explaining properties or methods. While writing public APIs for a package, you might want to consider still adding documentation. When you recognize redundant documentation, verify whether you can just remove the documentation or whether an improvement of the existing documentation makes more sense. The end goal should be to make your code easy to understand with as little clutter as possible.
If you like to prepare and optimize, even more, check out the optimization category page. Feel free to contact me or tweet me on Twitter if you have any additional tips or feedback.
Thanks!