Writing clean code is like writing poetry that is easily understood, clearly written, and concise.
Clean code means a scalable organization. It's the difference between a junior dev and a senior engineer. It means being able to pivot plans without much chaos.
It took me several recommendations to actually gain up the courage and read Clean Code. It's one of those books you can't help but judge by its cover. I'll admit though - it lives up to the hype. Insights are timeless, specific, practical, and with a spark of nerdy humor that gets to my heart. Today, I share with you my top take-aways.
1. Your code being readable is as important as it being executable
The majority of the cost of a software project is in long-term maintenance. Therefore, the code you write should clearly express your intent so that new developers can easily hop in and understand what is going on and why.
The clearer the author can make the code, the less time others will have to spend understanding it. This reduces defects and shrink the cost of maintenance.
- How to do this: good naming practices + single responsibility classes and functions + writing tests
2. Later equals never
Let's be honest. We've all said we'd go back and clean it up later and then forgot about it.
Don't leave scraps of useless code that no longer serve a purpose. They confuse other devs and provide no value. When changing a feature, always make sure to delete the old code. Tests should notify you if you're breaking stuff elsewhere anyways.
- How to do this: Deleting code can be scary, especially in a big architecture. That's why tests are key to keeping your code clean so you can delete with confidence.
3. Functions should be small
The first rule of functions is that they should be small - hardly 20 lines long. This is because the smaller and more focused the function is, the easier it is to choose a highly descriptive name.
When it comes to a function's arguments: the ideal number is 0. Then goes 1, then 2, but more than 3 should be avoided where possible.
- How to do this: Functions should follow the Single Responsibility Principle and the Open Closed Principle.
4. Duplication is the evil of all code
Duplication is the enemy of a well-designed system. It represents additional work, additional risk, and additional unnecessary complexity.
- How to do this: Avoid it at all times by keeping your code DRY (Don't Repeat Yourself), isolated, and modular.
5. The only good comment is the one you found a way not to write
"Nothing can be quite so helpful as a well-placed comment. But comments are, at best, a necessary evil."
The proper use of comments is to compensate for our failure to express our self in code. Comments are always failures. We must have them because we cannot always figure out how to express ourselves without them, but their use is not cause for celebration.
Thing is, comments often lie. Not always, and not intentionally, but too often. The older a comment is, and the farther away it is from the code it describes, the more likely it is to be just plain wrong.
The reason is simple: programmers can't realistically maintain all of them so all too often they become separated from the code they describe and become orphaned blurbs of ever decreasing accuracy.
Usually, comments are crutches or excuses for poor code or justifications for insufficient decisions, amounting to little more than the programmer talking to him/herself.
- How to do this: descriptive naming practices that allow devs to know what the variables represent + provide tests so other devs can see what the main functionalities are.
6. An object exposes behavior but not data
A module should not know about the innards of the objects it manipulates. Objects hide their data and expose operations. This means that an object should not expose its internal structure through
accessors because to do so is to expose, rather than to hide, its internal structure. There's no need for everyone to see you naked.
- How to do this: The scope of variables should be as local as possible, without exposing more than is necessary.
7. On testing
Test code is just as important as production code and as such, it must change and grow as the production code evolves.
Keeping tests clean is so important because without them, you lose the very thing that keeps your production code flexible, maintainable, and reusable. Without tests, every change is a possible bug. Tests eliminates the fear that cleaning code can potentially break it.
Readability keeps tests clean because they are an opportunity to explain to other devs the intent of the original author in plain English. This is why we want to test only a single concept in each test function so that the test is descriptive, reads easier, and is easier to track when it fails.
Clear separation and good test writing help define expectations and keeps the scopes clear.
How to do this: Follow the FIRST Principles:
- Fast: tests should be ran quickly. If you have to wait too long for each test you're less likely to run them often.
- Independent/isolated: tests should not depend on each other or build from each other. They should be as isolated as possible.
- Repeatable: tests should be repeatable in any environment - development, staging or production.
- Self-validating: tests should have a boolean output. They either pass or fail.
- Thorough: tests should aim to cover all edge cases, all security issues, every use case and happy path.
8. On error handling and exceptions
Each exception that you throw should provide enough context to determine the source and location of the error. Although you usually get a stack trace from any exception, a stack trace can't tell you the intent of the operation that failed.
You should avoid passing null in the code whenever possible. And if you are tempted to return null from a method, consider throwing an exception. Set error handling as a separate concern, something that is viewable independently of our main logic.
- How to do this: Create informative error messages and pass them along with your exceptions. Mention the operation that failed and the type of failure. Our most important concern should be how they are caught.
9. On classes
Classes should be small, but rather than counting lines, we should count responsibilities.
The names of classes are key to describe which responsibilities it fulfills. Our systems should be composed of many small classes, instead of a few large ones, where each small class encapsulates a single responsibility. Each class has a single reason to exist and collaborates with a few others to achieve the desired system behaviors.
There is seldom a good reason to have a public variable. Loosening encapsulation is always a last resort and there should be a small number of instance variables at all. Good software designs accommodate change without huge investments and rework. Narrowing the scopes of variables allows us to do this more easily.
- How to do this: Separation of concerns is one of the oldest and most important design techniques in our craft. This is for a reason. Classes should be open for extensions, but closed for modification. In an ideal system, we incorporate new features by extending the system, not by making modifications to existing code.
10. On format
Each blank line is a visual cue that identifies a new and separate concept.
Local variables should appear at the top of the function.
Instance variables should be declared at the top of the class.
Short lines are preferred to long ones — usually around 45 characters - 100-120 characters. After that, it's probably just careless.
- How to do this: Most of these options can be passed to a linter in your CI or text editor. Take advantage of these tools to keep you code as clean as possible.
Principles of Software Development
In summary, follow these practices and your code will be clean:
a) Naming variables: Implicitly knowing where things are - and using approaches such as suitable naming — is crucial to keep the code readable and hence, maintainable.
"You should name a variable using the same care with which you name a first-born child."
The hardest thing about choosing good names is that it requires good descriptive skills and a shared cultural background. Clean code is read and enhanced by any developer.
The name of a variable, function, or class, should answer all the big questions: why it exists, what it does, and how it is used. If a name requires a comment, then the name does not reveal its intent as it should. Programmers must avoid leaving false clues that obscure the true meaning of the code.
Longer names trumps shorter names, and any searchable name is better than a constant in the code base. Single-letter names can only be used as local variables inside short methods: the length of a name should correspond to the size of its scope. Methods should be verbs or verb phrases; a class name should not be a verb.
b) Minimal dependencies: We should avoid letting too much of our code know about the third-party particulars. It's better to depend on something you control than on something you can't control. If you do, it ends up controlling you.
c) Tidiness: A piece of code should be where you expect to find it. Code bases should be intuitive to navigate through; developer's intentions feel clear.
d) Cleaning: Don't litter your code with useless code scraps or lines of code that capture history or wishes of the future. Reduced duplication, high expressiveness and early building of simple abstractions.
e) Standardization: Your code should keep a consistent coding style and set of practices across repositories.
f) Self-discipline: you frequently reflect on one's work and are willing to change and improve. However, you don't fall into the hype too fast. Study new stacks with depth and purpose.
Truth is, keeping your code clean is key to maintaining innovation and organizations alive.
As systems scale and grow, it is inevitable that old technology practices and hacks get left behind in the code. Keeping your code base clean is more than a nice gesture for other devs. It's a necessity for long-term survival.
The cleaner your code, the happier the devs, the better the product, the longer it lasts.