Roles in C#

Traits (pdf) are described as composable units of behavior, and are a natural evolutionary path for languages like C# or Java. Microsoft must be seriously looking at them, since it's sponsored some research about Traits in C# (pdf). I'll describe what I think is an extension to the traits idea that would make a great feature in C# (and in Java, VB.NET, ...): roles.

Roles are already a reality in Perl 5, with the Moose::Roles module. They even have their own syntax in Perl 6. They help create better organized and reusable code, are an effective and safer alternative to multiple inheritance, and have explicit semantics (which means no surprises) as opposed to mixins. What I'll describe here is based on the theoretical traits (as described in the traits paper), on Perl roles and on Scala traits, all adapted to C#.

A role is, like a class, a type that holds behavior and state. It can be composed into a type (either a class, a struct or another role) to make its members (but not nested types) part of that type. Multiple roles can be composed into a single type, and conflicting members must be explicitly resolved by that type.

Consider this role:

role RPrintable {
  public void Print() {
    Console.WriteLine(ToString());
  }
}

It has a method that prints itself to the console. Itself will be the class that composes it, so the type of this (and of base) is not defined by the role, but changes for each class that uses it. This class composes it:

class Customer : RPrintable {
  public string Name { get; set; }
  public override string ToString() {
    return Name;
  }
}

Now the role member is part of the class interface:

var guybrush = new Customer { 
  Name = "Guybrush Threepwood" 
};
guybrush.Print();

The role type can also be used in its own right (like with an interface):

RPrintable printable = guybrush;
printable.Print();

// ...

void PrintThemAll(IEnumerable<RPrintable> printables) {
  foreach (var printable in printables) {
    printable.Print();
  }
}

// ...

void Method<T>(T doesRole1AndRole2) where T : Role1, Role2 { 
  // ...
}

Roles are implicitly abstract and cannot be instantiated. If a role defines an abstract member, a composing class must provide an implementation for it. This implementation can also come from other roles that the class composes. For example:

role RSchedulableCommand {
  public abstract void Execute();
  public void Schedule(DateTime when) {
    // uses a Timer to schedule a call to Execute
  }
}

class PrintCommand : RPrintable, RSchedulableCommand {
  public string What { get; set; }
  public void Execute() {
    Print();
  }
  public override string ToString() {
    return What;
  }
}

The PrintCommand class composes the RSchedulableCommand role, which requires an implementation for its abstract method Execute.

Roles can also implement interfaces. Classes that compose such roles will also compose those interface implementations. Let's look at a classic example in OO textbooks:

interface ISwitchable {
  void Toggle();
  bool IsOn();
}

role RSwitchable : ISwitchable {
  bool on = false;
  public void Toggle() {
    on = !on;
  }
  public void IsOn() {
    return on;
  }
}

The RSwitchable role provides a simple and common implementation for the ISwitchable interface. Without roles, classes that needed that default implementation would have to implement it themselves or create possibly unnatural, and certainly not ideal, hierarchical relationships. These two classes use that role:

class Light : RSwitchable { }

enum Strength { Low, Medium, High }
class Fan : RSwitchable { 
  public Strength Strength { get; set; }
}

And this class needs a different implementation:

class CompositeSwitcher : ISwitchable {
  ISet<ISwitchable> devices = new HashSet<ISwitchable>();
  bool on;
  public CompositeSwitcher(bool on = false) {
    this.on = on;
  }
  public void Register(ISwitchable device) {
    Conform(device);
    devices.Add(device);
  }
  void Conform(ISwitchable device) {
    if (device.IsOn() != IsOn()) {
      device.Toggle();
    }
  }
  public void Toggle() {
    foreach (var device in devices) {
      device.Toggle();
    }
    on = !on;
  }
  public void IsOn() {
    return on;
  }
}

In fact, the CompositeSwitcher class is so generic, that it should be made into a role:

role RCompositeSwitcher : ISwitchable { 
  // ...
  public RCompositeSwitcher(bool on = false) {
    this.on = on;
  }
  // ...
}

Now it can be used on more interesting compositions:

class MasterController : RCompositeSwitcher, RSchedulableCommand {  
  public MasterController(bool on = false) : 
    RCompositeSwitcher(on) {} // this is how you call a role constructor
  public void Execute() {
    Toggle();
  }
}

Now we can schedule the toggling of a number of devices:

var controller = new MasterController(on: false);
controller.Register(bedroomLight);
controller.Register(livingRoomFloorLamp);
controller.Register(patioLight);
// imagine we have some handy extension methods for dates:
controller.Schedule(DateTime.Now.At(19)); // switch on today at 19:00
controller.Schedule(DateTime.Now.NextDay().At(8)); // switch off tomorrow at 08:00

By using roles, MasterController composes the code it needs instead of implementing it all, manually delagating it, or relying on a base class. No single base class would be suitable for it. Should it inherit from a CompositeSwitcher base class, or from a SchedulableCommand class? By eliminating such confusing decisions, and making reusability painless, roles promote a superior, more maintainable design.

Conflict Resolution

