Scenario 3: PostconditionTest – Architectural Principles
This scenario explores that postconditions implemented in a supertype should yield the same outcome in its subtypes, but subtypes can be more strict about it, never less.The following code is the consumer of the subject under test:
var value = 5;
var result = sut.Do(value);
Console.WriteLine($”Do something with {result.Value}”);
The preceding code is very standard and very similar to the second scenario. We have the value variable that could come from anywhere. Then we pass it to the Do method. Finally, we do something with the result; in this case, we write a line to the console. The Do method returns an instance of a Model class, which has only a Value property. Here’s the code:
public record class Model(int Value);
The initial subject under test (the SuperClass) simulates that at some point during the execution, it returns a Model instance and sets the value of the Value property to the value of the value parameter. Here’s the code:
public class SuperClass
{
public virtual Model Do(int value)
{
return new(value);
}
}
Next, the SubClassOk class simulates that the execution changed and returns a SubModel instance instead. The SubModel class inherits the Model class and adds a DoCount property. When executing the code, everything is fine because the output is invariant (a SubModel is a Model and behaves the same). Here’s the code:
public class SubClassOk : SuperClass
{
private int _doCount = 0;
public override Model Do(int value)
{
var baseModel = base.Do(value);
return new SubModel(baseModel.Value, ++_doCount);
}
}
public record class SubModel(int Value, int DoCount) : Model(Value);
Finally, the SubClassBreak class simulates that the execution changed and returns null when the value of the value parameter is 5. When executing the code, it breaks at runtime with a NullReferenceException when accessing the Value property during the interpolation that happens in the Console.WriteLine call. Here’s the code:
public class SubClassBreak : SuperClass
{
public override Model Do(int value)
{
if (value == 5)
{
return null;
}
return base.Do(value);
}
}
This last scenario shows once again how a simple change can break our program. Of course, this is also an overly simplified example focusing only on the postcondition and history constraint, but the same applies to more complex scenarios.What about the history constraint? We added a new state element to the SubClassOk class by creating the _doCount field. Moreover, by adding the SubModel class, we added the DoCount property to the return type. That field and property were nonexistent in the supertype, and they did not alter its behaviors: LSP followed!