Pros and cons of securing an interface contract through a subclass

Asked

Viewed 101 times

8

This article presents an interface IList<T> containing the methods Add(T item) and Count(). The contract of that interface expects that when an item is added, the method Count() reflects the new number of items in the list. This can even be defined in the interface documentation (a post-condition of the contract).

However, the interface itself, in most languages, does not guarantee that this contract will be fulfilled.

He proposes to solve this by creating a subclass CollectionContract<T> implementing the logic of fulfilling the contract:

public abstract class CollectionContract<T> : IList<T> {
    public void Add(T item){
        AddCore(item);
        this.count++;
    }
    public int Count {
        get { return this.count; }
    }
    protected abstract void AddCore(T item);

    private int count;

    ...
}

One of the comments argues that such an approach could introduce a overhead unacceptable at execution time even when overhead is limited to builds of debug:

In computer Science and software Engineering, the Terms and conditions on a Contract are typically referred to as pre- and post-conditions. While Abstract classes could be used to validate that These conditions are satisfied through the use of the template-method Pattern, this approach can introduce Unacceptable run-time overhead Even when Limited to debug builds.

He also says that in languages without multiple inheritance, such as C# (and Java), this approach profoundly impacts class design. Moreover, the logic of fulfilling the contract is making assumptions that make the subclass rigid and inflexible (such as assuming that the counter must be stored in a private field, and that this field must be of the whole type).

Finally, it proposes that the fulfillment of the contract be guaranteed by means of unitary tests carried out on the classes that implement the interface (although this is not an ideal solution, since it is external to the contract and to the language).

My questions are:

  • What kind of situation does the commentator refer to when talking about overhead at runtime?

  • Such situations where the overhead occur are difficult or easy to predict? (for the purpose of helping to decide when it is feasible to adopt the solution of the article)

  • In fact, answer me directly: it is always incorrect to try to guarantee a contract in the manner of the article?

  • Even in languages that allow multiple inheritance?

(For anyone interested in the context of the subject, I came to this article on starting interfaces this one and passing by this other. And this one is also interesting).

1 answer

9


Ensuring that a contract is fulfilled is a desirable concept but not always feasible. Because first of all it is necessary express the terms of the contract in logic, then it is necessary prove that the desired logic has been implemented. And this "proof", when possible, causes at least a longer compilation time, at most requiring that checks be done constantly at runtime. And, of course, it increases the amount of code subject to bugs (because the contract statement itself is also code, and is also subject to errors).

In this example above there is not much overhead, because rarely would anyone implement a list without an auxiliary field to store the count. But who left the comment is right to say that it restricts the use of the interface:

  • If the implement wants to add other methods to change the list, but does not have [writing] access to the field count - since it is private - how to do? It would be necessary to call one of the methods of this abstract class, which in turn would call again a method of the concrete class, which would finally do the service. These redirects would bring a overhead unnecessary.

  • Another possibility would be not exist a method in the abstract class that does what one wants. For example, if the list implementation is a chained list (or even a tree), and you want a method that puts a new element in order, how to do this efficiently? If you could implement yourself - with a cursor - and at the end update the count manually, blz, but without that access you become dependent on having a method in the abstract class that meets your needs.

  • If you want a list thread-safe, will have to somehow make atomic the operation of "adds an element and increments the count". If the implementation that has already come ready is not atomic, this reduces your options to implement this atomicity efficiently.

  • Does the contract say that by adding an element the count necessarily has to increase? What if the element cannot be entered in the list for any reason (for example, if the list does not accept repeated elements, and what is being added is already in the list)? From what I read in the documentation of IList.Add, this is a possibility, and by trying to "impose" the contract in this way the implementation further restricts the types of lists that can be created, more than the contract requires.

These are just a few examples. As a rule, it is difficult to answer in the abstract whether "it is always incorrect" or not to do so, but my suspicion is that it is. Because if something is very well defined to the point of not giving room for variations, this should not even be a contract - but a concrete implementation. If the interface exists as something abstract, it is because it has been predicted that it may be necessary to implement it in radically different ways. Restricting the implementation puts more "barriers" that the programmer will have to overcome when trying to do something different than was originally predicted.

P.S. Some contracts can rather be guaranteed, and there is no harm in doing so (although it is not mandatory). A good example is the type system itself: static typing languages require the programmer to only combine variables, values, methods, classes, if they all have the correct "type", and the program does not compile if this cannot be verified and proved. Already those with dynamic typing do not do this during the compilation, but end up having right overhead at time of execution. This would be a counter-example, where the guarantee of contracts [in principle at least] reduces the overhead instead of increasing it.

  • One thing that occurred to me: when he talks about overhead in debug builds, he’s referring to assertions, like the ones in Java? Because otherwise I don’t understand how it can be proved the implementation of a contract at runtime in this type of language. But otherwise it all makes a lot of sense, if nobody has anything to add I’ll end up accepting the answer.

  • 1

    @Piovezan I don’t know exactly what he’s talking about, but I understood it in a similar way to you. There are many forms of conditional compilation - for example when doing if with a false constant, many compilers do not generate the code, knowing it will never run. This allows you to create a debug build (putting the constant as true), with all the checks, but when it comes to putting the code into production runs in "dumb mode" - without checking or proving anything, as you noticed. P.S. I suggest to wait more before accepting, this type of question usually generates interesting answers.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.