Henrick Tissink
Posted on September 4, 2019
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 :]
Posted on September 4, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 26, 2024