DEV Community

Cover image for Pseudo Traits in C#
Henrick Tissink
Henrick Tissink

Posted on

Pseudo Traits in C#

C# 8.0 Interfaces are not really Interfaces anymore

alt text

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

alt text

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 for Running() { ... }
  • Swimmer would have an implementation for Swimming() { ... }
  • and Flyer would come with an implementation for Flying() { ... }.

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

alt text

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

alt text

Let's put together some trait boilerplate.

public abstract class Trait { }

interface ITrait<T> where T : Trait { }
Enter fullscreen mode Exit fullscreen mode

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 { }
Enter fullscreen mode Exit fullscreen mode

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!");
    }
}
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

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(); 
Enter fullscreen mode Exit fullscreen mode

alt text

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)

Collapse
 
radumg profile image
Radu Gidei

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).

Collapse
 
htissink profile image
Henrick Tissink

Interesting suggestion - I'll take a crack at it :] that would make traits really powerful.

Collapse
 
radumg profile image
Radu Gidei

👏 let us know how you get on and if we can help, I’d be happy to.

Collapse
 
lolle2000la profile image
Lolle2000la

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

Collapse
 
lolle2000la profile image
Lolle2000la • Edited

I have made an article myself in response to this article.

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.

Collapse
 
yeah69 profile image
Dima • Edited

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 >

Collapse
 
bandysc profile image
Bartosz Korczyński

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"?

Collapse
 
htissink profile image
Henrick Tissink

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 or CanBounce fall in the same category of trait classes.

Collapse
 
dbarwikowski profile image
Daniel Barwikowski

Hey, great article. I'll play with it as it look really fun

Also, shouldn't ShapeTraits methods be static?

Collapse
 
htissink profile image
Henrick Tissink

yes! well spotted.

Collapse
 
agrothe profile image
Andrew Grothe

Porting code from Java just got a lot easier though, because of this.

Collapse
 
webbes profile image
webbes

Or one could simply use the good old decorator pattern.