5

Programming against interfaces is an often-heard good practice in software development. Together with extension methods, this provides a great functionality. However, in C#, there are limitations to that. Let's declare a simple interface for a 3D-Point:

public interface IPoint3D
{
  double X { get; }
  double Y { get; }
  double Z { get; }
}

I can add extension methods for this point in a static class, for example:

public static class Point3DExtensions
{
  public static double SumOfCoordinates(this IPoint3D point)
  {
    return point.X + point.Y + point.Z;
  }
}

However, what I can not code is functionality which returns an instance of the interface itself, like for example:

public static class Point3DExtensions
{
  public static IPoint3D Add(this IPoint3D point1, IPoint3D point2)
  {
    return new IPoint3D(point1.X + point2.X, point1.Y + point2.Y, point1.Z + point2.Z);
  }
}

The problem here is the part new IPoint3D, because I can't create an instance of an interface (it's just a contract, not an implementation).

As a workaround, I'd see two possibilites:

In the same assembly as the interface, create an internal class implementing the interface, for example:

internal class SimplePoint : IPoint3D
{
  public double X { get; }
  public double Y { get; }
  public double Z { get; }

  public SimplePoint(double x, double y, double z)
  {
    this.X = x;
    this.Y = y;
    this.Z = z;
  }
}

This class won't be seen from outside the assembly, but now, my extension method Add can still have IPoint3D as return type, but actually return an instance of SimplePoint. Noone from outside of the assembly will be able to cast the returned instance to SimplePoint, and doesn't need to.

The second option would be to add something like a Create-Method to the interface:

IPoint3D Create(double x, double y, double z);

However, this method is not static, so I would have to invoke it from any instance of IPoint3D, which is kind of weird, and I don't always have such an instance. The problem here is that static methods are not allowed for interfaces in C#. If they were, one could simply add static IPoint3D Create(double x, double y, double z); or even something like new(double x, double y, double z); (for a constructor) to the interface, and use that for object creation.

Now, the actual questions:

  • Am I trying to misuse interfaces here?
  • Did I miss any possibility to get the desired behaviour?
  • Which languages would offer this desired functionality?

Side Note: What I would like to have is e.g. an extension method for classes implementing an interface ICircle3D, which can calculate a number of points IPoint3D on those circles.

  • 8
    I would not use interfaces for value-objects, unless you have a good reason to. – CodesInChaos Dec 9 '16 at 12:52
  • 2
    Is there a reason you can't use generics for this? I'm not very experienced with C# so I don't know if there are any restrictions regarding extension methods, but AFAIK generics can be constrained to provide a new operator for an argument. – amon Dec 9 '16 at 13:03
  • 1
    @amon is on the right path, but the new constraint requires a parameterless constructor. Good for general value types, but bad for this sort of immutable class design unfortunately. I expect that this is awkward in most languages, and is avoided by not interfacing things like this that are not ever going to get different implementations. – Telastyn Dec 9 '16 at 13:05
  • @Telastyn Regarding "[...] not interfacing things like this that are not ever going to get different implementations." Problem is, we have e.g. different Circle-Classes in different assemblies, which we do not want to merge at this point, but we would still like to be able to calculate points on all those circles... Reasons for not merging are that they offer a lot of different functions, and we don't want to clutter each other's classes... (However, the extension methods kind of do that, too, but in another place). – Timitry Dec 9 '16 at 13:34
  • 1
    If you have different Circles in different assemblies, you can't edit those assemblies to add the interface in the first place since you don't have the code. If you do have the code, then you can go in and fix your duplicate code... – Telastyn Dec 9 '16 at 13:46
11

I doubt that an interface like IPoint3D brings you more benefits than trouble, so here my suggestion: create a concrete, immutable (so also sealed) class Point3D instead. When using interfaces, the geometric point/vector operations can only be implemented in a fashion where the underlying type information gets lost, but that is not a problem of the programming language, but a problem of the domain.

The reason for this becomes clearer when you try to make two different implementations of that interface, lets say an AutocadPoint3D (representing a point in an autocad CAD model, with lots of additional data like tags, color, layer etc.) and a GoogleEarthPoint3D (representing a coordinate in an KML model). Now try to add points of those two classes by a method like

   IPoint3D Add(IPoint3D point1, IPoint3D point2)

