This is the fifth post in the
Poka-yoke design series - also known as
encapsulation .
The default constructors are “smell” in the code. Exactly. This may sound outrageous.
, but we take into account the following: object orientation is the
encapsulation of behavior
and data into connected pieces of code (classes). Encapsulation means that a class must protect the integrity of the data it encapsulates. When data is necessary, it must be requested through the constructor. In contrast, the default constructor says that no external data
is required . This is a rather weak statement regarding class invariants.
Please note that this post describes the " smell ". This means that when a certain idiom or pattern (in this case, the default constructor) is found in the code, this should cause additional research.
As we will further note, there are several scenarios, when everything is fine with default constructors, so the goal of this post is not to destroy the default constructors. The goal is to provide food for thought.
If you read
my book , you already know that injection into the constructor is the dominant DI pattern precisely because it statically shows dependencies and protects the integrity of the class, ensuring that the initialized consumer of these dependencies is always in a consistent state. This is a fail-safe design, because the
compiler enhances this relationship by providing fast feedback .
This principle extends far beyond DI. In the
previous post, I described how a constructor with arguments statically opens the necessary arguments.
public class Fragrance : IFragrance { private readonly string name; public Fragrance(string name) { if (name == null) { throw new ArgumentNullException("name"); } this.name = name; } public string Spread() { return this.name; } }
The Fragrance class protects the integrity of the name by requiring it through the constructor. Since the class needs a name to implement its own behavior, it is good practice to request it through the constructor. The default constructor would not be fault tolerant, since it would introduce
temporal connectivity .
Bear in mind that objects must be containers of behavior and data. While objects contain data, they must be encapsulated. In the (very common) case, when it is impossible to define meaningful defaults, the data must be passed to the object through the constructor. Thus, the presence of default constructors may indicate a violation of encapsulation.
In what cases is the use of default constructors justified?
There are scenarios in which the use of default constructors is completely justified (I am sure that there are more such scenarios than those listed below):
- If the constructor can assign meaningful defaults to all field contents, it still protects its invariants. As an example, the default constructor of the UriBuilder class initializes its internal values to a consistent set that sets Uri to localhost until one or more of its properties are subsequently modified. You can agree or not with the default behavior, but it is consistent and provides encapsulation.
- If the class does not contain data, it is obvious that there is nothing to protect. However, this may be a symptom of “smell” “ envy of foreign members ”, which can usually be considered proven if the class in question is specific.
If such a class can easily be declared static, then this is a distinct sign of this “smell”.
If, on the other hand, a class implements an interface, this may be a sign that it represents pure behavior.
A class that represents pure behavior by implementing an interface is not necessarily a bad thing. This design can be very powerful.
In conclusion, the presence of a default constructor must be a signal in order to stop and think about the invariants of the class in question. Does the default constructor guarantee the integrity of the encapsulated data? If yes, then the default constructor fits; if not, it doesn't. In my experience, default constructors are the exception rather than the rule.