C# 8.0 Interfaces are not really Interfaces anymore
C# 8.0 ships with the wonderfully horrible idea of interfaces with default implementations.
First of all I'd like to start by saying that interfaces with default implementation is a terrible idea. An interface is a contract void of implementation and has no place having default implementation. This defeats the entire purpose of an interface. What could possibly be better is for the allowance of multiple inheritance of abstract classes. But I digress.
Traits
The idea behind interfaces with default implementations all comes from a programming concept known as a trait. A trait is a programming construct that allows you to attach default behaviour to objects, by simply having them exhibit the trait.
You could have traits like Runner
, Swimmer
, and Flyer
.
-
Runner
would have an implementation forRunning() { ... }
-
Swimmer
would have an implementation forSwimming() { ... }
- and
Flyer
would come with an implementation forFlying() { ... }
.
Now you could simply define a Penguin
with both the traits Runner
and Swimmer
without actually having to write the code for Swimming()
or Running()
because by having the traits the functionality is already implemented.
C# really wasn't developed to do this, or at least to do this well. Traits are a more functional construct. A language like Swift
, with it's Protocol-Oriented Programming
style, caters to this concept very nicely with Protocol Extensions
.
Let's take a look at a creative way of doing this in C# 7.0, and I use the term creative very loosely.
Get Schwifty
In Swift
a Protocol
behaves very similarly to an Interface
in C#, and achieves the ability of traits through a Protocol Extension
. A Protocol Extension
in Swift should in theory then be similar to an Interface
extension in C#. This inspired me to try and figure out a way to achieve traits in C#.
Let's create shapes.
- Some shapes can roll
- and some shapes can bounce.
- Some shapes can both roll and bounce.
The Object-Oriented way of implementing this would be to create interfaces for rolling and bouncing.
Any shape that rolls must implement the rolling
interface, and any shape that bounces must implement the bouncing
interfaces. Could it perhaps be agreed that rolling and bouncing are not so different for each of the shapes? That a bounce is a bounce and a roll is a roll. And maybe we don't want to go and implement a bounce or a roll for every single shape we create.
But wait, you may say, why not just create a super class
that rolls? or a super class
that bounces?
Firstly, inheritance is bad. Secondly, are you going to create a super class that rolls, a super class that bounces, and a super class that rolls and bounces? Mmm, if only you had multiple inheritance...
You don't have multiple inheritance with classes - but you can implement multiple interfaces.
So... what do we do now? I hinted at interface extensions, let me explain how it works.
- disclaimer: this is in no-way recommended as a good way of writing code, it is strictly a thought experiment.
Let's Get Cooking
Let's put together some trait boilerplate.
public abstract class Trait { }
interface ITrait<T> where T : Trait { }
We have an abstract class Trait
to be a filter for our traits. We never want to implement Trait
directly, but we sure do want a family of things that are traits. The trait class will be used when we define the interfaces.
Using an interface is key, because we can implement multiple of them, we can have as many traits as we want.
Next, let's create some traits.
public CanRoll : Trait { }
public CanBounce: Trait { }
Again, this is kind of boiler plate. It's the bit of fluff that will allow us to implement magical traits.
Finally, lets get to the juice.
public static class ShapeTraits
{
// this only applies to objects that CanBounce
public static void Bounce(this ITrait<CanBounce> bouncer)
{
Console.WriteLine("I can bounce!");
}
// this only applies to objects that CanRoll
public static void Roll(this ITrait<CanRoll> roller)
{
Console.WriteLine("I can roll!");
}
}
There it is - our two beautiful traits implemented. Next, let's create some shapes.
// A cylinder can roll
public class Cylinder: ITrait<CanRoll>
{
// this is empty!
}
// A cube can bounce
public class Cube: ITrait<CanBounce>
{
// this is empty!
}
// And, implementing multiple interfaces,
// a ball can bounce and roll!
public class Ball: ITrait<CanBounce>, ITrait<CanRoll>
{
// this is empty!
}
Now we can create instances of these classes, classes containing no code at all, and because of these pseudo traits they can actually do things!
var cylinder = new Cylinder();
cylinder.Roll(); // this works!
var cube = new Cube();
cube.Bounce(); // this works!
var ball = new Ball();
// a ball that rolls and bounces
ball.Roll();
ball.Bounce();
By using interface extensions, we have managed to mimic the ability of traits, to create something I call pseudo traits. It's a different way of thinking about objects, and I must say I really like it.
You can build extensions methods and hand pick which ones you want to belong to a certain trait. Picking which traits you want on an object allows you to construct an object bit-by-bit, just by implementing the right interfaces.
Is this a good idea? Honestly, I don't know. Is it a fun thought experiment? I definitely think so :]
Top comments (12)
Great post Patrick, thanks for sharing! Been thinking about something like this for a while now, have you explored the ability to add traits at runtime ? Thatβs the nugget I havenβt able to crack (in a satisfactory manner).
Interesting suggestion - I'll take a crack at it :] that would make traits really powerful.
π let us know how you get on and if we can help, Iβd be happy to.
For me the biggest benefit of default interface implementations is (binary) backwards compactability and interoperability with Java. And I think originally that was it's purpose.
When you use interfaces as contacts for extensions you would have to make a new interface to retain comparability with old implementers, and you cannot break all 2 week old extensions for some new features.
That's what I'm excited about.
I always thought as traits being a side-product. And as I read the discussions and the then-WIP spec of them it sounded like it being one
I have made an article myself in response to this article.
Why interface default implementations in C# are a great thing
Lolle2000la γ» Sep 5 γ» 5 min read
It outlines why I think that default interfaces are great. I actually agree about them not being useful as traits (because of confusion), but have a few other reasons that make me excited about them.
Fun thought experiment, indeed. But has some limitations as well.
What if you need some data for your Traits? Where will the data be accessable? CanRoll could have properties, like Velocity or Acceleration. However, than the ITrait<T> interface is no marker interface anymore. To access this data you'll need a "T Value" property in ITrait. Hence, the traited class would need a Value-property per Trait.
The pseudo traits could have name collisions as well. What if you implement "public static void Roll(this ITrait<CanRickRoll> roller)" and assign ITrait<CanRickRoll> on Ball? What does "ball.Roll();" do?
Using this pseudo trait pattern would require to keep this limitations in mind. The default interface sure have their own set of limitations. But they become standard for all code bases of C#8.0 and above, so all developer can align themselves to this specific limitations and learn to deal with it. Integrating the pseudo trait pattern into a code base of C#8.0 and above would consequetly mean to potentially deal with limitations of default interfaces and the pseudo trait pattern.
EDIT: Manually escaped < and >
What is the point of having Trait class here instead of traits being just an empty interface? Just to know it is a "pseudo trait"?
It's a filter/grouping, so you know that what you're extending is definitely a (pseudo) trait. You can do without it - it's definitely a stylistic choice. But I like the safety of it. Your
CanRoll
orCanBounce
fall in the same category of trait classes.Hey, great article. I'll play with it as it look really fun
Also, shouldn't ShapeTraits methods be static?
yes! well spotted.
Porting code from Java just got a lot easier though, because of this.
Or one could simply use the good old decorator pattern.