"Any fool can write code that a computer can understand. Good programmers write code that humans can understand." — Martin Fowler.
I have seen a lot of experienced developers writing code that is hard to read and not enjoyable to work with. I am sure you have worked with such code.
I am passionate about writing code that is easy to read and maintain.
In this blog post, I want to share with you some simple and easy tips — I use in everyday life to write a much better code.
Tip 1: Naming
When naming your variables, methods, and classes, ensure you give them clear and meaningful names. Good naming conventions enhance code readability and help others (and your future self) understand the intent of your code without needing additional context.
Let's explore a few examples of bad naming that lead to confusion for those who are reading the code:
csharppublic class AccountService { private string _str; private DateTime _dateTime; private decimal _iNumber; public bool HasNoTime() { return _dateTime < DateTime.Now; } public bool RemoveAmount(decimal amount) { _iNumber -= amount; return _iNumber >= 0; } }
Do you really understand the purpose of this class, its methods and private fields? This is a class that operates with a BankAccount, checks if an account is expired and performs withdrawal.
If you write such code, you will understand its intent, but what if someone else reads it? Or you will read this code a few weeks later?
Let's rewrite this code to make it more obvious:
csharppublic class BankAccount { private string _ownerName; private DateTime _expirationDate; private decimal _balance; public bool IsExpired() { return _expirationDate < DateTime.Now; } public bool Wihdraw(decimal amount) { if (_balance >= amount) { _balance -= amount; return true; } return false; } }
Now it's clear that we're working with BankAccount, its owner, expiration date and balance.
Best Practices:
- Classes: Use nouns that describe the object's purpose (e.g., UserRepository, EmailService).
- Methods: Use verbs or verb phrases (e.g., GetUserById, SendEmail).
- Variables: Choose descriptive names that reflect their role (e.g., totalPrice, isActive).
Tip 2: Remove Redundant Comments in The Code
Often poor naming leads to comments in the code explaining the intent. This is a bad practice.
Such comments clutter your code, make it less readable and can become outdated. Your code is your source of truth. Comments should explain the WHY, not the WHAT.
Comments in the code should not duplicate what the code can already tell you. So instead of this:
csharp// Calculate the price for the product var price = product.Price - Math.Max(product.Price * product.Discount / 100, MaxDiscount);
It's better to replace comment with a method that clearly indicates the code intent:
csharpvar price = GetDiscountedPrice(product); private decimal GetDiscountedPrice(Product product) { var discountValue = Math.Max(product.Price * product.Discount / 100, MaxDiscount); return product.Price - discountValue; }
Best Practices:
- Remove comments that describe what the code does.
- Remove historical comments or commented code - GIT remembers everything
- Use comments to explain complex logic or reasons behind decisions.
- Use comments to write code summary for your public contracts (classes, methods, models)
- Ensure comments stay updated with code changes.
Tip 3: Format Code with Indentations and Whitespaces
Proper code formatting enhances readability. Consistent indentation and spacing make it easier to follow the code's structure.
This code really reads hardly:
csharppublic class Calculator{ public int Add(int a,int b){return a+b;} }
Well formated code is way more readable:
csharppublic class Calculator { public int Add(int a, int b) { return a + b; } }
Best practices:
- Use consistent indentation (e.g., tabs or 4 spaces).
- Place braces on new lines consistently.
- Add spaces around operators and after commas.
- Use empty lines to separate logical blocks of code.
- Use empty lines to separate public from private fields in your classes.
Tip 4: Reduce Nesting
Deeply nested code is hard to read and maintain:
csharpif (user is not null) { if (user.IsActive) { if (user.HasPermission) { foreach (var preference in user.Preferences) { // Perform action } } } }
The recommended practice is try to use not more than 2 levels of nesting. Reducing nesting improves code readability:
csharpif (user is null || !user.IsActive || !user.HasPermission) { return; } foreach (var preference in user.Preferences) { // Perform action }
Best practices:
- Use guard clauses to handle edge cases upfront.
- Avoid nesting by returning early.
- Consider refactoring complex nested logic into separate methods.
Tip 5: Return Early
When conditions aren't met - return early from the method and prevent unnecessary code execution. As we have seen in the previous tip, returning early from the method reduces nesting, and as a result, it improves code readability.
Instead of this code:
csharpif (user is not null) { if (user.IsActive) { if (user.HasPermission) { foreach (var preference in user.Preferences) { // Perform action } } } }
You can use return early principle:
csharpif (user is null) { return; } if (!user.IsActive) { return; } if (!user.HasPermission) { return; } foreach (var preference in user.Preferences) { // Perform action }
This allows reading code from top to bottom line-by-line without a need to scroll up and down to see the full context.
You can also merge some if statements if they belong together:
csharpif (user is null || !user.IsActive || !user.HasPermission) { return; }
You can improve the readability of this code further by extracting a complex if statement to a separate private method:
csharpif (!IsUserAllowedToUpdatePreferences(user)) { return; } foreach (var preference in user.Preferences) { // Perform action } private bool IsUserAllowedToUpdatePreferences(User user) { return user is not null && user.IsActive && user.HasPermission; }
Best practices:
- Check for invalid conditions at the beginning of methods.
- Use return, continue, or break statements to exit early.
- Improves code flow and readability.
Tip 6: Get Rid of Else Keyword
Else keyword in most cases reduces code readability. Let'e explore this example of code:
csharpvar shipmentAlreadyExists = await dbContext.Shipments .AnyAsync(x => x.OrderId == request.OrderId, cancellationToken); if (shipmentAlreadyExists) { logger.LogInformation("Shipment for order '{OrderId}' is already created", request.OrderId); } else { var shipment = request.MapToShipment(shipmentNumber); await dbContext.AddAsync(shipment, cancellationToken); await dbContext.SaveChangesAsync(cancellationToken); } return shipmentAlreadyExists ? Results.Conflict(...) : Results.Ok(response);
Often when reading code in the else statement, you need to scroll up to see the corresponding if. This is a small example, but imagine a much bigger code base.
After you add an early return, an else block becomes unnecessary:
csharpvar shipmentAlreadyExists = await dbContext.Shipments .AnyAsync(x => x.OrderId == request.OrderId, cancellationToken); if (shipmentAlreadyExists) { logger.LogInformation("Shipment for order '{OrderId}' is already created", request.OrderId); return Results.Conflict(...); } var shipment = request.MapToShipment(shipmentNumber); await dbContext.AddAsync(shipment, cancellationToken); await dbContext.SaveChangesAsync(cancellationToken); return Results.Ok(response);
This code reads more easily, as you read it line-by-line, without a need to scroll up and down.
Best practices:
- Simplify code by eliminating unnecessary else blocks.
- Improves the linear flow of the code.
Tip 7: Avoid double negatives
When writing if statements - avoid using double negatives as they are really confusing.
For example:
csharpif (!user.IsNotActive) { // User is active }
This is really confusing.
And this reads much better:
csharpif (user.IsActive) { // User is active } if (!user.IsActive) { // User is NOT active }
Name all your boolean variables, properties and methods from the positive side, answering what is happening or what happened instead of what didn't happen.
For example, bad naming:
- user.IsNotActive
- user.HasNoDept
- creditCard.IsNotExpired
This is a better naming:
- user.IsActive
- user.HasDept
- creditCard.IsExpired
Tip 8: Avoid Magic Numbers and Strings
Magic numbers and strings are hard-coded values with no explanation. Replace them with named constants or enums.
csharpvar discountValue = Math.Max(product.Discount, 100); if (order.Status == 3) { // Process order } if (user.MembershipLevel == "Silver") { // Process user order }
csharpconst MaxDiscount = 100; const SilverMembershipLevel = "Silver"; public enum OrderStatus { Pending = 1, Processing = 2, Completed = 3 }
With constants and enums, code becomes more readable and more maintainable:
csharpvar discountValue = Math.Max(product.Discount, MaxDiscount); if (order.Status is OrderStatus.Completed) { // Process order } if (user.MembershipLevel == SilverMembershipLevel) { // Process user order }
Whenever you need to increase the maximum amount of discount, rename the membership level or add new order statuses — you can do it in a single place.
Tip 9: Control Number of Method Parameters
Methods with many parameters are hard to read and use. Limit the number of parameters, ideally, to three or fewer.
csharppublic void CreateUser(string firstName, string lastName, int age, string email, string phoneNumber) { // Create user }
It's much better to group related method parameters into a separate class:
csharppublic class CreateUserRequest { public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } public string Email { get; set; } public string PhoneNumber { get; set; } } public void CreateUser(CreateUserRequest request) { // Create user }
It will be much easier to add new properties to the class without changing the method's signature.
Tip 10: Applying the Single Responsibility Principle
A class or method should have one, and only one, reason to change. This principle simplifies maintenance and enhances code clarity.
Instead of one monstrous class and method:
csharppublic class ReportService { public void GenerateReport() { // Fetch data // Analyze data // Generate report // Send report via email } }
Break it down to multiple methods or classes:
csharppublic class DataFetcher { } public class DataAnalyzer { } public class ReportGenerator { } public class EmailService { }
Let's explore an example where a method is doing multiple things:
csharppublic Task Handle(User user) { await SaveUserAndSendEmailAsync(user); } private Task SaveUserAndSendEmailAsync(User user) { } private Task ValidateAndUpateUserAsync(User user) { }
For example, here SaveUserAndSendEmailAsync
is doing 2 things: saving user in the database and sending email afterward.
I also like breaking down such methods into separate single responsible methods:
csharppublic Task<Result> Handle(User user) { if (!IsValid(user)) { return Result.BadRequest(...); } await SaveAsync(user); await SendEmailAsync(user); } private Task SaveAsync(User user) { } private Task SendEmailAsync(User user) { } private Task<bool> IsValid(User user) { }
This way, code is much more maintainable and more readable.
Tip 11: Correctly Use Braces
Always use braces { }
with conditional statements, even if they're one-liners.
This prevents errors when adding new lines and makes your code more readable and predictable.
csharpif (isValid) ProcessData(); foreach (var preference in user.Preferences) AddToReport(preference); SaveReport();
This code hardly reads, right?
Add braces and newlines, and it will be much easier to read it:
csharpif (isValid) { ProcessData(); } foreach (var preference in user.Preferences) { AddToReport(preference); } SaveReport();
Best practices:
- Consistent use of braces improves readability.
- Reduces the risk of bugs from unintended code execution.
Tip 12: Do Not Return Null for Collections
Returning null collections can lead to NullReferenceException
or to too many if checks.
csharppublic List<Item>? GetOrderItems() { if (noItemsFound) { return null; } return items; } public void ProcessOrder(...) { var items = GetOrderItems(); // NullReferenceException is thrown here var count = items.Count; foreach (var item in items) { // Process items } } public void ProcessOrder(...) { var items = GetOrderItems(); if (items is null) { return; } var count = items.Count; foreach (var item in items) { // Process items } }
Instead, return an empty collection.
csharppublic List<Item> GetOrderItems() { if (noItemsFound) { return new List<Item>(); } return items; } public void ProcessOrder(...) { var items = GetOrderItems(); // No need for any additional checks var count = items.Count; foreach (var item in items) { // Process items } }
By returning an empty collection, you make your code safer and more readable.
In C# 12 you can simplify returning of an empty collection to collection expressions by using [ ]
syntax:
csharppublic List<Item> GetOrderItems() { if (noItemsFound) { return []; } return items; }
Summary
These are small tips that can make your code much better. Remember that consistency across your solution is very important. You should use the same naming conventions, code formatting across the whole solution for consistency.
Whenever you write some code: add new features, update existing or fix bugs — take a minute and improve the code you touch. This is called a Boy Scout Rule — leave your code better than you found it. By applying this rule, you will improve code quality and perform code refactoring without a significant effort and time.
If you want to further improve your code, I recommend adding static code analysis.
Hope you find this blog post useful. Happy coding!