Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How would you implement a "trait" design-pattern in C#?

I know the feature doesn't exist in C#, but PHP recently added a feature called Traits which I thought was a bit silly at first until I started thinking about it.

Say I have a base class called Client. Client has a single property called Name.

Now I'm developing a re-usable application that will be used by many different customers. All customers agree that a client should have a name, hence it being in the base-class.

Now Customer A comes along and says he also need to track the client's Weight. Customer B doesn't need the Weight, but he wants to track Height. Customer C wants to track both Weight and Height.

With traits, we could make the both the Weight and the Height features traits:

class ClientA extends Client use TClientWeight
class ClientB extends Client use TClientHeight
class ClientC extends Client use TClientWeight, TClientHeight

Now I can meet all my customers' needs without adding any extra fluff to the class. If my customer comes back later and says "Oh, I really like that feature, can I have it too?", I just update the class definition to include the extra trait.

How would you accomplish this in C#?

Interfaces don't work here because I want concrete definitions for the properties and any associated methods, and I don't want to re-implement them for each version of the class.

(By "customer", I mean a literal person who has employed me as a developer, whereas by "client" I'm referring a programming class; each of my customers has clients that they want to record information about)

like image 807
mpen Avatar asked May 23 '12 23:05

mpen


People also ask

What is trait in programming?

In computer programming, a trait is a concept used in object-oriented programming which represents a set of methods that can be used to extend the functionality of a class.

What is the point of traits in Rust?

A trait tells the Rust compiler about functionality a particular type has and can share with other types. Traits are an abstract definition of shared behavior amongst different types. So, we can say that traits are to Rust what interfaces are to Java or abstract classes are to C++.

Is it allowed to make an interface use traits?

Traits can not implement interfaces. A trait allow both classes to use it for common interface requirement. It supports the use of abstract methods.

What is the use of traits?

Traits are used to declare methods that can be used in multiple classes. Traits can have methods and abstract methods that can be used in multiple classes, and the methods can have any access modifier (public, private, or protected).


2 Answers

You can get the syntax by using marker interfaces and extension methods.

Prerequisite: the interfaces need to define the contract which is later used by the extension method. Basically the interface defines the contract for being able to "implement" a trait; ideally the class where you add the interface should already have all members of the interface present so that no additional implementation is required.

public class Client {
  public double Weight { get; }

  public double Height { get; }
}

public interface TClientWeight {
  double Weight { get; }
}

public interface TClientHeight {
  double Height { get; }
}

public class ClientA: Client, TClientWeight { }

public class ClientB: Client, TClientHeight { }

public class ClientC: Client, TClientWeight, TClientHeight { }

public static class TClientWeightMethods {
  public static bool IsHeavierThan(this TClientWeight client, double weight) {
    return client.Weight > weight;
  }
  // add more methods as you see fit
}

public static class TClientHeightMethods {
  public static bool IsTallerThan(this TClientHeight client, double height) {
    return client.Height > height;
  }
  // add more methods as you see fit
}

Use like this:

var ca = new ClientA();
ca.IsHeavierThan(10); // OK
ca.IsTallerThan(10); // compiler error

Edit: The question was raised how additional data could be stored. This can also be addressed by doing some extra coding:

public interface IDynamicObject {
  bool TryGetAttribute(string key, out object value);
  void SetAttribute(string key, object value);
  // void RemoveAttribute(string key)
}

public class DynamicObject: IDynamicObject {
  private readonly Dictionary<string, object> data = new Dictionary<string, object>(StringComparer.Ordinal);

  bool IDynamicObject.TryGetAttribute(string key, out object value) {
    return data.TryGet(key, out value);
  }

  void IDynamicObject.SetAttribute(string key, object value) {
    data[key] = value;
  }
}

And then, the trait methods can add and retrieve data if the "trait interface" inherits from IDynamicObject:

public class Client: DynamicObject { /* implementation see above */ }

public interface TClientWeight, IDynamicObject {
  double Weight { get; }
}

public class ClientA: Client, TClientWeight { }

public static class TClientWeightMethods {
  public static bool HasWeightChanged(this TClientWeight client) {
    object oldWeight;
    bool result = client.TryGetAttribute("oldWeight", out oldWeight) && client.Weight.Equals(oldWeight);
    client.SetAttribute("oldWeight", client.Weight);
    return result;
  }
  // add more methods as you see fit
}

Note: by implementing IDynamicMetaObjectProvider as well the object would even allow to expose the dynamic data through the DLR, making the access to the additional properties transparent when used with the dynamic keyword.

like image 90
Lucero Avatar answered Sep 21 '22 23:09

Lucero


Traits can be implemented in C# 8 by using default interface methods. Java 8 introduced default interface methods for this reason too.

Using C# 8, you can write almost exactly what you proposed in the question. The traits are implemented by the IClientWeight, IClientHeight interfaces that provide a default implementation for their methods. In this case, they just return 0:

public interface IClientWeight
{
    int getWeight()=>0;
}

public interface IClientHeight
{
    int getHeight()=>0;
}

public class Client
{
    public String Name {get;set;}
}

ClientA and ClientB have the traits but don't implement them. ClientC implements only IClientHeight and returns a different number, in this case 16 :

class ClientA : Client, IClientWeight{}
class ClientB : Client, IClientHeight{}
class ClientC : Client, IClientWeight, IClientHeight
{
    public int getHeight()=>16;
}

When getHeight() is called in ClientB through the interface, the default implementation is called. getHeight() can only be called through the interface.

ClientC implements the IClientHeight interface so its own method is called. The method is available through the class itself.

public class C {
    public void M() {        
        //Accessed through the interface
        IClientHeight clientB = new ClientB();        
        clientB.getHeight();

        //Accessed directly or through the class
        var clientC = new ClientC();        
        clientC.getHeight();
    }
}

This SharpLab.io example shows the code produced from this example

Many of the traits features described in the PHP overview on traits can be implemented easily with default interface methods. Traits (interfaces) can be combined. It's also possible to define abstract methods to force classes to implement certain requirements.

Let's say we want our traits to have sayHeight() and sayWeight() methods that return a string with the height or weight. They'd need some way to force exhibiting classes (term stolen from the PHP guide) to implement a method that returns the height and weight :

public interface IClientWeight
{
    abstract int getWeight();
    String sayWeight()=>getWeight().ToString();
}

public interface IClientHeight
{
    abstract int getHeight();
    String sayHeight()=>getHeight().ToString();
}

//Combines both traits
public interface IClientBoth:IClientHeight,IClientWeight{}

The clients now have to implement thet getHeight() or getWeight() method but don't need to know anything about the say methods.

This offers a cleaner way to decorate

SharpLab.io link for this sample.

like image 15
Panagiotis Kanavos Avatar answered Sep 20 '22 23:09

Panagiotis Kanavos