📜 ⬆️ ⬇️

The principle of substitution Liskov and contracts

The idea of ​​this note is inspired by the article by Alexander Bindu “ Addition to the LSP ” and can be considered as a detailed commentary on the article by Alexander.

So, the next question is, suppose, one of the team members is trying to implement the IList of T interface in the DoubleList of T class so that when adding an element using the Add method, not one but two elements will be added. Since the List of T class always adds only one element, we can assume that this behavior violates the Liskov Substitution Principle.

Now let's consider whether this is true when it comes to the .NET platform, well, let's talk about whether we can say that the method violates the Liskov substitution principle in the absence of a formal specification of what this method should do.
')


First you need to make a small amendment to the condition of the problem (yes, I understand that this is not fair, but in this case it is justified). The original task talked about the IList of T interface implementing the DoubleList class, but in fact the .NET Framework adds the Add method in the ICollection of T interface, which means it should be considered whether the DoubleList of T class violates the collection contract and not the list .

NOTE
For ease of reading the code and running the examples, I will not use a generic version of the DoubleList class, but a specialized container for storing rows, i.e. in fact, our DoubleList will implement the ICollection of string interface.

public class DoubleList : ICollection<string> { private readonly List<string> _backingList = new List<string>(); public void Add(string item) { //     ,   2  _backingList.Add(item); _backingList.Add(item); } //     } 


Now, suppose that somewhere in the code we use the ICollection of string interface (that is, we have polymorphic use) and expect that the number of elements will increase strictly by one. Such behavior can be expressed either by using a formal postcondition (for example, using the Code Contract), or it can be expressed as a unit test. As we move on to contracts later, let's consider the unit test first:

 [Test] public void TestAddMethodAddsOnlyOneElement() { ICollection<string> collection = new DoubleList(); int oldCount = collection.Count; collection.Add("foo"); Assert.That(collection.Count, Is.EqualTo(oldCount + 1)); } 


Yes, indeed, this test will work fine when used as a collection List of string object, and will fall if DoubleList is used.

But there is only one question: can we really expect such behavior from the Add method of the ICollection of T interface? When it comes to correctness (in other words, whether it is a bug or not, and it doesn’t matter where: in the code or design), it is determined only by whether the behavior of the code corresponds to the specification or not. The specification may be formal or non-formal, but the program is incorrect only when it stops doing what it is intended for, and if no one knows what it is intended for, then no one can say that it does not work correctly.

Some expression, such as x = y / 2 , is in itself neither correct nor incorrect. It all depends on what the relationship between x and y should be (for more on this, see “Contract Design. Software Correctness” ). The situation with the Add method is similar: without a formal or at least informal description of what this method should do, we cannot say whether it is implemented correctly by the heir. If we open the official documentation for the ICollection (Of T) .Add Method , then all that we will see is one line in which it will be said that this method adds an element to the collection. At the same time, everything else (for example, whether it will be added exactly or in what quantity) is only our assumptions, and the only thing you can do is try to find out how other implementations of the ICollection of T interface behave.

Let's add the following test, but this time, we’ll check that adding two identical elements will increase the number of elements by 2. It’s clear that this is a more special case than adding one element, but, again, return to the documentation of the Add method, then there is not a word that we cannot add the same elements; the only limitation is that we do not add items to the collection for reading only. There are no other preconditions (in the official documentation, they are usually expressed as exceptions), and therefore no other checks are needed before calling the Add method:

 [TestCaseSource("GetAllCollections")] public void TestAddToCollectionTwiceAddsTwoElement(ICollection<string> collection) { int oldCount = collection.Count; if (collection.IsReadOnly) { Console.WriteLine("Current collection type ({0}) is readonly. Skipping it...", collection.GetType()); return; } collection.Add("foo"); collection.Add("foo"); Assert.That(collection.Count, Is.EqualTo(oldCount + 2)); } 


Once again I draw attention to the verification of the preconditions of the Add method, contracts are not a game with only one goal, therefore we must fulfill our part of the contract so that the called code executes its own. The test itself is parameterized (from NUnit), whose input values ​​will be taken from the GetAllCollections method. I will not bother with searching for all existing classes that implement the ICollection of T interface, but simply add some popular types of collections manually:

