Coding Best Practices
Key Principles
SOLID Principles
Single Responsibility
Every scope (Project, Class, or Method) should have only one responsibility, ensuring that it has only one reason to change.Open/Closed
Classes should be open for extension, typically through composition, and closed for modification (except for bug fixes).Liskov Substitution
Classes should adhere to polymorphism and be substitutable for their base abstractions.Interface Segregation
Use specific interfaces that, when combined, compose the complete contract of an object, instead of interfaces with multiple contracts.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:
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:
Good (Without Guard library):
Good (With Guard library):
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:
Good (Without Guard):
Good (With Guard):
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):
Example (With Guard):
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:
Good:
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:
is should also be used when validating types for the same reason.
Example:
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:
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.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.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