This is the third post in the
Poka-yoke design series - also known as encapsulation.
Automatic properties are one of the most redundant features in C #. I know that many people love them very much, but they solve a problem you don’t have to face.
I absolutely agree that the code that looks like this is completely redundant:
private string name; public string Name { get { return this.name; } set { this.name = value; } }
However, the solution is not to rewrite this code as follows:
public string Name { get; set; }
The problem with the code in the first example is not its ceremoniality. The problem is that it breaks encapsulation. In fact
“[...] getters and setters do not provide encapsulation or information hiding: they are a way of violating these principles, which is legalized by the programming language.”
James O. Coplien & Gertrud Bjørnvig. Lean Architecture. Wiley. 2010. p. 134.
While I personally believe that automatic properties have their uses, I very rarely find them. They are never suitable for reference types and are only rarely applicable in value types.
Code smell: automatic property of reference type
First of all, let's consider a very large set of properties that are disclosed by the reference type.
In the case of reference types, the possible value is null. However, considering the Poka-yoke design, null is never a suitable value, since it leads to a NullReferenceExceptions.
The Null Object pattern is the best alternative to cope with situations where the value may be undefined.
In other words, automatic properties like the Name property are never appropriate. The setter must have some kind of protective expression in order to protect itself against the null value (and, possibly, against other non-valid values). Here is a solid example:
private string name; public string Name { get { return this.name; } set { if (value == null) { throw new ArgumentNullException("value"); } this.name = value; } }
Alternatively, the guard expression can also check for null and provide a default value:
private string name; public string Name { get { return this.name; } set { if (value == null) { this.name = ""; return; } this.name = value; } }
However, this implementation contains a
POLA violation, because the getter sometimes returns values ​​other than the assigned ones. You can correct this problem by adding an associated field of type Boolean, which indicates whether the name was assigned null, so that null could be returned from the setter in this exceptional case, but this leads to another “smell” in the code.
')
Code smell: automatic properties of value types
If the property type is a value type, the case becomes less clear, since the values ​​cannot accept null. This means that nods never fit. However, straightforward consumption of a value type may also not be appropriate. In essence,
it is only permissible for the class to intelligently accept and process any values ​​of this type.
If, for example, a class can actually work only with a certain subset of all possible values, a protective expression can be entered. Consider an example:
public int RetryCount { get; set; }
This property could be used to set the desired number of attempts to perform a given operation. The problem is that the automatic property allows you to set a negative value, which is meaningless. One possible solution is to add a protective expression:
private int retryCount; public int RetryCount { get { return this.retryCount; } set { if (value < 0) { throw new ArgumentOutOfRangeException(); } this.retryCount = value; } }
However, in many cases, the disclosure of a property of a primitive type is most likely a case of “obsession with primitives”.
Revised design: protective expression
As I described earlier, the fastest way to fix the problem with an automatic property is to implement a property with a protective expression. This ensures that class invariants are properly encapsulated.
Revised design: value type property
When an automatic property is a value type, the implementation of a protective expression can still make sense. However, when a property is a real symptom of “primitive obsession,” the best alternative is to introduce the correct value object.
Consider, as an example, the following property:
public int Temperature { get; set; }
This is a bad design for several reasons. It is not related to the meaning of the unit of measurement and allows the assignment of unlimited values. What happens if it is assigned to -100? If the unit of measurement is Celsius, then everything is fine, but if - Kelvin, then there is an error. Regardless of the unit of measurement, the attempt to assign an int.MinValue should fail.
A more sustainable design could be achieved by introducing a new type of Temperature and by changing the type of property to the one entered. In addition to protecting invariants, a new class could also encapsulate conversion between different temperature measures.
However, if the value object is implemented as a reference type, the situation becomes equivalent to the situation described above and a null check is necessary. The use of automatic properties is appropriate if the value object is implemented as a value type.
Conclusion: automatic properties are rarely relevant. In fact, they are valid only when the property type is a value type and all probable values ​​are valid. Since there are several cases in which automatic properties are appropriate, their use cannot be completely excluded, but the need to use them should be considered as a reason for further investigation. The use of automatic properties is a “smell” in the code, but not an anti-pattern.
It is worth noting that the properties may also violate the
law of Demeter , but this is already a topic for a future post.