A class or interface should have a single purpose
A class is representing an abstraction of some business concept, a plain data structure or responsible for orchestrating the interaction between other classes. It should never be a combination of those. This rule is widely known as the Single Responsibility Principle, one of the SOLID principles.
Only “Construct” a usable object
There should be no need to set additional properties before the object can be used for whatever purpose it was designed. When creating a constructor, make sure that the resulting object can be used safely.
An interface should be small and focused
Don’t force implementations of methods that will not be used. This rule is more commonly known as the Interface Segregation Principle.
Tip: If you find yourself with methods that throw a NotImplementedException then the interface might be too complicated.
Reduce dependancies by using interfaces
It is very important to decouple classes as much as possible. There are times when classes need to be coupled. Generally speaking, try to avoid it. Using interfaces makes it easier to unit test.
Avoid Singleton Pattern or singletons in DI
Due to the nature of the Singleton Pattern and singletons in the DI framework, you should be very cautious when using them. If the application that you are designing is meant for high availability in a cloud environment, you should avoid it at all costs. If it is necessary use the cloud features for the singleton.
Don’t refer to derived classes from the base class
Creating a base class that references a derived class breaks proper object-oriented programming. If you find yourself doing this you might consider overriding in the derived class and substituting the base with the derived.
You should consider this as very strong advice against it.
Avoid bidirectional dependencies
This means that two classes know about each other’s public members or rely on each other’s internal behavior. Refactoring or replacing one of those two classes requires changes on both parties and may involve a lot of unexpected work. The most obvious way of breaking that dependency is introducing an interface for one of the classes and using Dependency Injection.
Exception: Domain models such as defined in Domain-Driven Design tend to occasionally involve bidirectional associations that model real-life associations. In those cases, I would make sure they are really necessary, but if they are, keep them in.
Classes should have state and behavior
Classes can represent many types of objects in C#. Generally speaking, when a class represents an abstract business concept (order, project) you will want to make sure that it has things that it can do (behavior or methods) and states that represent before or after a behavior.
Example: A class that contains a list of items would have a “count” state. It could also have an “Add” behavior that changes the count state.
Allow properties to be set in any order
Properties should be stateless with respect to other properties, i.e. there should not be a difference between first setting property DataSource and then DataMember or vice versa.
Don’t use mutual exclusive properties
Having properties that cannot be used at the same time typically signals a type that is representing two conflicting concepts. They should be separated into their own concerns.
A method or property should do only one thing
Similarly a method should have a single responsibility.
Don’t expose stateful objects through static members
A stateful object is an object that contains many properties and lots of behavior behind that. If you expose such an object through a static property or method of some other object, it will be very difficult to refactor or unit test a class that relies on such a stateful object. In general, introducing a construction like that is a great example of violating many of the guidelines of this chapter.
A classic example of this is the HttpContext.Current property, part of ASP.NET. Many see the HttpContext class as a source for a lot of ugly code. In fact, the testing guideline that Isolates the Ugly Stuff often refers to this class.
Return an IEnumerable<T>
or ICollection<T>
instead of a concrete collection class
In general, you don’t want callers to be able to change an internal collection, so don’t return arrays, lists or other collection classes directly. Instead, return an IEnumerable<T>
, or, if the caller must be able to determine the count, an ICollection<T>
.
Note: You can also use IReadOnlyCollection<T>
, IReadOnlyList<T>
or IReadOnlyDictionary<TKey, TValue>
.
Properties, methods, and arguments representing strings or collections should never be null
Returning null can be unexpected by the caller. Always return an empty collection or an empty string instead of a null reference. This also prevents cluttering your code base with additional checks for null, or even worse, string.IsNotNullOrEmpty()
.
Define parameters as specific as possible
If your member needs a specific piece of data, define parameters as specific as that and don’t take a container object instead. For instance, consider a method that needs a connection string that is exposed through some central IConfiguration
interface. Rather than taking a dependency on the entire configuration, just define a parameter for the connection string. This not only prevents unnecessary coupling, but it also improved maintainability in the long run.
Consider using Domain-specific value types rather than primitives
Instead of using strings, integers, and decimals for representing Domain-specific types such as an ISBN number, email address, or amount of money, consider created dedicated value objects that wrap both the data and the validation rules that apply to it. Doing this, you prevent ending up having multiple implementations of the same business rules, which both improve maintainability and prevent bugs.
Make Math.Min and Math.Max your friends
Code is like art in that it can be done in so many ways. In these cases, you should consider readability. One example is the use of Math.Min and Math.Max. These are used to determine the smallest (min) or largest (max) between two numbers. Experimental data shows that these are faster than inline “if” statements at large scales. However, the main benefit of using them regularly in the code is to make it more readable. When you use these, it makes the intent abundantly obvious.
Easy to read:
for (i = 1; i < n; i++) { if (arr[i] > max) { max = arr[i]; } if (arr[i] < min) { min = arr[i]; } }
Easier to read:
for (i = 1; i < n; i++) { max = Math.Max(max, arr[i]); min = Math.Min(min, arr[i]); }