Interface Implementation Through Aggregation

It's very tedious, mechanical and error-prone to implement an interface where all its members just delegate the call to some field or property that also implements the interface. For these C# types:

interface I {
  void M1();
  void M2();
  void M3();
}

class C : I {
  public void M1() { /*...*/ }
  public void M2() { /*...*/ }
  public void M3() { /*...*/ }
}

To implement the interface I in another class that just delegates the implementation to C, all the delegating code has to be done manually:

class D : I {
  I impl = new C();
  public void M1() { impl.M1(); }
  public void M2() { impl.M2(); }
  public void M3() { impl.M3(); }
}

It's not a surprise, then, that a very common request to enhance C# is the ability to lean on the compiler to generate this delegating code. The idea is to have some kind of syntax like this to achieve the same result as above:

// Warning: Invalid C# ahead
class D : I {
  I impl = new C() implements I;
}

This has already been implemented in the Oxygene language. You express it like this:

type D = public class(I)
  private property Impl : I := new C(); readonly; implements public I;
end;

The property Impl is initialized with an instance of C, and the declaration implements public I instructs the compiler to implement I in D by creating public delegating methods to Impl.

This naïve solution, though, has many limitations:

If two or more interfaces have conflicting members, some form of disambiguation is necessary. In Oxygene, the first declared implementation in the source code wins, and its members override any subsequent implementations with the same members. This kind of implicit behavior is fragile and might be surprising. It's also not sufficient to resolve all situations, like when you'd prefer to use different members from different interfaces that are in conflict;

You can't use abstract classes. There are many situations where abstract classes provide parts of an interface implementation for reuse. It should suffice for the delegating class to just implement the members declared abstract in the abstract class. Since you need an instance of a class that implements the interface, abstract classes can't be directly used for this;

Although it is possible to have local specializations for some interface members (what can be used to resolve conflicting members), they are unreliable and may hide unintended bugs. This is because the implementing class might call then, and since it's unaware of the delegating class, it will not call the specializations. This code demonstrates the issue:

namespace Samples;

interface

uses System;

type I = public interface
  method M1; 
  method M2;
end;

type ImplementingClass = public class(I)
  public method M1; virtual;
  public method M2; virtual;
end;

type DelegatingClass = public class(I)
  private property IImpl : I := new ImplementingClass(); readonly; implements public I;
  public method M2; // local specialization of I::M2
end;

type MainProgram = public class
  class method Main;
end;

implementation

method ImplementingClass.M1;
begin
  Console.WriteLine("ImplementingClass.M1");
  M2(); // this call is to ImplementingClass::M2, and it's unaware of specializations
end;

method ImplementingClass.M2;
begin
  Console.WriteLine("ImplementingClass.M2");
end;

method DelegatingClass.M2;
begin
  Console.WriteLine("DelegatingClass.M2");
end;

class method MainProgram.Main;
begin
  var iImpl := new DelegatingClass() as I;
  Console.WriteLine("====M1");
  iImpl.M1();
  Console.WriteLine("====M2");
  iImpl.M2();
end;
 
end.

This code prints:

====M1
ImplementingClass.M1
ImplementingClass.M2
====M2
DelegatingClass.M2

When calling M2 directly, the expected specialization for I::M2 in DelegatingClass is called. But M1, which is implemented in ImplementingClass, calls in turn ImplementingClass::M2, completely oblivious of the local specialization that was provided in DelegatingClass.

So, this technique doesn't properly solve the original problem of automatic delegation. Roles could be used to cleanly solve the problem, overcoming all those limitations. Also, if the role actually expresses the default implementation for the interface, it can effectively replace the interface:

// Warning: Invalid C# ahead
role R {
  public void M1() { /*...*/ }
  public void M2() { /*...*/ }
  public void M3() { /*...*/ }
}

class D : R {
}

Comments

  1. http://nemerle.org/Design_patterns

    Here you have design pattern macro in Nemerle.
    Very handy.

    ReplyDelete
  2. @NN: Thanks for the link. I know that Nemerle also has this as a macro (the Proxy design pattern). But, it still suffers from the same limitations that I outlined above. I'd like to see traits in Nemerle...

    ReplyDelete
  3. I don't think traits are going to appear in the near future.
    You can however improve this macro and add your part to the compiler :)

    ReplyDelete
  4. Haven't used a newer release of Oxygene, but from what I recall this is not true:
    "In Oxygene, the first declared implementation in the source code wins"

    In Oxygene, you can explicitly state which member implements which member of your interface:

    method Abc; implements ISomeInterface.Def;

    http://prismwiki.codegear.com/en/Implements_(keyword)

    ReplyDelete
  5. @Robert Giesecke: There are two uses for the implements keyword that are of interest: when you implement a whole interface through a property; and when you implement a single member of an interface through a member in the class. The first usage is the one I'm referring to in the post. What I mean is that, when you implement 2 (or more) whole interfaces through 2 properties and these interfaces declare conflicting members, the first implemented interface member is the one that ends up in the class. And, as you mentioned, it is possible to use implements on a single member to disambiguate and delegate to the member that you really want to be in the class. But you have to be aware that a conflict exists in the first place in order to do that, the compiler doesn't help you there (I also don't know about newer versions, so this could have changed). And the specialization problem that I mentioned can still happen, because you're effectively creating a local specialization.

    ReplyDelete

Post a Comment

Popular posts from this blog

The Acyclic Visitor Pattern

Some OO Design

NRoles: An experiment with roles in C#