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)
Solution 1:
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.
Solution 2:
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.