 private static ICollection<string>[] GetAllCollections() { return new ICollection<string>[] { new List<string>(), new HashSet<string>(), new Collection<string>(), new BindingList<string>(), new LinkedList<string>(), new ObservableCollection<string>(), new ReadOnlyCollection<string>(new List<string>()), new SortedSet<string>(), new string[]{}, }; } 


Now we run the above test and see that at least two types of collections add only one element, although we asked to add two. These are, of course, collection types that do not allow duplicate storage, such as HashSet of T and SortedSet of T.

Now let's return to the original question: does the implementation of the Add method of the DoubleList class violate the Liskov substitution principle? Answer: No, it does not break!

The Add method of the ICollection of T interface does not impose any restrictions on how many elements will be in the collection after the addition, which means that we cannot require a certain rule from all classes that implement this interface. In fact, the following precondition is more plausible (based on the behavior of collections implementing ICollection of T ): after calling the Add method, the next call to the Contains method should return true and the number of elements in the collection should not decrease (ie, newCount> = oldCount).

In this case, if in our test we replace the existing statement with:

 Assert.IsTrue(collection.Contains("foo")); 


then all tests will pass successfully.

Conclusion

We can safely say that DoubleList does not violate the principle of replacing Liskov, or it violates, along with some standard classes of collections from BCL. On the other hand, I agree that the design of the DoubleList class is “crooked”, but not because of a violation of a certain principle, which is practically impossible to understand the formulation in my right mind (*), I would rather appeal to the fact that such behavior is intuitively unclear and will certainly lead to accompanying problems.

It is not for nothing that Meyer, in his thick book (see additional references), assigns such an important role to the formal specification of programs with the help of statements (preconditions, postconditions and invariants). It is the absence of a formal description of what the method should do that makes it so difficult to write an heir that would work “correctly”, since no one can say what is “right”. The signature of the method (and a possible comment) is too informal to understand " will the behavior change if you use the heir instead of the base class ".
Next time we will look at how Code Contracts (the support of which is partially included in the mscorlib) can help in solving our problem, and on what kind of formal post-condition the Add method has.

-

(*) I will move away from the usual rule for forming a footnote directly in the text and will do a separate subsection on the definition of the Liskov replacement principle.

The principle of replacement Liskov. Definition

In the excellent book by James Coplien, “ Programming in C ++, ” there is an excellent, and completely incomprehensible definition of the Liskov principle of substitution:

... if for every object o1 of type S there exists an object o2 of type T such that for all programs P defined in context T, the behavior of P does not change when o1 is replaced by o2, then S is the base type for T.

In general, everything is fine, except for the fact that it is completely unclear what “ in the context of T ” means and it is not at all clear what it means “the behavior of P will not change ” when this behavior is not described anywhere.

Another popular, but no more understandable definition can be found in the book of Bob Martin :

It should be possible to substitute any of its subtypes instead of the base type .

This definition is simpler, but hardly clearer. In any object-oriented programming language, there is an implicit conversion from the heir to the base class (when accounting for the use of open inheritance), so this requirement should be attributed to the compiler or programming language, rather than to the classes defined by users. Martin himself in his book (as well as in the article The Liskov Substituion Principle ) talks about the importance of contracts, and in particular about the importance of preconditions and postconditions for specifying the behavior of virtual methods.

Contracts - this is the very possibility of specifying the behavior of P , o
which is stated in the original definition, and it is thanks to this specification that we will be able to guarantee (or at least understand) that the behavior of the heir corresponds to the behavior of the base class or interface.

Additional links


  1. Bertrand Meyer. Object-oriented design of software systems
  2. Robert K. Martin. Principles, patterns and techniques of agile development in C #
  3. Robert C. Martin The Liskov Substitution Principle
  4. Robert C. Martin Design Principles and Patterns
  5. Design by contract. About software correctness
  6. Design by contract. Inheritance

Source: https://habr.com/ru/post/139039/


All Articles