🎆8 Tips To Write Clean Code
date
Oct 31, 2023
slug
8-tips-to-write-clean-code
status
Published
tags
Chia sáş»
Sưu tầm
Study
summary
Clean code is code that’s easy to read, maintain, and understand.
type
Post
Clean code is code that’s easy to read, maintain, and understand.
I consider writing clean code a skill.
And it’s a skill that you can learn and improve with deliberate practice.
My favorite approach for practicing clean coding is doing refactoring exercises.
So I prepared one for you today, and we’re going to improve it one step at a time by applying clean code principles.
Let’s dive in!
Starting Point
I like starting with a problem when trying to learn new concepts.
And the more illustrative the problem, the better.
So we’ll use some poorly written code as the starting point for our refactoring.
And in each step, I will highlight what the current issue is and how we will fix it.
Here’s what I see when I look at theÂ
Process
 method:- Deep nesting of code — 4 levels, to be precise
- Precondition checks are applied one after the other
- Throwing exceptions to represent a failure
How can we turn this into clean code?
public void Process(Order? order) { if (order != null) { if (order.IsVerified) { if (order.Items.Count > 0) { if (order.Items.Count > 15) { throw new Exception( "The order " + order.Id + " has too many items"); } if (order.Status != "ReadyToProcess") { throw new Exception( "The order " + order.Id + " isn't ready to process"); } order.IsProcessed = true; } } } }
#1: Early Return Principle
It should be painfully obvious by now that the initial version is deeply nested because of theÂ
if
 statements applying precondition checks.We’ll solve this using the early return principle, which states that we should return from a method as soon as the conditions for that have been met.
In the case of theÂ
Process
 method, this means moving from a deeply nested structure to a set of guard clauses.public void Process(Order? order) { if (order is null) { return; } if (!order.IsVerified) { return; } if (order.Items.Count == 0) { return; } if (order.Items.Count > 15) { throw new Exception( "The order " + order.Id + " has too many items"); } if (order.Status != "ReadyToProcess") { throw new Exception( "The order " + order.Id + " isn't ready to process"); } order.IsProcessed = true; }
#2: Merge If Statements To Improve Readability
The early return principle makes theÂ
Process
 method more readable.But there’s no need to have one guard clause after another.
So we can merge all of them into oneÂ
if
 statement.The behavior of theÂ
Process
 method remains unchanged, but we remove a lot of excess code.public void Process(Order? order) { if (order is null || !order.IsVerified || order.Items.Count == 0) { return; } if (order.Items.Count > 15) { throw new Exception( "The order " + order.Id + " has too many items"); } if (order.Status != "ReadyToProcess") { throw new Exception( "The order " + order.Id + " isn't ready to process"); } order.IsProcessed = true; }
#3: Use LINQ For More Concise Code
A quick improvement can be using LINQ to make the code more concise and expressive.
Instead of checking forÂ
Items.Count == 0
, I prefer using the LINQÂ Any
 method.You could argue that LINQ has worse performance, but I always optimize for readability.
There are far more expensive operations in an application than a simple method call.
public void Process(Order? order) { if (order is null || !order.IsVerified || !order.Items.Any()) { return; } if (order.Items.Count > 15) { throw new Exception( "The order " + order.Id + " has too many items"); } if (order.Status != "ReadyToProcess") { throw new Exception( "The order " + order.Id + " isn't ready to process"); } order.IsProcessed = true; }
#4: Replace Boolean Expression With Descriptive Method
Merging multiple conditions into oneÂ
if
 statement means writing less code, but it can decrease readability with complex conditions.However, you can fix this and improve readability by using a variable or method with a descriptive name.
I prefer using methods, so I will introduce theÂ
IsProcessable
 method to represent the precondition check.public void Process(Order? order) { if (!IsProcessable(order)) { return; } if (order.Items.Count > 15) { throw new Exception( "The order " + order.Id + " has too many items"); } if (order.Status != "ReadyToProcess") { throw new Exception( "The order " + order.Id + " isn't ready to process"); } order.IsProcessed = true; } static bool IsProcessable(Order? order) { return order is not null && order.IsVerified && order.Items.Any(); }
