Question

Let's say there is a member SomeMethod in an interface ISomeInterface as follows:

public interface ISomeInterface
{
    int SomeMethod(string a);
}

For the purposes of my program, all consumers of ISomeInterface act upon the assumption that the returned int is greater than 5.

Three ways come to mind to solve this -

1) For every object that consumes ISomeInterface, they assert that the returned int > 5.

2) For every object that implements ISomeInterface, they assert that the int they're about to return is > 5.

Both the above two solutions are cumbersome since they require the developer to remember to do this on every single implementation or consumption of ISomeInterface. Furthermore this is relying upon the implementation of the interface which isn't good.

3) The only way I can think to do this practically is to have a wrapper that also implements ISomeInterface, and returns the underlying implementation as follows:

public class SomeWrapper : ISomeInterface
{
    private ISomeInterface obj;

    SomeWrapper(ISomeInterface obj)
    {
        this.obj = obj;
    }

    public int SomeMethod(string a)
    {
        int ret = obj.SomeMethod("hello!");
            if (!(ret > 5))
            throw new Exception("ret <= 5");
        else
            return ret;
    }
}

The problem though now is that we're again relying on an implementation detail of ISomeInterface via what the SomeWrapper class does, although with the benefit that now we've confined it to a single location.

Is this the best way to ensure an interface is implemented in the expected manner, or is there a better alternative? I understand interfaces may not be designed for this, but then what is the best practice to use some object under the assumption that it behaves a certain way more than what I can convey within its member signatures of an interface without needing to do assertions upon every time it's instanced? An interface seems like a good concept, if only I could also specify additional things or restrictions it's supposed to implement.

Was it helpful?

Solution

Instead of returning an int, return a value object that has the validation hard-coded. This is a case of primitive obsession and its fix.

// should be class, not struct as struct can be created without calling a constructor
public class ValidNumber
{
    public int Number { get; }

    public ValidNumber(int number)
    {
        if (number <= 5)
            throw new ArgumentOutOfRangeException("Number must be greater than 5.")
        Number = number;
    }
}

public class Implementation : ISomeInterface
{
    public ValidNumber SomeMethod(string a)
    {
        return new ValidNumber(int.Parse(a));
    }
}

This way, the validation error would happen inside the implementation, so it should show up when developer tests this implementation. Having the method return a specific object makes it obvious that there might be more to it than just returning a plain value.

OTHER TIPS

To complement the other answers, I'd like to partially comment on the following note in the OP by providing a broader context:

An interface seems like a good concept, if only I could also specify additional things or restrictions it's supposed to implement.

You are making a good point here! Let us consider on which levels we can specify such restrictions (constraints):

  1. in the language's type system
  2. via meta annotations internal or external to the language and external tools (static analysis tools)
  3. via runtime assertions — as seen in other answers
  4. documentation targetted at humans

I elaborate on every item below. Before that, let me say that the constraints get weaker and weaker the more you transition from 1 to 4. For example, if you only rely on point 4, you are relying on developers correctly applying the documentation, and there is no way anyone is able to tell you whether those constraints are fulfilled other than humans themselves. This, of course, is much more bound to contain bugs by the very nature of humans.

Hence, you always want to start modelling your constraint in point 1, and only if that's (partially) impossible, you should try point 2, and so on. In theory, you always would like to rely on the language's type system. However, for that to be possible you would need to have very powerful type systems, which then become untractable — in terms of speed and effort of type checking and in terms of developers being able to comprehend types. For the latter, see Is the Scala 2.8 collections library a case of “the longest suicide note in history”?.

1. Type System of the Language

In most typed (OO-flavored) languages such as C# it is easily possible to have the following interface:

public interface ISomeInterface
{
  int SomeMethod(string a);
}

Here, the type system allows you to specify types such as int. Then, the type checker component of the compiler guarantees at compile time that implementors always return an integer value from SomeMethod.

Many applications can already be built with the usual type systems found in Java and C#. However, for the constraint you had in mind, namely that the return value is an integer greater than 5, these type systems are too weak. Indeed, some languages do feature more powerful type systems where instead of int you could write {x: int | x > 5}1, i.e. the type of all integers greater than 5. In some of these languages, you also need to prove that as an implementor you always really return something greater than 5. These proofs are then verified by the compiler at compile time as well!

Since C# does not feature some types, you have to resort to points 2 and 3.

2. Meta Annotations internal/external to the Language

This other answer already provided an example of meta annotations inside the language, which is Java here:

@NotNull
@Size(min = 1)
public List<@NotNull Customer> getAllCustomers() {
  return null;
}

Static analysis tools can try to verify whether these constraints specified in the meta annotations are fulfilled or not in the code. If they cannot verify them, these tools report an error.2 Usually, one employs static analysis tools together with the classic compiler at compile time meaning that you get constraint checking at compile time as well here.

Another approach would be to use meta annotations external to the language. For example, you can have a code base in C and then prove the fulfillment of some constraints in a totally different language referring to that C code base. You can find examples under the keywords "verifying C code", "verifying C code Coq" among others.

3. Runtime Assertions

At this level of constraint checking, you outsource the checking from compile and static analysis time completely to runtime. You check at runtime whether the return value fulfills your constraint (e.g. is greater than 5), and if not, you throw an exception.

Other answers already showed you how this looks code-wise.

This level offers great flexibility, however, at the cost of deferring constraint checking from compile time to runtime. This means that bugs might get revealed very late, possibly at the customer of your software.

4. Documentation Targetted At Humans

I said that runtime assertions are quite flexible, however, still they cannot model every constraint you could think of. While it's easy to put constraints on return values, it's for instance hard (read: untractable) to model interaction between code components as that would require some kind of "supervisory" view on code.

