NRoles: An experiment with roles in C#
In the last months, I've been developing in my spare time a proof-of-concept post-compiler with Mono.Cecil to enable roles in C#: NRoles. Roles are high level constructs that enable better code reuse through easier composition. They are very similar to traits as described in the traits paper (pdf).
Set up
NRoles is hosted on google code. You can get the latest source code with this mercurial command:
hg clone https://code.google.com/p/nroles/ nroles
You can also download the latest binary package from the downloads page.
To set up a Visual Studio project to use NRoles, unzip the binary package (or compile from source) in a folder accessible to the project. I normally have a project root folder, with folders for the project code (src) and for any used libraries (lib). For example:
src\ project files ... lib\ nroles-v0.1.2-bin\ NRoles.dll nutate.exe other NRoles binaries ...
In the project, add a reference to the NRoles.dll assembly. This is a very minimal assembly used to annotate the code to enable the post-compiler to create roles and compositions. Then, add a post-build event to the project to run the post-compiler (nutate.exe):
"$(SolutionDir)..\lib\nroles-v0.1.2-bin\nutate.exe" "$(TargetPath)"
Now nutate.exe runs whenever the project is built, and any errors or warnings are reported in the Visual Studio error list view.
Roles and compositions
Let's take a look at some code based on the examples in the traits paper. Roles are classes marked with the interface NRoles.Role
. This is a simple role that implements IEquatable<T>
and provides a Differs
method:
public abstract class REquatable<T> : IEquatable<T>, Role {
public abstract bool Equals(T other);
public bool Differs(T other) {
return !Equals(other);
}
}
To distinguish roles from normal classes or interfaces, I prefix them with the letter R
. REquatable<T>
provides the Differs
method, but still requires classes that compose it to implement Equals
, since it's abstract.
To compose a role, a class uses the NRoles.Does<TRole>
marker interface, with the desired role as a type parameter. A role can also compose other roles. This is a role that implements IComparable<T>
and composes REquatable<T>
:
public abstract class RComparable<T> : IComparable<T>, Does<REquatable<T>>, Role {
public bool Equals(T other) {
return CompareTo(other) == 0;
}
public abstract int CompareTo(T other);
public bool LessThan(T other) {
return CompareTo(other) < 0;
}
public bool GreaterThan(T other) {
return CompareTo(other) > 0;
}
public bool LessThanOrEqualTo(T other) {
return CompareTo(other) <= 0;
}
public bool GreaterThanOrEqualTo(T other) {
return CompareTo(other) >= 0;
}
public bool IsBetween(T min, T max) {
return GreaterThan(min) && LessThan(max);
}
}
Classes that compose RComparable<T>
will also compose REquatable<T>
and gain the implementations of the corresponding IComparable<T>
and IEquatable<T>
interfaces. RComparable<T>
"implements" REquatable<T>.Equals
in terms of CompareTo
. CompareTo
is left abstract and needs to be implemented by further composing classes.
The following role represents a circle and composes REquatable<T>
and RComparable<T>
:
public class RCircle : Does<REquatable<RCircle>>, Does<RComparable<RCircle>>, Role {
public int Radius { get; set; }
public double Area { get { return Math.PI * Radius * Radius; } }
public int CompareTo(RCircle other) {
if (other == null) throw new ArgumentNullException("other");
return Radius.CompareTo(other.Radius);
}
}
All roles in a composition have the same priority. This includes all roles declared in the composition itself and also these roles' roles, and so on. Any role will only appear once in a composition, even if it was composed multiple times through different "paths". This way, it's very different from multiple inheritance, which requires you to consider the whole hierarchy and places different priorities in each class. As a result, conflicting members from multiple roles must be resolved by the composition (more on this here), which is also different from the way that mixin inheritance normally works, where the composition order defines the relative priorities of the mixins.
This simple composition represents the RGB (red, green and blue) values for a color:
public class Rgb : Does<REquatable<Rgb>> {
public int R { get; set; }
public int G { get; set; }
public int B { get; set; }
public bool Equals(Rgb other) {
return R == other.R && G == other.G && B == other.B;
}
}
And this role uses the Rgb
composition and represents a color:
public class RColor : Does<REquatable<RColor>>, Role {
public Rgb Rgb { get; set; }
public bool Equals(RColor other) {
return Rgb.Equals(other.Rgb);
}
}
The next composition is a circle, and uses RCircle
, RColor
and REquatable<T>
:
public class Circle : Does<RCircle>, Does<RColor>, Does<REquatable<Circle>> {
public bool Equals(Circle other) {
if (other == null) throw new ArgumentNullException("other");
return
this.As<RCircle>().Equals(other.As<RCircle>()) &&
this.As<RColor>().Equals(other.As<RColor>());
}
}
Circle
has an Equals
method that reuses the implementations from RCircle
and RColor
. Since NRoles works as a post-compiler, to use compositions in the same assembly that they're defined, the As<TRole>()
extension method casts the composition type to one of its composed roles. This operation is always safe (provided that you post-compile the assembly). You don't need to use this trick in other assemblies, since the compositions are implicitly convertible to the roles that they compose. The roles become interfaces that the compositions implement (look here for the conceptual idea).
The Circle
class ends up composing many behaviors from different roles, hopefully giving a taste of how roles improve the overall separation of concerns of the design.
NRoles is an experiment in a very early stage. Ideally, roles would be available in the language itself. But, I hope it can already be useful to create better designs; and maybe to show language designers that this is an important concept. So please, download and play with it, and comment on any issues and possibilities. And of course: contribute!
This is ridiculously awesome. Nice work!
ReplyDelete