#5: Prefer Throwing Custom Exceptions
Now let’s talk about throwing exceptions. I like to use exceptions for “exceptional” situations only, and I don’t use them for flow control in my code.
Having said that, if you do want to use exceptions for flow control, it’s better to use custom exceptions.
You can introduce valuable contextual information and better describe the reason for throwing the exception.
And if you want to handle these exceptions globally, you can create a base class to be able to catch specific exceptions.
public void Process(Order? order) { if (!IsProcessable(order)) { return; } if (order.Items.Count > 15) { throw new TooManyLineItemsException(order.Id); } if (order.Status != "ReadyToProcess") { throw new NotReadyForProcessingException(order.Id); } order.IsProcessed = true; } static bool IsProcessable(Order? order) { return order is not null && order.IsVerified && order.Items.Any(); }
#6: Fix Magic Numbers With Constants
A common code smell I see is the use of magic numbers.
They are usually easy to spot because they’re used to check if numeric some condition applies.
The problem with magic numbers is that they carry no meaning.
The code is harder to reason about, and more error-prone.
Fixing magic numbers should be straightforward, and one solution is introducing a constant.
const int MaxNumberOfLineItems = 15; public void Process(Order? order) { if (!IsProcessable(order)) { return; } if (order.Items.Count > MaxNumberOfLineItems) { throw new TooManyLineItemsException(order.Id); } if (order.Status != "ReadyToProcess") { throw new NotReadyForProcessingException(order.Id); } order.IsProcessed = true; } static bool IsProcessable(Order? order) { return order is not null && order.IsVerified && order.Items.Any(); }
#7: Fix Magic Strings With Enums
Similar to magic numbers, we have the magic strings code smell.
A typical use case for magic strings is to represent some sort of state.
You’ll notice that we’re comparing theÂ
Order.Status
 value to a magic string to check if the order is ready to process.A few problems with magic strings:
- Easy to make mistakes (typo)
- Lack of strong typing
- Not refactoring proof
Let’s create anÂ
OrderStatus
 enum
 to represent the possible states:enum OrderStatus { Pending = 0, ReadyToProcess = 1, Processed = 2 }
And now we have to use the appropriateÂ
OrderStatus
 in the check:const int MaxNumberOfLineItems = 15; public void Process(Order? order) { if (!IsProcessable(order)) { return; } if (order.Items.Count > MaxNumberOfLineItems) { throw new TooManyLineItemsException(order.Id); } if (order.Status != OrderStatus.ReadyToProcess) { throw new NotReadyForProcessingException(order.Id); } order.IsProcessed = true; order.Status = OrderStatus.Processed; } static bool IsProcessable(Order? order) { return order is not null && order.IsVerified && order.Items.Any(); }
#8: Use The Result Object Pattern
I said I don’t prefer using exceptions for flow control. But how can we fix this?
One solution is using the result object pattern.
You can use a genericÂ
Result
 class to represent all types of results or a specific one like ProcessOrderResult
.To make your result objects encapsulated, expose a set of factory methods to create the concrete result type.
public class ProcessOrderResult { private ProcessOrderResult( ProcessOrderResultType type, long orderId, string message) { Type = type; OrderId = orderId; Message = message; } public ProcessOrderResultType Type { get; } public long OrderId { get; } public string? Message { get; } public static ProcessOrderResult NotProcessable() => new(ProcessOrderResultType.NotProcessable, default, "Not processable"); public static ProcessOrderResult TooManyLineItems(long oderId) => new(ProcessOrderResultType.TooManyLineItems, orderId, "Too many items"); public static ProcessOrderResult NotReadyForProcessing(long oderId) => new(ProcessOrderResultType.NotReadyForProcessing, oderId, "Not ready"); public static ProcessOrderResult Success(long oderId) => new(ProcessOrderResultType.Success, oderId, "Success"); }
Using anÂ
enum
 like ProcessOrderResultType
 will make consuming the result object easier with switch expressions. Here's the enum
 to represent the ProcessOrderResult.Type
:public enum ProcessOrderResultType { NotProcessable = 0, TooManyLineItems = 1, NotReadyForProcessing = 2, Success = 3 }
And now theÂ
Process
 method becomes:const int MaxNumberOfLineItems = 15; public ProcessOrderResult Process(Order? order) { if (!IsProcessable(order)) { return ProcessOrderResult.NotProcessable(); } if (order.Items.Count > MaxNumberOfLineItems) { return ProcessOrderResult.TooManyLineItems(order); } if (order.Status != OrderStatus.ReadyToProcess) { return ProcessOrderResult.NotReadyForProcessing(order); } order.IsProcessed = true; order.Status = OrderStatus.Processed; return ProcessOrderResult.Success(order); } static bool IsProcessable(Order? order) { return order is not null && order.IsVerified && order.Items.Any(); }
Here’s how using anÂ
enum
 for the ProcessOrderResult.Type
 allows you to write a switch expression:var result = Process(order); result.Type switch { ProcessOrderResultType.TooManyLineItems => Console.WriteLine($"Too many line items: {result.OrderId}"), ProcessOrderResultType.NotReadyForProcessing => Console.WriteLine($"Not ready for processing {result.OrderId}"), ProcessOrderResultType.Success => Console.WriteLine($"Processed successfully {result.OrderId}"), _ => Console.WriteLine("Failed to process: {OrderId}", result.OrderId), };
Takeaway
That’s it, 8 tips to write clean code:
Writing clean code is a matter of deliberate practice and experience.
Most people will read about clean coding principles, but few will strive to apply them daily.
This is where you can set yourself apart.
I also made a video about these clean code tips, you can watch it here.
Hope this was helpful.
See you next week!
Today’s action step: Take a look at your project and see if you’re making some of the mistakes I highlighted here. And then fix them using the clean code tips I shared with you.