For example, a method int f(void) might guarantee that its return value is the current score of the player in the game, but only as long as int g(void) has not been called superseeding the return value of f. This constraint is something you probably need to defer to human-oriented documentation.


1: Keywords are "dependent types", "refinement types", "liquid types". Prime examples are languages for theorem provers, e.g. Gallina which is used in the Coq proof assistant.

2: Depending on what kind of expressiveness you allow in your constraints, fulfillment checking can be an undecidable problem. Practically, this means that your programmed method fulfills the constraints you specified, but the static analysis tool is unable to prove them. Or put differently, there might be false negatives in terms of errors. (But never false positives if the tool is bug-free.)

You're trying to design by contract, where that contract is that the return value must be greater than 5. Unfortunately, if you're relying on an interface, the only contract you have is the method signatures.

I'd suggest using an abstract class instead. Here's an approach I would take in Java:

public abstract class SomeAbstraction {
    public final int someMethod(String a) {
        // You may want to throw an exception instead
        return Math.max(6, someAbstractMethod());
    }

    protected abstract int someAbstractMethod();
}

As long as your sub-classes are isolated in a separate package (where someAbstractMethod would be inaccessible) then clients of this abstraction will only have access to someMethod and can safely rely on the return value always being greater than 5. The contract is enforced in one place, and all sub-classes must adhere whether they know it or not. Using the final keyword on someMethod has the added benefit of preventing sub-classes from forcibly breaking that contract by overriding it.

Note: Typically, a contract violation should be exceptional behavior. Thus, you'd probably want to throw an exception or log an error instead of just forcing the return value. But this depends entirely on your use case.

You may have validating annotations that restrict the aceptable returned values. Here is an example in Java for Spring taken from baeldung.com but in C# you have a similar feature:

   @NotNull
   @Size(min = 1)
   public List<@NotNull Customer> getAllCustomers() {
        return null;
   }

If you use this approach you must consider that:

  • You need a validation framework for your language, in this case C#
  • Adding the annotation is just one part. Having the annotation validated and how this validation errors are handled are the important part. In the case of Spring it creates a proxy using dependency injection that would throw a Runtime ValidationException
  • Some annotations may help your IDE to detect bugs at compile time. For example, if you use @NonNull the compiler can check that null is never returned. Other validations needs to be enforced at runtime
  • Most validation frameworks allow you to create custom validations.
  • It is very useful for processing input data where you may need to report more than one broken validation at the same time.

I do not recommend this approach when the valdiation is part of the business model. In this case the answer from Euphoric is better. The returned object will be a Value Object that will help you create a rich Domain Model. This object should have a meaningful name with the restrictions acording to the type of business you do. For example, here I can validate that dates are reasonables for our users:

public class YearOfBirth

     private final year; 

     public YearOfBirth(int year){
        this.year = year;    
        if(year < 1880){
            throw new IllegalArgumentException("Are you a vampire or what?");
        }  
        if( year > 2020){
            throw new IllegalArgumentException("No time travelers allowed");     
         }
     }
}

The good part is that this kind of object can attract very small methods with simple and testeable logic. For example:

public String getGeneration(){
      if( year < 1964){
           return "Baby Boomers";
      }
      if( year < 1980 ){
           return "Generation X";
      }
      if( year < 1996){
           return "Millenials";
      }
      // Etc...
}

It doesn't have to be complicated:

public interface ISomeInterface
{
    // Returns the amount by which the desired value *exceeds* 5.
    uint SomeMethodLess5(string a);
}

You can write a unit test to find all implementations of an interface, and run a test against each of them.

var iType= typeof(ISomeInterface);
var types = AppDomain.CurrentDomain.GetAssemblies()
    .SelectMany(s => s.GetTypes())
    .Where(p => iType.IsAssignableFrom(p));

foreach(var t in types)
{
    var result = t.SomeMethod("four");
    Assert.IsTrue(result > 4, "SomeMethod implementation result was less than 5");
}

When a new implementation of ISomeInterface is added to your project, this test should be able to test it and fail if it returns something less than 5. This, of course, assumes you can test it properly from the input to this method alone, which I am using "four" here. You might need to do other ways to setup this call.

Intellisense Code Documentation

Most languages have a form of Intellisense Code Documentation. For C#, you can find information on it here. In a nutshell, it is comment documentation the you IDE Intellisense can parse, and make available to the user when they want to use it.

What your interface documentation says is the behavior of a call is the only real contract of how it should behave. After all, your interface doesn't know how to tell the difference between a random number generator that gives good output, and one that always returns 4 (determined by a perfectly random die roll).

Unit Tests to verify the documentation is implemented faithfully

After the documentation, you should have a unit test suite for your interface that given an instance of a class that implements the interface, gives off the expected behavior when run through various use cases. While you could bake the unit tests into an abstract class to reinforce the behavior, that is overkill and will probably cause more pain than it's worth.

Meta Annotations

I fill I should also mention some languages also support some form of meta annotations. Effectively assertions that are evaluated at compile time. While they are limited in the types of checks they can do, they can at least verify simple programing mistakes at compile time. This should be considered more a compiler assist with the Code Documentation than an enforcer of the interface.

In Java if you have a custom constructor, the empty constructor doesn't apply anymore and so you can force calling the custom constructor. My understanding is that C# works the same.

So you could have something like

public class SecondClass {
     public SecondClass(//some argument){
     // do your checks here and throw an exception
     }
}

public abstract class FirstClass extends SecondClass{
    public FirstClass(){
     // we're forced to call super() here because no empty constructor in SecondClass
     super(//some argument)
      }
}

This is pretty rough, but you get the idea.

Licensed under: CC-BY-SA with attribution
scroll top