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):
- in the language's type system
- via meta annotations internal or external to the language and external tools (static analysis tools)
- via runtime assertions — as seen in other answers
- 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.)
!(ret > 5)
, it is possible thatret == 5
.open
be the only method ofUnopenedFoo
, returning aFoo
withtransmit
andclose
. Using types to prevent callingtransmit
afterclose
is much harder, e.g. needing linear/affine/uniqueness types. Most languages do this with scope instead, e.g. puttingclose
logic in a destructor, or bothopen
andclose
logic in awithFoo
function/method taking a callback.