Concrete role members don't override concrete members from other roles in a composition. If two concrete members are in conflict (same signature for methods, same name for other members), they generate a compile-time error. Conflicts must be resolved explicitly by the composing class. For these roles:

role R1 {
  public void M() {
    Console.WriteLine("R1::M");
  }
}
role R2 {
  public void M() {
    Console.WriteLine("R2::M");
  }
}

This class won't compile:

class C : R1, R2 { } // Error!

The C class composes two roles that have a conflicting method. To resolve the conflict, the class can supersede the role methods. It can still call the superseded members by using a C++ like syntax:

class C : R1, R2 { // OK
  public void M() {
    R1::M();
    R2::M();
  }
}

The conflict can also be resolved by some special operators in the composing class. Let's look at each one in turn:

Exclude

The class can exclude one of the methods, thus resolving the conflict:

class C : R1, R2 {
  R1 { // this is a role view for R1
    exclude void M();
  }
}

// R1::M is excluded from the class
C c = new C();
c.M();       // R2::M
((R1)c).M(); // R2::M
((R2)c).M(); // R2::M

If the class excludes all members involved in the conflict, a compile-time error is generated.

I call the syntax to define these operators inside the class a role view. A role view starts with a role name followed by a block, where the role members can be adapted, based on their signature. It's like an interface body, but with special annotations for each member. You also don't need parameter names, just their types.

Impose

As opposed to excluding, a class can impose a method, which will exclude all the other conflicting methods:

class C : R1, R2 {
  R1 { 
    impose void M();
  }
}

// R1::M is imposed over R2::M
// it has the same effect as excluding R2::M
C c = new C();
c.M();       // R1::M
((R1)c).M(); // R1::M
((R2)c).M(); // R1::M

If the class imposes two or more conflicting members, there's still a conflict.

Alias

The class can also alias one of the methods:

class C : R1, R2 {
  R1 { 
    alias(AliasedM) void M();
    // or: aliasing(M) void AliasedM();
  }
}

// R1::M is aliased in C as C::AliasedM
C c = new C();
c.M();        // R2::M
c.AliasedM(); // R1::M
((R1)c).M();  // R1::M
((R2)c).M();  // R2::M

You can alias all conflicting members. Aliased members can still conflict with other members though.

Hide

Finally, the class can hide one of the conflicting methods (it can even hide them all). This is akin to explicit interface implementations:

class C : R1, R2 {
  R1 { 
    hide void M();
  }
}

// R1::M is hidden in C
C c = new C();
c.M();        // R2::M
((R1)c).M();  // R1::M
((R2)c).M();  // R2::M

Composing roles at instantiation time

Roles can also be composed in a class (not in a struct though) at instantiation time:

var sourceAccount = new CheckingAccount() : RMoneySource;
var destinationAccount = new SavingsAccount() : RMoneySink;
sourceAccount.TransferTo(destinationAccount, amount);

// ...

RCompositeSwitcher switcher = 
  new object() : RCompositeSwitcher(on: false); // parameterized constructor

If there are conflicts, though, they still need to be resolved. A role view is the only option here, and its block follows the corresponding role name (or role constructor call):

var d = new D() : 
  R1 { 
    hide void M(); 
  }, 
  R2,
  R3(someConstructorParameter) {
    alias(NewName) string OldName { get; }
  };

This gives an interesting and powerful way to organize the code, where classes only play certain roles depending on the current execution context. The alternative would be to pollute a class static interface with many roles that wouldn't be applicable to many of its usages.

The types of these dynamically composed classes are anonymous and generated by the compiler. They're implicitly convertible to the classes or roles in the corresponding expressions.

Self-type constraints

A role might require that types that compose it be of a certain type:

role R where this : I {
}

The role R can only be composed into classes that implement the I interface. This is similar to a generic type constraint, and I call it a self-type constraint. A self-type constraint cannot be of a generic type parameter, though:

role R1<T> where this : T { } // Error!
role R2<T> where this : IList<T> { } // OK
role R3<T> where this : List<T>, IFoo, IBar { } // OK

The special annotations class, struct and new(), that can occur in generic type constraints, are also not allowed. Only types (classes, interfaces and roles, but not structs) can be used as self-type constraints.

A role with a self-type constraint can access the visible members of the constrained types in its body, and it's implicitly convertible to their types.

To compose a role with this kind of constraint, a class must fulfill it:

interface I { }
role R where this : I { }
class C : R { } // Error!
class D : R, I { } // OK!

Final thoughts

Roles are the missing link in mainstream object oriented programming languages, and they will create a new revolution in object oriented design. They will make composition as easy as inheritance and enable unprecedented reuse. The DCI (Data, Context, Interactions) architecture already promotes them as first class citizens in the analysis, design and implementation space. There are many more details worth mentioning, but I hope this post is enough to understand them and to see how they would work in C#.

Update: I created a follow up post with a possible implementation strategy for roles in C#.

Comments

Popular posts from this blog

The Acyclic Visitor Pattern

Some OO Design

NRoles: An experiment with roles in C#