4R Digital Help

Coding Best Practices

Key Principles

SOLID Principles

  1. Single Responsibility
    Every scope (Project, Class, or Method) should have only one responsibility, ensuring that it has only one reason to change.

  2. Open/Closed
    Classes should be open for extension, typically through composition, and closed for modification (except for bug fixes).

  3. Liskov Substitution
    Classes should adhere to polymorphism and be substitutable for their base abstractions.

  4. Interface Segregation
    Use specific interfaces that, when combined, compose the complete contract of an object, instead of interfaces with multiple contracts.

  5. Dependency Inversion
    Dependencies should be inverted, meaning a class should not depend directly on others. Instead, it should depend on abstractions (usually interfaces) provided through injection, ensuring decoupling.

DRY: Don't Repeat Yourself

Except in very specific cases, no code logic should be repeated, especially containing business rules. Code repetition is a strong indicator of poor engineering and should be refactored to allow for reuse.

YAGNI: You Ain't Gonna Need It

Do not implement anything that falls under "might need it later." If you don't need it now, don't do it. Cases where implementing something for the future makes sense are rare and depend on the certainty that the need will arise. Such cases are justifiable only when the cost of implementing it in the future will be much higher, assessed through experience.

KISS: Keep It Simple Stupid

Often, we want to implement fancy and intricate things, but they end up being over-engineered. Ideally, we should always opt for the simplest implementation, as long as it is correct, making future changes and maintenance easier.

Tell, Don't Ask

When developing in an object-oriented manner, operations should always be kept close to their data. Instead of collecting data and processing it, we should instruct the data owner to do so.

Fail Fast

Fail Fast by using exceptions and Defensive Programming (below) as opposed to failing slowly, debugging the application to figure out what, when, why and how something went wrong.

Best Practices

Defensive Programming

Defensive programming involves ensuring that subsequent code can be executed without unexpected results, such as exceptions or inappropriate values. Validating parameters and returns from other contexts, regardless of knowledge and/or confidence that they will not cause problems, ensures that the current context will execute perfectly and as expected.

There are basically two types of defenses:

  1. Parameter Validation: In parameter validation, each parameter should be validated against all possible adverse scenarios for the context, especially null values or references.


    This validation is not necessary for private methods or constructors.

Bad:

public class CustomerController { private readonly ICustomerService _service; public CustomerController(ICustomerService service) { this._service = service; } public Customer GetById(string customerId) { return this.service.Find(customerId); } }

Good (Without Guard library):

