Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Accept the weakest, return the strongest. But why? [closed]

Tags:

c#

interface

Asking myself again and again what good code is, I read the advice "Accept the weakest, return the strongest".

For me it is obvious why to accept the weakest: The method declares the weakest possible contract with its client. So the clients do not need to be "specialized" against a very "strong" interface.

"Return the strongest" is not that clear for me. Why should I return the strongest interface possible? What is the strongest interface? How do you quantify the strongness?

Assume there is a method returning a sequence of elements. The weakest interface would be of type IEnumerable. Following the guideline we should return someting like IList. But why?

I would like to ask for an explanation why to return the strongest interface.

like image 546
faronel Avatar asked Jun 05 '13 07:06

faronel


4 Answers

I don't think returning the "strongest" (or rather, most specific or derived) type is necessarily a good idea, because if your interface or contract specifies that you return List<T> you'll be stuck if you want to change that to Collection<T> in future, because both of those are distinct implementations of IList<T> with no common ancestor. You'd have to modify the interface.

The mantra you've cited seems very dogmatic - there are always exceptions and I'd just say you should stick with what's appropriate for a given job. It sounds like a bastardisation of the network/IO programming tenet "Be liberal in what you accept as input, but strict in what you output" - but network/IO programming is very different compared to defining the types of an OO interface or implementation because an ABI is a very different beast compared to a human-readable specification (e.g. the RFC for HTTP).

like image 163
Dai Avatar answered Nov 16 '22 02:11

Dai


This rule has several forms, here is another:

  • Input should be the most general/generic (not in the .NET/C# way) type
  • Output should be the most specific type

The reasoning behind this is to make it:

  1. Easy to take in all kinds of types that support the general interface
  2. Make it clear what kind of data structure that is actually being returned, gives the caller more flexibility and perhaps even more performance

Let's take an example.

Let's say you want to create a method that takes a collection of integers, processes them, and then returns a new collection of integers.

A perfectly good implementation of this could be:

public IEnumerable<int> Process(IEnumerable<int> input) { ... }

Note that I did not necessarily follow the second point here, but it depends on what the method does. If the method processes one item at a time, the above declaration is quite good, it signals that you're not necessarily getting back a list, or an array, or whatnot, just "a collection".

However, let's assume that the method has to gather all the values up first, then process them, and then return them. A better declaration in this case might be this:

public List<int> Process(IEnumerable<int> input) { ... }

Why is this better? Well, first of all, quite often, the caller wants to gather up all the values anyway, and now he doesn't have to, it already comes back as a single collection that contains all the values. Also, if the caller just wants to continue using the values as an IEnumerable<int>, that's possible as well.

The purpose of the rule is thus to increase flexibility and in some cases performance (memory or speed).

Note that the rule does not mean that you should always strive to return a list or an array or something. You should instead strive to return the "most functional" type possible from the data structure you already build up. Also note that you should of course never return references to internal data structures that will continue to live beyond the method call, instead return a copy.

like image 24
Lasse V. Karlsen Avatar answered Nov 16 '22 01:11

Lasse V. Karlsen


Interesting concept - one I've never explicitly heard before. It's basically Postel's Law "be conservative in what you send, liberal in what you receive" which is generally applied in cross-system communications.

IList is "stronger" in that it makes more guarantees than IEnumerable. An IList supports adding, removing, enumeration, etc. whereas IEnumerable only supports enumeration. Of course, List is even stronger, being a concrete type.

As for the wisdom of the advice itself - I think it's generally a good idea inside of an application, with some serious caveats for API designers.

On the positive side, returning the "strongest" type allows the caller all possible operations without breaking the contract (eg., by casting) or doing extra work (eg., copying the IEnumerable into a List). That's generally a help in an application and since it's within a single application, any breaking changes will be caught at compile-time and can easily be fixed.

However, for APIs that need to be concerned with backwards compatibility (eg., used by more than 1 app, or deployed independently) the return type is part of the contract. In that case, you really have to weigh the significance of forever supporting the IList return instead of a weaker IEnumerable. The specifics there should be dictated by the intent of the API, not by adhering to this rule.

FWIW, my personal pithy statement would be something like "return what's useful to the caller, and no more." I see this as a shortcut to deciding "what's useful to the caller" in the context of a single app, but actively harmful in an external API.

like image 20
Mark Brackett Avatar answered Nov 16 '22 01:11

Mark Brackett


"Strongest" is a bit of a vague term and could mean at least two things:

  • Return the "Most Derived" interface, e.g. return IList<T> or List<T> instead of ICollection<T> or IEnumerable<T>
  • Return the "Most Constrained" range of values. For example, return numbers in the range 10..20 rather than in the range 5..25

I'm going to take the opposite tack and suggest that you perhaps shouldn't always follow that advice, at least for the first point.

Consider the case of Linq. Imagine if the interface for Linq followed that advice and returned List<T> everywhere instead of IEnumerable<T>. Clearly, that would be unworkable.

For a specific example, consider Enumerable<T>.Range(int start, int count). This could return a List<T>, but that would be very inefficient in most cases.

Also imagine that you wanted to return a sequence of items from a class that you didn't want the caller to modify. You should return a ReadOnlyCollection<T> rather than a List<T>. In fact in some of those cases I think you should return an IEnumerable<T>.

Code Contracts and Overriding methods in derived classes

There's a different thing to consider when implementing overridden methods in derived classes.

You are allowed to weaken a precondition, since this will guarantee that any callers of the base class version continue to work. You could not have it so that a derived class was more strict about the values and/or types passed to a method (in this case, the compiler would not allow "weaker" (i.e. less derived) types to be passed in - but it would have no control over the allowed range of values that could be passed in).

Similarly, you are allowed to strengthen a postcondition. This can apply to both types and output ranges. For example, if the base class method returns an IEnumerable<T> you could return an IList<T> (although it would be returned as an IEnumerable<T> of course).

And if the base class said that returned values must be in the range 0..100, you could return values in the range 40..60 without breaking the code contract.

like image 25
Matthew Watson Avatar answered Nov 16 '22 01:11

Matthew Watson