The art of clean code
As a software engineer, I sometimes feel pressure on the deadline, but after I finish the task in a rush, I feel guilty and frustrated because of the bad code I have written.
And I often promise myself to refactor the code later as it is working now.
Don’t put off until tmr what I can do today
I am a software engineer, but I am not a professional.
A professional software engineer should be a craftsman that not be afraid to fight for his profession, dignity. Willing to spending time to write the well-crafted piece of code that everyone sees will nod their head and say this is what they expect the code to do.
Be a craftsman
Writing bad code will cost you more in the long one compares with crafting clean code.
Clean code: Productivity progressively increase
Bad code: Productivity progressively decrease
As professional craftsmen, there are 5 rules we have to remember.
- Sort (good naming)
- Systematize (code in the right place you expect)
- Shine (clean, no unnecessary comment and code should be self-explained)
- Standardization (Group standard)
- Self-discipline (Practice and willingness to change)
How does clean code look like?
Clean code does one thing well, no matter is a function / class / module, it should be very straightforward and should hide the unrelated confusing logic using submethod etc
Clean code can be read like well-written prose
We can also use tiny abstraction to hide the unnecessary duplications so that the code can be easier to be maintained.
Good programmer makes the language looks simple
Good naming is a key skill of a software craftsman, it makes your code cleaner and self-explained.
- Avoid disinformation or pun
Choose one name per concept, for example, we don’t want to see accountDetails and accountData in the same place as they have the same semantic meaning.
For example, I like to use the get prefix for a getter name most of the time because I think when readers look into the code, how to get something might not be the biggest concern, so only using get lets them know it is getting the data. If they want to know more about that, they can browse the method easily.
But for the method that mutates data, it is more important to let readers know what’s happening so that they can reuse that. So we need to use a different prefix for the naming when the semantics are different and readers need to know the difference. (push, append etc.)
2. Use Pronounceable name
A name that can be easily pronounced can make the communication between software engineers even better.
3. Use Searchable name
Avoid using a name like ‘x’, ‘y’, because when you try the search for it in IDE, it will generate tons of results as each word can contain one of these letters. So choose a meaningful name that can be searched.
4. Avoid confusing naming if used individually
Using class, or object we can better encapsulate the related logic and variables and the class/object prefix makes it easier to understand the context of the variable.
For example, we have some variables under the context of location, such as name, street, city, state. They might look okay when we put them together, but if we try to use name or state individually in a function or pass it as a prop, it will be very confusing since we won’t be able to clearly know that they are referring to a location. So we can utilize class/object to group them can prefix them as a good indicator.
We talked about how to do naming to allow readers to read the code without pressure. It’s time to look into doing how we can combine good naming and practice to create a clean and readable function.
We oven put too much unimportant logic into a function which can confuse the readers, and to make it as clean as a poem, we can split the logic into multiple small subfunctions, this way the readers can know whats going on with the function easily by just looking at the subfunctions’ name.
2. Blocks and Indenting
Using blocks like if, else, while etc. with tons of content inside the block is extremely confusing, we should try to make use of subfunctions with descriptive names and make the block one line.
Also, we should avoid making the indent level greater than 2 as the complexity starts to increase with nested content.
3. Only do one thing
Putting too many responsibilities into one function not only violates the SRP (Single Responsibility Principle), it also makes it harder to read and to be reused. So we should make sure the function is not too tedious and it only does one job. You know you are doing more than one job in the function when you try to use “and ”as the function name.
(But in some case we might want to explicitly name the function with “and ” and to do more than one job, it has the tradeoff)
4. Avoid “Side Effect” and mutation
A function should not do hidden work behind the scene, meaning that it should not do anything that is not described in the function name since it is confusing and it is where the bug comes from. If we have to do more than one thing in the function, we should still specify it in the function name.
Moreover, we should try to avoid mutation as this is very implicit and hard to be conceived. Trying to make the function pure and create fewer bugs, if we need to mutate something, we should only scope its mutation in the same object/class using this keyword.
5. Avoid flag argument
Using boolean (flag) as an argument means that the function will do two jobs at least because of the if else statement. But if the use case really requires us to do more than one job, make it explicit in the function name.
6. Favour less argument
Less argument is better than more arguments, because of the order of argument can be confused, so we should always try to minize the argument required by a function by grouping argument into a class.
For example, we have a function called getAddress(lat, lng), we can declare a Point class that consists of lat and lng and finally minimize the arguments into getAddress(Point xx).
7. Open Closed Principle
Open for extension closed for modification, we don’t want to see a function keep being expanded and finally lead to multiple functions need to be modified.
Using the switch statement as an example, if we have a function called calculatePay and it has a switch statement that chooses the calculation function to use base on a type (loan etc.). Imagine we can have removePay in the future and it will also need a switch, which is not ideal and not maintainable.
We should focus on the type of payment (say loan etc.) instead of the actions (calculate and remove etc.) and make different classes to represent the payment, in each payment classes we can encapsulate different actions related to that payment.
Then create a function to choose the right payment class to use base on the type argument, and this way we are closed for modification and group the actions in a fine place.
(This OCP section will be modified as I learn more)