public class CustomerController { private readonly ICustomerService _service; public CustomerController(ICustomerService service) { // Defends against injection failure for any reason. this._service = service ?? throw new ArgumentNullException(nameof(service)); } public Customer GetById(string customerId) { // Defends against invalid parameter that may cause runtime error. if (string.IsNullOrWhiteSpace(customerId)) { throw new ArgumentNullException(nameof(service)); } return this.service.Find(customerId); } }

Good (With Guard library):

public class CustomerController { private readonly ICustomerService _service; public CustomerController(ICustomerService service) { // Defends against injection failure for any reason. this._service = Guard.Against.Null(service); } public Customer GetById(string customerId) { // Defends against invalid parameter that may cause runtime error. Guard.Against.NullOrWhiteSpace(customerId); return this.service.Find(customerId); } }
  1. Result Validation: Result validation involves ensuring that results of external operations will not cause problems in the continuation of the context. Currently, it is recommended only in libs, like B.Nuget.

Bad:

public Customer GetById(string customerId) { var customer = this.service.Find(customerId); customer.Code = 12345; return customer; }

Good (Without Guard):

public Customer GetById(string customerId) { var customer = this.service.Find(customerId); if (customer is null) { var message = $"Unable to get the customer by id {customerId}"; throw new InvalidOperationException(message); } customer.Code = 12345; return customer; }

Good (With Guard):

public Customer GetById(string customerId) { var customer = this.service.Find(customerId); Guard.Ensure.NotNull(customer, $"Unable to get the customer by id {customerId}"); customer.Code = 12345; return customer; }

Code Flow and Early Exit

It is recommended to use a code flow pattern in methods that facilitate readability, maintenance, and prevent other issues. With this flow, validations that interrupt the method flow can be performed, avoiding the need for ifs or other nested structures.

This flow consists of three parts: Validation, Body, and Result.

Example (Without Guard):

public Customer Get(string id) { // Validation: Validates contracts that guarantee the perfect execution of the Body. if (string.IsNullOrWhiteSpace(id)) { throw new ArgumentNullException(nameof(id), "Parameter cannot have null or empty value"); } if (id.Length > 5) { throw new ArgumentOutOfRangeException(nameof(id), "Invalid parameter format."); } // Body: Operations that lead to the result. var customer = this._customerRepository.Get(id); if (customer is null) { throw new InvalidOperationException("Unable to find a customer by id."); } customer.Code = 12345; // Result: Return result, if any, and output values. return customer; }

Example (With Guard):

public Customer UpdateCustomerCode(string id) { // Validation: Validates contracts that guarantee the perfect execution of the Body. Guard.Against.Null(customerId, "Parameter cannot be null"); Guard.Against.NullOrEmpty(customerId, "Parameter cannot be empty"); Guard.Against.NullOrWhiteSpace(customerId, "Invalid parameter format."); Guard.Against.NegativeOrZero(customerId, "Id must not be zero or negative."); // Body: Operations that lead to the result. var customer = this._customerRepository.Get(id); Guard.Ensure.NotNull(client, "Unable to find a customer by id."); customer.Code = 12345; // Result: Return result, if any, and output values. return customer; }

Avoid Redundancies in Code

Redundant code is, as the name suggests, useless. Classic examples include try/catch with an empty catch clause "just to avoid throwing an exception" or if(true) { // Code }.

Don't Have Business Rules in Extension Methods

Extension methods are very useful but detached from the code structure; therefore, they should not contain any business rules. However, they are excellent as helpers.

Test Before Refactoring

It is an excellent practice to have the code covered by tests, especially before refactoring. The chance of breaking the logic is high.

Whenever You Touch Code, Improve It and Its Surroundings

A.k.a Leave it in a better state than when you found it.

It is important to aim for perfection, even if it is impossible. Whenever we are going to change some code, it is recommended to improve that code and its surroundings. This way, we are constantly improving the source code gradually. The scope of changes should be restricted to the lines or methods immediately connected in the same scope, or else we end up redoing the entire system.

Avoid Switches

Switches represent decision points with 3 or more options. Besides being highly complex, they also implicitly violate the SRP. It is recommended to avoid switches through the application of design patterns, such as Strategy. As a temporary solution, the switch should reside alone in a decision-making method, as centralized as possible for reuse.

Never Commit Commented Code

Commented code should not be committed simply because version control (git) already serves this purpose.

TODO Comments Must Be Resolved Before PR Merge

It is important to use them to help organize tasks; however, they must be resolved before merging the PR. Keeping them in the master branch doesn't make sense as they will lose their meaning.

Avoid Using Magic Numbers or Strings

Numbers and strings should be declared in variables with descriptive names for their respective functions. Using them directly in the code makes it difficult or impossible for third parties to understand these values.

Avoid Nested Ifs

Nested ifs increase code complexity. As a general rule, it should be limited to only 1 level. If you see the need for more levels, refactor to separate methods or even consider design patterns that solve the problem.

Prefer Method Calls Within Decision or Loop Scopes

Decision (if, switch) and loop (while, do, for, foreach) calls already have their own complexity, so we should reduce that of their scopes. Ideally, we should have the minimum inside their scopes, ideally only calls to methods that perform the operations.

Bad:

public IEnumerable<decimal> CalculateCredit(string[] clientIds) { foreach (var clientId in clientIds) { var client = this._clientService.Get(clientId); var salary = this._salaryService.Get(clientId); if (client.Age >= 18) { var credit = salary.MonthlyValue * 0.3; yield return credit; } } }

Good:

public IEnumerable<decimal> CalculateClientCredit(string[] clientIds) { foreach (var clientId in clientIds) { yield return CalculateCredit(clientId); } } private decimal CalculateCredit(string clientId) { const decimal noCredit = 0; var client = this._clientService.Get(clientId); var salary = this._salaryService.Get(clientId); return client.Age >= 18 ? CalculateCredit(salary.MonthlyValue) : noCredit; } private decimal CalculateCredit(decimal monthlyValue) => salary.MonthlyValue * 0.3;

Use Throw Instead of Throw ex

Throw ex will replace the stack trace, causing the loss of the original exception information. For all cases, Throw is the best option to continue error handling further down the stack.

Prefer Equality Validations with is Whenever Possible

Equality comparators like == and .Equals can be overridden, so when validating, we can have unexpected results. To ensure the explicitness of the written code and avoid surprises, use is whenever possible.

Example:

public void Add(User user) { if (user is null) { throw new ArgumentNullException(nameof(user)); } this._context.Add(user); }

is should also be used when validating types for the same reason.

Example:

public static bool ValidateType<T>(object obj) => obj is T;

Class Size

The size of a class is usually subjective because it depends a lot on what is implemented. Here are some references that may indicate that this class is too large and/or violates some principle:

  1. More than 5 injections
    Most classes that adhere to best practices and principles typically will not need more than 5 dependencies. The number of dependencies is a strong indicator that the class violates the SRP and possibly the OCP. In these cases, it is necessary to identify the best way to separate responsibilities so that all new classes adhere to the standards.

  2. More than 200 lines
    The number of lines is very subjective but can help identify a possible violation of SRP. It is necessary to evaluate whether all the code in the class serves the same responsibility. For example, in the case of a repository, the responsibility is to facilitate reading and writing data through an API. Depending on the number of available APIs, it is possible to exceed 200 lines without violating principles.

  3. Regions
    When thinking about using regions, the reasons for doing so already indicate a violation of some principle, in addition to SRP. We recommend never using regions, but if you think you should or need to, it is already certain that the class needs to be refactored.

Use appropriate Data Types

TL;DR:

Value Types

Struct

Record Struct

Reference Types

Record Class

Class

Last modified: 03 June 2024