So what should be the underlying type of the result be? A new AutocadPoint3D, a GoogleEarthPoint3D, or something different (like your SimplePoint)? The only thing which probably makes sense would probably be the SimplePoint, because it is something like the "smallest common denominator" of all 3D points. Moreover, IPoint3D cannot be used to force sealed implementations, so code which relies on IPoint3D cannot be sure the underlying type is always immutable. This might suffer from unexpected side effects when the implementer of the interface is not very careful.

Think about the reason for what purpose you typically use interfaces. The most frequent purpose is probably dependency injection, especially for unit testing. But it seldom makes sense to mock out a value object like Point3D. Imagine you need to test a class which needs an IPoint3D, you would probably inject a SimplePoint object with some predefined values as test data - but then you could also use a SimplePoint directly and save the interface. So the interface only adds "noise" to your code, with no real benefit.

  • 1
    Thanks for these interesting thoughts, about which I need to think about some time now :) Another problem which comes to mind is, that when adding two AutocadPoint3Ds, maybe some of their additional properties should be carried over to the new instance. Guess that's the point where you can't use an extension method and really have to write the "Add"-Method into the interface and force the implementation, or leave it out completely. – Timitry Dec 9 '16 at 13:44
  • 2
    @Timitry: exactly my point. Better give an AutocadPoint3D a conversion method ToPoint3D() with return type Point3D. That will result in a simpler, more straightforward design (actually, I am talking from experience, we use our own Point3D class in our inhouse development for more than 10 years, and the decision to make it immutable and not interface based worked very well for us). – Doc Brown Dec 9 '16 at 13:53
3

Firstly, please don't let this answer distract you from Doc Brown's advice. A class like Point3D has one characteristic that makes it slightly different from other data transfer objects: applications that use Point3D at all typically has to use it in lots of mathematical operations. It is not uncommon to see that an application contains thousands of lines of code that directly manipulate Point3D. Being a small object, copying (and conversion) is favored over abstraction (of an interface).

That said, the dilemma is a known limitation of the current version of C#, as it has been acknowledged by Jon Skeet in this answer:

I've previously suggested that "static interfaces" could express this reasonably neatly. They'd only be useful for generic type constraints (I suspect, anyway) but then you could express:

  • Constructors with arbitrary parameters
  • Static methods and properties
  • Operators

Given that the C# standard is iterating very quickly in recent years, I would not be surprised if this somehow gets added to the language soon.


(Edited)

That said, the designers of C# had to use some caution, because traditionally C# is designed to be extremely reflection-friendly, and there is a megaton of legacy code that depend on reflection that would break if new features are introduced without careful consideration.


A typical workaround looks like this. Note that:

  • Typically, the Create function is separated from the data contract, because the data contract does not need to be generic (recursively dependent on the concrete type), but the Create function has to use that trick.
  • You can see the same trick used in IComparable<T>,
    as in
    public class Concrete : IComparable<Concrete> {...}
  • The creation of a useless empty instance of SimplePoint prior to calling its Create method, as in return new T().Create(...) is an unfortunate overhead of this workaround.

Again, please don't let this code sample distract you from Doc Brown's advice. The workaround is unnecessarily complicated and not worthwhile for Point3D.


public interface IPoint3DFactory<Impl>
{
    Impl Create(double x, double y, double z);
}

public static class IPoint3DExtensionMethods
{
    public static T Add<T>(this T left, T right)
        where T : IPoint3D, IPoint3DFactory<T>, new()
    {
        return new T().Create(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
    }
}

public class SimplePoint : IPoint3D, IPoint3DFactory<SimplePoint>
{
    public double X { get; }
    public double Y { get; }
    public double Z { get; }

    public SimplePoint()
    {
    }

    public SimplePoint(double x, double y, double z)
    {
            X = x;
            Y = y;
            Z = z;
    }

    public SimplePoint Create(double x, double y, double z)
    {
        return new SimplePoint(x, y, z);
    }
}
  • Thank you for this really interesting trick! It hurts a bit to see this much of a workaround, and you're right that it shouldn't be used for simple things, but maybe this can get useful sometime in the future :) – Timitry Dec 12 '16 at 9:02
  • @Timitry Remember that copying and conversion can also be written as extension methods - with or without the cooperation of third-party code authors. (i.e. methods can be "added" after the fact without library modification.) Copying and conversion of small objects is no big deal, unless you need to do this for billions of objects per second. – rwong Dec 12 '16 at 9:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.