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

mpen picture mpen · May 24, 2012 · Viewed 24.6k times · Source

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)

Answer

Lucero picture Lucero · May 24, 2012

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.