Putting a Stop to Comments
Putting a Stop to Comments
Commenting code is often seen as a way of creating human-readable code. But, if your code is of high quality, are they really necessary? Read on to find out!
Join the DZone community and get the full member experience.Join For Free
Comments are an excellent tool for improving the understanding of code and can help a great deal in inspecting vague code. The major benefit of comments, though, is also their major deficit: they are ignored by the compiler. While they have a very important role to play in documenting code, their overuse can not only clutter code but also lead developers astray. In this article, we will look at the purpose of comments and how to avoid writing comments when better code is a much more effective tool for completing the same task.
Commenting Code
Comments have a very important role to play in both clean code and self-documented Agile projects, but their overuse can not only create visual clutter within otherwise clean code, but it can also inject indirection and actively lead developers astray. Considering this, there are places where comments are a good choice (although other alternatives do exist):
- Copyright and legal information: Many companies or projects require that the copyright information associated with a system be placed at the top of each source file. While this type of commenting does not add any information particular to the logic contained in a module, package, class, or method, it is nonetheless an important legal aspect to software engineering. If possible, these comments can be autogenerated or automatically added to files before being pushed, reducing the need for developers to manually include these statements. This also reduces the chance that copyright or legal information will become outdated as the code they annotate progresses.
- JavaDocs on public classes and methods: When creating a public Application Programming Interface (API) or framework, it is often beneficial to include comments for core public methods and classes. For example, the Java Standard Edition (SE) language features an entire suite of automatically created JavaDocs, as does the Spring Framework. For more information on how to document Representation State Transfer (REST) APIs using comments, see the Documenting REST APIs by OpenCredo.
Note that in the latter case, many Integrated Development Environments (IDEs) will display the JavaDocs associated with a method or class when the user hovers over that construct. This not only makes the JavaDocs for classes and methods useful after the comments have been processed and hosted on a project's web server but also while developers are interacting with the classes and methods within their IDE.
As with all other comments, these two types of comments must be updated each time a change is made to the class or method that it is associated with. Forgetting to do so may lead to serious discrepancies, with the comments and code falling out of sync. For example, the following may cause even the most seasoned software engineer to pause for a moment before calling this method:
public class Engineer { public double overtimeMultiplier() { // 25% for straight overtime; 15% extra for engineering salary benefit return 1.35; }
If we look only at the code, we see that an engineer makes 35% more for overtime than he or she would for normal working hours (the 1.0 in the 1.35 ensures that normal time is preserved; for example, if an engineer makes $40 per hour, then he or she would make (1.35)($40) = $54). If we look at the comment, though, we would expect to see a multiplier of 1.40 (25% + 15%, along with the 1.0 multiplier). This leaves us in a predicament: is the code wrong or is the comment wrong (or worse, both)? Even if we have tests, we cannot guarantee that the code and the tests are not incorrect. To solve this, we would need some external source, such as a knowledgeable domain expert, to resolve this issue.
Since a comment is not checked by the compiler, as the code changes, there is nothing requiring the developer to update the documentation to match the source code. As the code base grows, the possibility increases that more and more of the comments will fall out of sync with the code that they supplement. Even the two suggested applications of comments above can cause an issue if not properly maintained. For example, the following JavaDoc could lead a developer astray:
public class Garage { private List<Vehicle> vehicles = new ArrayList<>(); /** * Registers a new vehicle is the assocaited driver has a * valid driver's license. * * @param vehicle * The vehicle to register. * @param driver * The driver associated with the registered vehicle. This * driver must have a valid driver's license in order for * the vehicle to be registerd. * * @return * A receipt confirming the registration of the supplied * vehicle and driver. */ public void registerVehicle(Vehicle vehicle, Driver driver) { if (driver.hasValidLicense()) { vehicles.add(vehicle); } } }
If we inspect the JavaDocs associated with this method, we would except the above method to return a receipt object, but the method does not return such an object. Is the documentation wrong? Or was the code actually supposed to return a receipt object and the developer forget to add it? Uncertainty like this can cause some serious confusion about an otherwise simple section of code. Due to this, we can create a simple rule of thumb about comments:
Be suspicous about the validity of comments: Comments may vary independent of code and there is no guarantee that comments reflect the current state of the code.
Although comments can be very useful, and their inclusion conveys effort on the part of the original author to transfer the intent of the code, they must be taken with a grain of salt. In order to reduce this confusion, we have to look to another technique to document our intentions: clean code.
The Benefits of Clean Code
Writing clean code sounds great in theory, but it can be a difficult goal to achieve in practice. Once it is achieved, though, it removes a majority of the need for textual comments. In essence, if we wrote our code clearly enough, we would not need to relay supplemental information in the form of comments to other developers. For example, the following code contains some very precise comments, but our method body can be improved:
public class Person { private int points = 0; public int getPoints() { return points; } public void addPoint() { points++; } } public class MembershipRegistrar { private final List<Person> goldMembers = new ArrayList<>(); private final List<Person> silverMembers = new ArrayList<>(); public double calculateDues(Person person) { if (goldMembers.contains(person)) { // Gold membership cost an additional $30, but the processing // fees are waived if the person has more than 20 points return 100.0 + 30.0 - (person.getPoints() > 20 ? 10 : 0); } else if (silverMembers.contains(person) { // Silver membership cost an additional $20, but the processing // fees are waived if the person has more than 30 points return 100.0 + 20.0 - (person.getPoints() > 30 ? 10 : 0); } else { // Standard members pay full price return 100.0; } } }
Although we have supplied a decent level of commentary for our domain logic, this logic has not been captured in code. Whatsmore, if the domain knowledge were ever to change, we would be forced to update the comments to stay in sync with the new logic. Instead, we could have just written clearer code and captured the domain logic with more aptly named classes, methods, and variables. For example:
public interface MembershipLevel { public double getCost(); public boolean qualifiesForWaivedProcessingFee(Person person); } public class GoldMembership implements MembershipLevel { @Override public double getCost() { return 30.0; } @Override public boolean qualifiesForWaivedProcessingFee(Person person) { return person.getPoints() > 20; } } public class StandardMembership implements MembershipLevel { @Override public double getCost() { return 30.0; } @Override public boolean qualifiesForWaivedProcessingFee(Person person) { return person.getPoints() > 30; } } public class SilverMembership implements MembershipLevel { @Override public double getCost() { return 0.0; } @Override public boolean qualifiesForWaivedProcessingFee(Person person) { return false; } } public class Person { private int points = 0; private final MembershipLevel membershipLevel; private static final BASE_MEMBERSHIP_COST = 100.0; private static final PROCESSING_FEE = 10.0; public Person(MembershipLevel membershipLevel) { this.membershipLevel = membershipLevel; } public int getPoints() { return points; } public void addPoint() { points++; } public void getDues() { return BASE_MEMBERSHIP_COST + membershipLevel.getCost() - getProcessingFeeDiscout(); } private double getProcessingFeeDiscout() { if (membershipLevel.qualifiesForWaivedProcessingFee(this)) { return PROCESSING_FEE; } else { return 0.0; } } }
Although there are more classes and methods in our cleaner version, there is no need for comments to explain the algorithm that calculates membership dues. In this case, we explicitly create an association between a
Person and a
MembershipLevel, stating that each
Person has an accompanying
MembershipLevel. Regardless of the membership level, the dues for a person are calculated by summing the base membership cost and the additional cost associated with the membership level, minus any discounts on the processing fee. We also explicitly show that each
MembershipLevel has an associated cost and some criteria for discounting the processing fee.
Although we have presently explained the logic of the code, no such explanation is required in order to understand the logic contained in the code. Instead, an inspection of the code would result in a simple, yet solid understanding of what we are trying to accomplish. For example, the body of the
getDues() method can be read in English, as if it were a comment, but we have instituted this clarity as the code itself (not a comment that is not an executable method). If we change the algorithm for computing the dues, we do not have to update any documentation that accompanies the algorithm.
In many cases, transforming a code base into clean code is not a simple task. In fact, when dealing with a reasonably sized project, many of the classes and methods will be anything but clean. In order to get to a state of clean code, we have to refactor. For more information on refactoring, see Martin Fowler's Refactoring and the Refactoring Guru. With this in mind, we can develop a second rule:
When faced with a complicated or unclean code base, favor refactoring the code into a clean state over documenting the algorithms and logic using textual comments.
Note that we should favor refactoring over comments, but this does not preclude us from creating comments. Apart from the previously mentioned use cases for comments, we are faced with real-world temporal and financial constraints, which means that we may not be able to complete a full set of refactorings before moving on to higher priority tasks. In that case, we can write comments about the intent of the code (once we decipher it) and, where applicable, include TODO comments to direct our future time (or the future time of another developer) to improve this code. We work in a real-world with real restrictions: we must do our best to refactor the code to a self-documenting state and leave breadcrumbs for future refactoring work. For more information on clean coding in general, see the seminal Clean Code by Robert C. Martin ("Uncle Bob").
Conclusion
Comments are a great way of conveying supplemental information in software, but they have some serious side-effects. In particular, there is nothing keeping source code from progressing beyond the usefulness of the comments that accompany it. Whatsmore, if other developers come across this code, they are very unlikely to remove outdated comments, since they do not know the original intent of the comment. Thus, stale comments have a tendency to stay in code, prolonging the problem. Due to this, and at the advice of Jeff Atwood's excellent article on comments, we can conclude with a final rule about comments:
Develop software as if comments do not exist.
This is not an excuse to be lazy and leave our code both uncommented and unclean. In most cases, when we require comments, we have developed our solution with insufficient clarity for other developers to understand. Therefore, we should program as if comments never existed, creating code that is clean enough to be easily understood and applying comments only when absolutely necessary.
Opinions expressed by DZone contributors are their own.
{{ parent.title || parent.header.title}}
{{ parent.tldr }}
{{ parent.linkDescription }}{{ parent.urlSource.name }}